double check机制真的是完美的吗?

版权声明:本文为博主原创文章,转载请注明原文链接哦,forethought.top! https://blog.csdn.net/qq_36922927/article/details/84977365


单例我们是经常会用到的一种设计模式,为了减少对象的创建。
一个类有且仅有一个实例,并且自行实例化向整个系统提供。
但是在多线程的情况下,我们不得不考虑如何保证正确高效的获得单例对象。

1 单例的一般写法:

(这里以懒汉为例)

public class Singleton(){
private static Singleton instance;
private Singleton(){
}
public static  Singleton getInstance(){
if(null==instance){//1
instance=new Singleton();//2
}
return instance;//3
}



}

弊端:

如果线程a即将执行2,线程b执行到1,那么线程b也可能进入到2去实例化一个Singleton对象,最终导致两个线程获取到的不是同一个对象

2 synchronized改进:

public  synchronized  static Singleton  getInstance(){
if(null==instance){//1
instance=new Singleton();//2
}
return instance;//3
}

使用synchronized关键字给getInstance方法加锁,保证每个线程能获得同一个对象

弊端:

造成性能开销,毕竟只会出现一次实例化单例对象,99.9999%都是只是获取该对象

3 double check

public  synchronized  static Singleton  getInstance(){
if(null==instance){//1
synchronized(Singleton.class){//2
if(null==instance){//3
instance=new Singleton();//4
                      }    
                             }
}
      return instance;//5
}

这里就进行了两次检测 2和3
这样就减少了synchronized生效次数,减少了性能开销,而且还能保证“正确”的只创建一个对象??

弊端:

表面上看,是很完美的,但是创建对象并不是一蹴而就的,也会有耗时的操作。线程b执行到1,有可能引用的对象还没完成初始化!!

instance=new Singleton()
相当于是:
memory=allocate();//1申请内存
ctorInstance(memory);//2 初始化内存
instance=memory;// 3,指针 指向内存

这1,2,3可能会发生指令重排,编程
1,3,2 显然这个重排对单线程是没有影响的,但是在多线程这里,就可能导致
线程a完成了1,3 也就是instance已经指向了一块没被初始化的内存,线程b此时就能访问到instance,导致线程b拿到一个不完整的对象!!!!(没初始化完成)

java语言规范第7版本中:
所有线程在执行java程序是必须遵守intra-thread semantics.这个保证了重排序不改变单线程内的执行结果。
也就是说这一规范允许单线程内不改变执行结果的前提下,可以发生指令重排。

4 正确的改进:

思路有2:

1,禁用重排序,即不允许2,3重排序
2,允许2,3重排序,但是不能让其他线程看见这个重排序(也就是不能让其他线程访问重排序引起的中间状态)

思路1:可以使用volatile关键字,来禁用指令重排

private volatile static Singleton instance;

思路2:基于类初始化

JVM在类的初始化阶段,(即Class被加载之后,且被线程使用之前),会执行类的初始化。
在类的初始化期间,JVM会去获得一个锁。这个锁可以同步多个线程对同一个类的初始化。

public  class Singleton(){
private static class InstanceHolder{
public static Instance instance=new Instance();
                      }
}
public static Singleton getInstance(){
return InstanceHolder.insatnce;//这里将导致内部类InstanceHolder初始化
}

}

初始化一个类,包括这个类的静态初始化和初始化在这个类中声明的静态字段。
根据java语言规范,一个类或接口类型T在下列情况会被立即初始化
1,T是一个类,而且一个T类型的对象被创建
2,T是一个类,而且T中的一个静态方法被调用
3,T中声明的一个静态字段被赋值
4,T中声明的一个静态字段被使用,而且这个字段不是一个常量字段
5,T是一个顶级类(Top Level Class),而且一个断言语句嵌套在T内部被执行(不理解)

上面的思路2即是满足了这里的第4种情况。

总结:

1,单例一般的写法导致线程安全问题,
2,单例的synchronized对getInstance加锁,导致性能问题
3,双重检测机制可能导致获取到未被初始化完成的对象
4,正确的double check 应该考虑对象初始化这个耗时的过程能否正常(及时)完成

猜你喜欢

转载自blog.csdn.net/qq_36922927/article/details/84977365