如何书写线程安全的单例实现?
在某些场景下只需要初始化一个对象一次,在后面需要使用的时候直接获取之前初始化的对象,这可以借助单例模式编程模帮助我们来实现这个需求,常见的单例模式有饿汉式和懒汉式的实现方式,还有通过枚举的方式实现单例,今天就将单例模式的实现进行简单的总结并就其线程安全性进行浅析,有不妥的地方还请大家批评指正。所有代码实例皆以JAVA进行实现。
一、懒汉式实现
private SingleronExample2(){} // 提供私有的构造方法防止在外部实例化
private static SingleronExample2 instance = null;
public static SingleronExample2 getInstance(){
if (instance == null){ // 判断是否已经初始化
instance = new SingleronExample2();
}
return instance;
}
以上是懒汉式核心代码,懒汉式顾名思义,在需要的时候才初始化需要的目标对象,懒汉式的实现是线程不安全的,来看看线程不安全的原因,在多线程并发访问的情况下,多个线程都来获取单例对象(第一次获取),同时运行到了if (instance == null)这段代码,发现instance == null,然后两个访问都会执行new SingleronExample2();这就实例化两个对象,无法满足单例的需求。
那么单例模式是否可以通过改进实现线程安全呢,答案是肯定的,可以通过加锁的方式来实现懒汉式的线程安全,下面就来看一下一个实现。
private SingleronExample5(){}
private static SingleronExample5 instance = null;
private static SingleronExample5 getInstance(){
if (instance == null){
synchronized(SingleronExample5.class){
if (instance == null){
instance = new SingleronExample5();
}
}
}
return instance;
}
来看看上面这段代码,通过synchronized关键字对SingleronExample5.class加锁,使用双重检测机制判断单例对象是否已经实例化,加锁后在同一个时刻只能有一个线程获取锁,并执行加锁代码块中的内容进行对象实例化,这样可以保证在大部分情况下的单例对象不会被重复初始化,但仍然不是完全线程安全的,来看看是什么原因呢?
这就需要谈到JVM和cpu的指令重排序机制,编译源代码时,编译器依据对上下文的分析,对指令进行重排序,以更适合于CPU的并行执行。一个对象实例化的步骤为:
(1)memory = allocate()分配内存空间
(3)instanance = memory 设置instance纸箱刚刚分配的内存
(2)ctorInstannce初始化对象
在初始化时如果一个线程刚好执行了(3)后另外一个线程立马走到了第一个检测的位置:if (instance == null),此时instance != null,便直接返回了instance而此时的instance==null。并没有被实例化完成。
我们可以统一个关键字来修改单例对象volatile,java虚拟机中对volatile关键字描述的是:(1)内存可见性;(2)禁止指令重排序
什么是内存可见性? 我们知道对于普通的非volatile的变量,CPU每次在使用时会从内存中拷贝一份到CPU cache中,对变量的操作(读、写)都是操作的拷贝后的副本,然后再同步到内存中,而对于被volatile修饰的对象如果你状态或者值发生变更,能够立马被其它线程所看见,volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,保证了每次读写变量都从主内存中读,跳过CPU cache这一步。当一个线程修改了这个变量的值,新值对于其他线程是立即得知的。
禁止指令重排序?前面提到了编译源代码时,编译器依据对上下文的分析,对指令进行重排序,以更适合于CPU的并行执行。被volatile修饰的变量在编译时其实例化的指令不会被重排序。
所以将单例对象再使用volatile修饰后便可以保证饿汉实现的方式线程安全了。
private SingleronExample5(){}
private static volatile SingleronExample5 instance = null;
private static SingleronExample5 getInstance(){
if (instance == null){
synchronized(SingleronExample5.class){
if (instance == null){
instance = new SingleronExample5();
}
}
}
return instance;
}
加锁出了使用synchronized(虚拟机级别的锁,JVM来帮助我们是加锁和解锁机制,方便好用)还可以使用ReentrantLock来实现加锁和解锁。
private SingleronExample5(){}
prvate static Lock lock = new ReentrantLock();
private static volatile SingleronExample5 instance = null;
private static SingleronExample5 getInstance(){
if (instance == null){
lock.lock(); // 加锁
try { if (instance == null){
instance = new SingleronExample5();
}
} finally {
Lock.unlock(); // 解锁
}
}
}
return instance;
}
二、饿汉式实现
private SingleronExample1(){}
private static SingleronExample1 instance = new SingleronExample1();
public static SingleronExample1 getInstance(){
return instance;
}
上面的代码便是饿汉式的单例实现,从实现上来看非常的简单,可以看到饿汉式采用的是将类加载时就将到单例对象实例化了,在需要使用时直接返回类加载时实例化的单例对象,它是线程安全的。
三、过枚举的方式实现单例
private SingleronExample4(){}
public static SingleronExample4 getInstance(){
return Singleton.INSTANCE.getSingleton();
}
private enum Singleton{
INSTANCE;
private SingleronExample4 singleton;
// JVM会保证这个方法绝对只是被调用一次
Singleton(){
singleton = new SingleronExample4();
}
public SingleronExample4 getSingleton() {
return singleton;
}
}
枚举的实现仅给出一个代码实现,枚举的构造函数Singleton()是JVM保证其只会被执行一次。