也说一下DCL问题
29 Nov 2014
Double-checked Locking是一个经典的Java并发问题。根据网上看到的资料加上同别人的讨论,这里把我的理解尝试用一种更易懂的方法整理一下。
这个问题来自怎么线程安全的发布单例,先看下面这段code,它在需要的时候才会初始化一个单例资源:
在多线程环境下面显然是有问题的,多个线程可能会重复初始化Resource。解决方法很简单,加同步锁:
哈,问题解决了。但每次获取资源时都要synchronized一把,对性能上很有影响。于是有人发明了双重检查锁定,先检查下Resource是否为空,如果不是就不用进行synchronized了;在synchronized块里还要再检查一遍是否为空,因为在Resource初始化之前可能有两个线程同时检测到Resource为空并尝试进入同步块。
这样就没问题了吗?重点来了,由于new Resource()不是一个原子操作,实际执行的时候由于指令优化可能是这样
-
初始化Resource对象;
-
把Resource对象地址赋给resource变量;
也可能是这样
-
初始化一半Resource对象;
-
把Resource对象地址赋给resource变量;
-
初始化剩下的Resource对象;
如果是第一种情况还好;如果是第二种情况,我们假设线程A刚走到第2步的时候就被操作系统挂起了,线程B执行了一下getResource方法,由于resource变量已经被赋了一个地址所以不为null,然后线程B就直接把这个地址傻乎乎的返回出去了。这样在线程B后面的代码里实际上使用的就是一个残缺的Resource对象。
问题清楚了,解决的思路其实很简单,确保resource的赋值操作在对象初始化后执行。这里可以用volatile和final两个关键字来解决。
volatile
由于有了volatile的存在,resource的赋值指令不会被优化到new Resource()中间去,这就保证了另外一个线程如果看到了resource被赋值的时候,其指向的对象一定是被初始化完成的。
这里有一个性能上可以做的优化是,先把resource赋给本地变量,然后操作和返回这个本地变量。这样就不用访问多次resource变量了,据说可以提高25%的性能。
final
对一个final的字段的初始化,JVM保证了从别的线程看起来,拿到对象引用后final字段一定被初始化好了。也就是resourceHolder的赋值一定是在new Resource()后面。请注意这里的本地变量不是多余的。如果没有的话,第二个resourceHolder == null可能会被代码优化干掉。
static
对于DCL,还有一种利用static的更优雅的写法
这段代码在初次执行到return ResourceHolder.resource这一行的时候,JVM才会对ResourceHolder类进行类加载和初始化。而这个过程由JVM保证是线程安全的。