单例模式
1. 饿汉式
1 | public class EHan { |
优点:没有线程安全问题
缺点:在初始化时就创建好了,浪费内存空间
2. 懒汉式
2.1 线程不安全
1 | public class LHan { |
优点:只有当用的时候才检查是否有实例,没有才创建
缺点:有线程安全与不安全两种,区别在于是否有 synchronized 关键字
2.2 线程安全
1 | public class LHan { |
3. DCL
由于线程安全的懒汉式的 synchronized 是加在方法上的,如果该方法里还有其他的一些代码,会降低执行效率,加锁的粒度太粗,所以可以进而改写下面这个
3.1 懒汉式优化后的一种写法
1 | public class LHan { |
但是这种写法也存在一定问题,当线程A在判断instance==null后停住了,此时还没有创建实例;线程B抢到了资源,发现instance==null,也会进入到代码块,于是A和B都会创建一个实例。
3.2 DCL 单例模式
Double Check Lock 两次检查,中间插入一个lock
1 | public class LHan { |
有可能会存在,A在第一层判断结束后停住,B进入同步代码块,new一个对象实例,进行一定操作后,把instance置为null,然后A进入同步代码块,会再次new一个对象。解决:添加版本号。
3.2.1 关于重排序的一个解答
1 | public class Singleton { |
如上代码段中的注释:假设线程一执行到instance = new Singleton()这句,这里看起来是一句话,但实际上其被编译后在JVM执行的对应会变代码就发现,这句话被编译成8条汇编指令,大致做了三件事情:
1)给instance实例分配内存;
2)初始化instance的构造器;
3)将instance对象指向分配的内存空间(注意到这步时instance就非null了)
如果指令按照顺序执行倒也无妨,但JVM为了优化指令,提高程序运行效率,允许指令重排序。如此,在程序真正运行时以上指令执行顺序可能是这样的:
a)给instance实例分配内存;
b)将instance对象指向分配的内存空间;
c)初始化instance的构造器;
这时候,当线程一执行b)完毕,在执行c)之前,被切换到线程二上,这时候instance判断为非空,此时线程二直接来到return instance语句,拿走instance然后使用,接着就顺理成章地报错(对象尚未初始化)。
具体来说就是synchronized虽然保证了线程的原子性(即synchronized块中的语句要么全部执行,要么一条也不执行),但单条语句编译后形成的指令并不是一个原子操作(即可能该条语句的部分指令未得到执行,就被切换到另一个线程了)。
根据以上分析可知,解决这个问题的方法是:禁止指令重排序优化,即使用volatile变量。
4. 静态内部类
1 | public class Singleton { |
静态内部类的方式效果类似双检锁,但实现更简单。但这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。