应用场景:我们需要一个单例模式:一个类有且仅有一个实例,并且自行实例化向整个系统提供。
我们先看单例模式里面的懒汉式:
public class Singleton { private static Singleton singleton;
private Singleton(){}
public static Singleton getInstance(){ if(singleton == null){ singleton = new Singleton(); }
return singleton; } } 我们都知道这种写法是错误的,因为它无法保证线程的安全性。为了确保安全性,就是在getInstance方法上面做了同步,但是synchronized就会导致这个方法比较低效,导致程序性能下降。
因此我们把synchronized放在方法同步为:DCL 即Double Check Lock,中卫双重检查锁定。
public class Singleton {
private static Singleton singleton;
private Singleton(){}
public static Singleton getInstance(){
if(singleton == null){ // 1
synchronized (Singleton.class){ // 2
if(singleton == null){ // 3
singleton = new Singleton(); // 4
}
}
}
return singleton;
}
}
就如上面所示,DCL:A 线程进行判空检查之后开始执行synchronized代码块时发生线程切换(线程切换可能发生在任何时候),B 线程也进行判空检查,B线程检查 singleton == null 结果为true,也开始执行synchronized代码块,虽然synchronized 会让二个线程串行执行,如果synchronized代码块内部不进行二次判空检查,singleton 可能会初始化二次。
这个代码看起来很完美,理由如下:
但是这种DCL方式其实是有缺陷的,具体什么缺陷呢?我们先看看happen-before原则
Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程的程序执行,却会影响到多线程并发执行的正确性。可以通过volatile、synchronize、Lock来保证一定的有序性。
JMM中具有一定先天的有序性,不需要任何手段就能保证有序性,这就是我们经常见到的happens-before原则。重排序需要遵守happens-before规则,不能说你想怎么排就怎么排。
Happen-Before八大原则:
1)单线程happen-before原则(程序顺序规则):在同一个线程中,书写在前面的操作happen-before后面的操作。 这条规则是说,在单线程中操作间happen-before关系完全是由源代码的顺序决定的,这里的前提“在同一个线程中”是很重要的。 2)锁的happen-before原则(锁管理原则):同一个锁的unlock操作happen-before此锁的lock操作。 这里的“后续”指的是时间上的先后关系,unlock操作发生在退出同步块之后,lock操作发生在进入同步块之前。这是条最关键性的规则,线程安全性主要依赖于这条规则。 这里关键条件是必须对“同一个锁”的lock和unlock。但是仅仅是这条规则仍然不起任何作用,它必须和传递性原则联合起来使用才显得意义重大。 3)volatile的happen-before原则():对一个volatile变量的写操作happen-before对此变量的任意(读/写)操作(当然也包括写操作了)。 4)happen-before的传递性原则:如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作。
前四条规则是比较重要的,后四条是比较显而易见的。
5)线程启动的happen-before原则(start()规则):同一个线程的start方法happen-before此线程的其它方法。 6)线程中断的happen-before原则(程序中断规则):即对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。(线程t1写入的所有变量,调用Thread.interrupt(),被打断的线程t2,可以看到t1的全部操作) 7)线程终结的happen-before原则:线程中的所有操作都happen-before线程的终止检测。可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。 (线程t1写入的所有变量,在任意其它线程t2调用t1.join(),或者t1.isAlive() 成功返回后,都对t2可见。) 8)对象创建的happen-before原则(对象finalize规则):一个对象的初始化完成先于他的finalize方法调用。(对象调用finalize()方法时,对象初始化完成的任意操作,同步到全部主存同步到全部cache。)
如果两个操作的执行次序无法从happens-before规则推导出来,那么就不能保证他们的有序性,虚拟机就可以随意的对他们进行重排序。
举例(即happens-before关系并不代表了最终的执行顺序)
public double rectangleArea(double length , double width){
double len;
double wid;
len=length;//A
wid=width;//B
double area=leng*wid;//C
return area;
}
上面的操作在运行之前编译器和处理器可能会进行优化,在程序中
A happens-before B
B happens-before C
A happens-before C //happens-before具有传递规则
(1)因为A happens-before B,所以A操作产生的结果leng一定要对B操作可见,但是现在B操作并没有用到length,所以这两个操作可以重排序;
(2)如果A操作和C操作进行了重排序,因为leng没有被赋值,所以leng=0,area=0*wid也就是area=0,A和C不能进行重排序;
但是这种DCL方式其实是有缺陷的,具体什么缺陷呢?
当线程A在获取了Instance.class锁时,对singleton进行 singleton = new Instance() 初始化时,实例化一个对象要分为三个步骤:
step1、 分配内存空间。 step2、 初始化对象。 step3、 将内存空间的地址赋值给对应的引用。
我们使用synchronized ,在synchronized代码块内 ,指令符合锁的happen-before原则(锁管理原则),但step2和step3这两步并不违反Happen-Before规则,因此可以指令重排序,类似的有x=1,y=1,z=x+y可以被编译器优化为y=1,x=1,z=x+y。 正是因为指令重排序,所以上面的过程也可能变为如下的过程: step1、 分配内存空间。 step3、 将内存空间的地址赋值给对应的引用。 step2、 初始化对象 。
如果发生了重排序:,此时线程A的singleton只分配内存地址,但还没有初始化对象,此时singleton是不为空的。
那么就回导致线程B在红框部分判断会出错,直接返回singleton变量,即return的singleton对象是一个没有被初始化的对象。
当然,指令重排序的问题并非每次都会进行,在某些特殊的场景下,编译器和处理器是不会进行重排序的,但上述的举例场景则是大概率会出现指令重排序问题。
java并发编程实战(2):volatile实现原理:volatile的特性是确保变量可见性和禁止指令重排。
对于上面的DCL其实只需要做一点点修改即可:将变量singleton声明为volatile即可:
public class Singleton {
//通过volatile关键字来确保安全
private volatile static Singleton singleton;
private Singleton(){}
public static Singleton getInstance(){
if(singleton == null){
synchronized (Singleton.class){
if(singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
当singleton声明为volatile后:对它的写操作就会有一个内存屏障,这样,在singleton的赋值完成之前,就不用会调用读操作。
注意:volatile阻止的不是singleton = newSingleton()这句话内部[step1、step2、step3]的指令重排,而是保证了在一个写操作([step1、step2、step3]完成之前,不会调用读操作(if (instance == null))。
该解决方案的根本就在于:利用classloder的机制来保证初始化singleton时只有一个线程。JVM在类初始化阶段会获取一个锁,这个锁可以同步多个线程对同一个类的初始化。
public class Singleton {
private static class SingletonHolder{
public static Singleton singleton = new Singleton();
}
public static Singleton getInstance(){
return SingletonHolder.singleton;
}
}
这种解决方案的实质是:运行步骤2和步骤3重排序,但是不允许其他线程看见。缺点就是有可能singleton在整个程序运行生命周期都没有用到。
public class Singleton {
/**
* 构造方法私有化
*/
private Singleton(){
}
private static class SingleHolder{
private static final Singleton singleton = new Singleton();
}
/**
* 内部类方式获取单例
* @return
*/
public static Instance getInstance(){
return SingleHolder.singleton;
}
}
这种从jvm虚拟机上保证了单例,并且也是懒式加载。