做者:小小木的博客
www.cnblogs.com/wyc1994666/p/11394755.html
html
1. 单例模式常见问题
为何要有单例模式java
单例模式是一种设计模式,它限制了实例化一个对象的行为,始终至多只有一个实例。当只须要一个对象来协调整个系统的操做时,这种模式就很是有用.它描述了如何解决重复出现的设计问题,面试
好比咱们项目中的配置工具类,日志工具类等等。后端
如何设计单例模式 ?设计模式
1.单例类如何控制其实例化安全
2.如何确保只有一个实例多线程
经过一下措施解决这些问题:架构
private构造函数,类的实例话不对外开放,由本身内部来完成这个操做,确保永远不会从类外部实例化类,避免外部随意new出来新的实例。并发
该实例一般存储为私有静态变量,提供一个静态方法,返回对实例的引用。若是是在多线程环境下则用锁或者内部类来解决线程安全性问题。函数
2. 单例类有哪些特色 ?
私有构造函数
它将阻止从类外部实例化新对象
它应该只有一个实例
这是经过在类中提供实例来方法完成的,阻止外部类或子类来建立实例。这是经过在java中使构造函数私有来完成的,这样任何类都不能访问构造函数,所以没法实例化它。
单实例应该是全局可访问的
单例类的实例应该是全局可访问的,以便每一个类均可以使用它。在Java中,它是经过使实例的访问说明符为public来完成的。
节省内存,减小GC
由于是全局至多只有一个实例,避免了处处new对象,形成浪费内存,以及GC,有了单例模式能够避免这些问题。
3. 单例模式8种写法
下面由我给你们介绍8种单例模式的写法,各有千秋,存在即合理,经过本身的使用场景选一款使用便可。咱们选择单例模式时的挑选标准或者说评估一种单例模式写法的优劣时一般会根据一下两种因素来衡量:
1.在多线程环境下行为是否线程安全
2.饿汉以及懒汉
3.编码是否优雅(理解起来是否比较直观)
1. 饿汉式线程安全的
public class SingleTon{ private static final SingleTon INSTANCE = new SingleTon(); private SingleTon(){ } public static SingleTon getInstance(){ return INSTANCE; } public static void main(String[] args) { SingleTon instance1 = SingleTon.getInstance(); SingleTon instance2 = SingleTon.getInstance(); System.out.println(instance1 == instance2); } }
这种写法是很是简单实用的,值得推荐,惟一缺点就是懒汉式的,也就是说不论是否须要用到这个方法,当类加载的时候都会生成一个对象。
除此以外,这种写法是线程安全的。类加载到内存后,就实例化一个单例,JVM保证线程安全。关注公众号Java技术栈回复设计模式获取我整理的系列Java设计模式教程。
2. 饿汉式线程安全(变种写法)。
public class SingleTon{ private static final SingleTon INSTANCE ; static { INSTANCE = new SingleTon(); } private SingleTon(){} public static SingleTon getInstance(){ return INSTANCE; } public static void main(String[] args) { SingleTon instance1 = SingleTon.getInstance(); SingleTon instance2 = SingleTon.getInstance(); System.out.println(instance1 == instance2); } }
3. 懒汉式线程不安全。
public class SingleTon{ private static SingleTon instance ; private SingleTon(){} public static SingleTon getInstance(){ if(instance == null){ instance = new SingleTon(); } return instance; } public static void main(String[] args) { SingleTon instance1 = SingleTon.getInstance(); SingleTon instance2 = SingleTon.getInstance(); System.out.println(instance1 == instance2); // 经过开启100个线程 比较是不是相同对象 for(int i=0;i<100;i++){ new Thread(()-> System.out.println(SingleTon.getInstance().hashCode()) ).start(); } } }
这种写法虽然达到了按需初始化的目的,但却带来线程不安全的问题,至于为何在并发状况下上述的例子是不安全的呢 ?
// 经过开启100个线程 比较是不是相同对象 for(int i=0;i<100;i++){ new Thread(()-> System.out.println(SingleTon.getInstance().hashCode()) ).start(); }
为了使效果更直观一点咱们对getInstance 方法稍作修改,每一个线程进入以后休眠一毫秒,这样作的目的是为了每一个线程都尽量得到cpu时间片去执行。代码以下
public static SingleTon getInstance(){ if(instance == null){ try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } instance = new SingleTon(); } return instance; }
执行结果以下
上述的单例写法,咱们是能够创造出多个实例的,至于为何在这里要稍微解释一下,这里涉及了同步问题
形成线程不安全的缘由:
当并发访问的时候,第一个调用getInstance方法的线程t1,在判断完singleton是null的时候,线程A就进入了if块准备创造实例,可是同时另一个线程B在线程A还未创造出实例以前,就又进行了singleton是否为null的判断,这时singleton依然为null,因此线程B也会进入if块去创造实例,这时问题就出来了,有两个线程都进入了if块去创造实例,结果就形成单例模式并不是单例。
注:这里经过休眠一毫秒来模拟线程挂起,为初始化完instance
为了解决这个问题,咱们能够采起加锁措施,因此有了下面这种写法
4. 懒汉式线程安全(粗粒度Synchronized)。
public class SingleTon{ private static SingleTon instance ; private SingleTon(){} public static SingleTon synchronized getInstance(){ if(instance == null){ instance = new SingleTon(); } return instance; } public static void main(String[] args) { SingleTon instance1 = SingleTon.getInstance(); SingleTon instance2 = SingleTon.getInstance(); System.out.println(instance1 == instance2); // 经过开启100个线程 比较是不是相同对象 for(int i=0;i<100;i++){ new Thread(()-> System.out.println(SingleTon.getInstance().hashCode()) ).start(); } } }
因为第三种方式出现了线程不安全的问题,因此对getInstance方法加了synchronized来保证多线程环境下的线程安全性问题,这种作法虽解决了多线程问题可是效率比较低。
由于锁住了整个方法,其余进入的现成都只能阻塞等待了,这样会形成不少无谓的等待。
因而可能有人会想到可不可让锁的粒度更细一点,只锁住相关代码块能否?因此有了第五种写法。关注公众号Java技术栈回复多线程获取我整理的系列Java多线程教程。
5. 懒汉式线程不安全(synchronized代码块)
public class SingleTon{ private static SingleTon instance ; private SingleTon(){} public static SingleTon getInstance(){ if(insatnce == null){ synchronied(SingleTon.class){ instance = new SingleTon(); } } return instance; } public static void main(String[] args) { SingleTon instance1 = SingleTon.getInstance(); SingleTon instance2 = SingleTon.getInstance(); System.out.println(instance1 == instance2); // 经过开启100个线程 比较是不是相同对象 for(int i=0;i<100;i++){ new Thread(()-> System.out.println(SingleTon.getInstance().hashCode()) ).start(); } } }
当并发访问的时候,第一个调用getInstance方法的线程t1,在判断完instance是null的时候,线程A就进入了if块而且持有了synchronized锁,可是同时另一个线程t2在线程t1还未创造出实例以前,就又进行了instance是否为null的判断,这时instance依然为null,因此线程t2也会进入if块去创造实例,他会在synchronized代码外面阻塞等待,直到t1释放锁,这时问题就出来了,有两个线程都实例化了新的对象。
形成这个问题的缘由就是线程进入了if块而且在等待synchronized锁的过程当中有可能上一个线程已经建立了实例,因此进入synchronized代码块以后还须要在判断一次,因而有了下面这种双重检验锁的写法。
6. 懒汉式线程安全(双重检验加锁)
public class SingleTon{ private static volatile SingleTon instance ; private SingleTon(){} public static SingleTon getInstance(){ if(instance == null){ synchronied(SingleTon.class){ if(instance == null){ instance = new SingleTon(); } } } return instance; } public static void main(String[] args) { SingleTon instance1 = SingleTon.getInstance(); SingleTon instance2 = SingleTon.getInstance(); System.out.println(instance1 == instance2); // 经过开启100个线程 比较是不是相同对象 for(int i=0;i<100;i++){ new Thread(()-> System.out.println(SingleTon.getInstance().hashCode()) ).start(); } } }
这种写法基本趋于完美了,可是可能须要对一下几点须要进行解释:
-
第一个判空(外层)的做用 ?
-
第二个判空(内层)的做用 ?
-
为何变量修饰为volatile ?
第一个判空(外层)的做用
首先,思考一下可不能够去掉最外层的判断?答案是:能够
其实仔细观察以后会发现最外层的判断跟可否线程安全正确生成单例无关!!!
它的做用是避免每次进来都要加锁或者等待锁,有了同步代码块以外的判断以后省了不少事,当咱们的单例类实例化一个单例以后其余后续的全部请求都不必在进入同步代码块继续往下执行了,直接返回咱们曾生成的实例便可,也就是实例还未建立时才进行同步,不然就直接返回,这样就节省了不少无谓的线程等待时间,因此最外的判断能够认为是对提高性能有帮助。
第二个判空(内层)的做用
假设咱们去掉同步块中的是否为null的判断,有这样一种状况,A线程和B线程都在同步块外面判断了instance为null,结果t1线程首先得到了线程锁,进入了同步块,而后t1线程会创造一个实例,此时instance已经被赋予了实例,t1线程退出同步块,直接返回了第一个创造的实例,此时t2线程得到线程锁,也进入同步块,此时t1线程其实已经创造好了实例,t2线程正常状况应该直接返回的,可是由于同步块里没有判断是否为null,直接就是一条建立实例的语句,因此t2线程也会创造一个实例返回,此时就形成创造了多个实例的状况。
由于虚拟机在执行建立实例的这一步操做的时候,实际上是分了好几步去进行的,也就是说建立一个新的对象并不是是原子性操做。在有些JVM中上述作法是没有问题的,可是有些状况下是会形成莫名的错误。关注公众号Java技术栈回复JVM获取我整理的系列JVM教程。
首先要明白在JVM建立新的对象时,主要要通过三步。
1.分配内存
2.初始化构造器
3.将对象指向分配的内存的地址
由于仅仅一个new 新实例的操做就涉及三个子操做,因此生成对象的操做不是原子操做。
而实际状况是,JVM会对以上三个指令进行调优,其中有一项就是调整指令的执行顺序(该操做由JIT编译器来完成)。46张PPT弄懂JVM性能调优,这篇推荐看下。
因此,在指令被排序的状况下可能会出现问题,假如 2和3的步骤是相反的,先将分配好的内存地址指给instance,而后再进行初始化构造器,这时候后面的线程去请求getInstance方法时,会认为instance对象已经实例化了,直接返回一个引用。
若是这时还没进行构造器初始化而且这个线程使用了instance的话,则会出现线程会指向一个未初始化构造器的对象现象,从而发生错误。
7. 静态内部类的方式(基本完美了)
public class SingleTon{ public static SingleTon getInstance(){ return StaticSingleTon.instance; } private static class StaticSingleTon{ private static final SingleTon instance = new SingleTon(); } public static void main(String[] args) { SingleTon instance1 = SingleTon.getInstance(); SingleTon instance2 = SingleTon.getInstance(); System.out.println(instance1 == instance2); // 经过开启100个线程 比较是不是相同对象 for(int i=0;i<100;i++){ new Thread(()-> System.out.println(SingleTon.getInstance().hashCode()) ).start(); } } }
-
由于一个类的静态属性只会在第一次加载类时初始化,这是JVM帮咱们保证的,因此咱们无需担忧并发访问的问题。因此在初始化进行一半的时候,别的线程是没法使用的,由于JVM会帮咱们强行同步这个过程。
-
另外因为静态变量只初始化一次,因此singleton仍然是单例的。
8. 枚举类型的单例模式(太完美以致于。。。)
public Enum SingleTon{ INSTANCE; public static void main(String[] args) { // 经过开启100个线程 比较是不是相同对象 for(int i=0;i<100;i++){ new Thread(()-> System.out.println(SingleTon.getInstance().hashCode()) ).start(); } } }
这种写法从语法上看来是完美的,他解决了上面7种写法都有的问题,就是咱们能够经过反射能够生成新的实例。可是枚举的这种写法是没法经过反射来生成新的实例,由于枚举没有public构造方法。
关注公众号Java技术栈回复"面试"获取我整理的2020最全面试题及答案。
推荐去个人博客阅读更多:
2.Spring MVC、Spring Boot、Spring Cloud 系列教程
3.Maven、Git、Eclipse、Intellij IDEA 系列工具教程
以为不错,别忘了点赞+转发哦!