多线程下单例模式存在的问题
单例模式相信很多人都了解过,不过对于初学者来说,单例模式主要是在单线程中的操作,事实上很多问题只要涉及到了多线程当中,就会暴露出许多问题,同样的,单例模式如果应用到多线程当中,那它就不叫单例模式了,不信?让我们来瞧一瞧。
首先编写一个单例模式对于大家来讲应该是很简单的事了,那么在测试方法中,我们来开启多个线程,来获取我们单例模式new出来的对象回怎样呢?我们来看下面一段代码:
public class SingletonDemo {
private static SingletonDemo instance=null;
private SingletonDemo(){
System.out.println(Thread.currentThread().getName()+" 我是构造方法SingletonDemo。。。");
}
private static SingletonDemo getInstance(){
if (instance==null){
instance = new SingletonDemo();
}
return instance;
}
public static void main(String[] args) {
//开启多个线程
for (int i =1; i <=20; i++) {
new Thread(() -> SingletonDemo.getInstance(),String.valueOf(i)).start();
}
}
}
乍一看代码似乎没什么毛病,我们来运行一下
1 我是构造方法SingletonDemo。。。
2 我是构造方法SingletonDemo。。。
4 我是构造方法SingletonDemo。。。
3 我是构造方法SingletonDemo。。。
Process finished with exit code 0
哎?不是单例模式吗?为什么会有多个构造方法执行了?这就是多线程的神(wu)奇(yv)之处,在多线程环境下,每个线程都是抢着进行,大家都是在并行执行,在上面代码中,首先执行线程一,在判断完实例对象为null时或者正在new一个实例对象的时候,线程二抢到了资源,所以线程二开始执行,而在线程二执行的时候,有可能又会被其他线程抢占资源而执行下去,所以就会造成,构造方法被执行了多次,实例对象被new了多次。
如何解决这种问题呢?
会有人想到在get方法前加synchronized修饰,这肯定是可以的,但是这样加,我们程序的并发性就会降低很多,我们只是因为创建一个实例对象这一句代码而出现的问题,那么我们就要在这句代码上动点文章了。
那么对于这种问题,可以通过DCL(Double Check Lock)双端检锁机制来解决,也就是下面这段代码:
public class SingletonDemo {
private static SingletonDemo instance=null;
private SingletonDemo(){
System.out.println(Thread.currentThread().getName()+" 我是构造方法SingletonDemo。。。");
}
private static SingletonDemo getInstance(){
if (instance==null){
synchronized (SingletonDemo.class) {
if (instance==null) {
instance = new SingletonDemo();
}
}
}
return instance;
}
public static void main(String[] args) {
//开启多个线程
for (int i =1; i <=20; i++) {
new Thread(() -> SingletonDemo.getInstance(),String.valueOf(i)).start();
}
}
}
如果第一次检查实例对象部位null,则返回已实例化的对象,也就不需要执行下面的锁和初始化操作,这样做确实大大减少了性能开销,并且通过加锁,避免了多个线程去实例对象,但是仅仅这样就可以了吗?在大多数情况下,我们写这样的程序出来的结果是符合我们的预期的,但是其实实际上,还是有漏洞的,在我们的程序被java虚拟机编译的时候,是被写为字节码来执行的,而我们new一个实例对象,在我们的程序当中是一行代码。但编译器实现的时候却是这样:
memory = allocate(); 1
ctorInstance(memory); 2
instance = memory; 3
第二步和第三步之间没有数据依赖,所以有可能在编译器上会被重排序,就会导致出现下面这种情况:
memory = allocate(); 1
instance = memory; 3
ctorInstance(memory); 2
第三步被提前了!可是这个时候只是分配了内存地址,我的对象没有被初始化啊!但是现在如果线程二来执行第一步验证,这时候回显示instance!=null,所以B线程就继续往下了,那么他返回的会是一个没有被初始化的对象,这个时候,volatile关键字就来了,因为什么呢?volatile关键字有一个特性,那就是:禁止指令重排。不允许编译器重排序。我们在刚开始为实例null值的instance加上volatile关键字修饰,上面的问题就可以得到解决。