一,java并发编程中对于竞态条件的解释
由于对于执行片段没有正确的同步,程序片段不具有应有的原子性。在不恰当的并发执行时序中出现的不正确的结果,进一步可以解释为:程序的计算正确定取决于多个线程交替执行的时序。
常见的竞态条件
1.先检查后执行
2.复合操作
延迟初始化是先检查后执行说产生的竞态条件对典型例子
延迟初始化需要解决的两个问题:
1.对象不一致
2.对象不完全初始化
二,由延迟初始化演变的双重检测锁
如下是一个延迟初始化的例子
package ceshi;
public class LazyInit {
private Object obj=null;
public Object getInstance() {
if(obj==null) ---->1
obj = new Object(); ---->2
return obj;
}
}
该类在单线程程序中是可以正确执行的,并且可以确保起到延迟初始化的效果,然而在并发程序中,该类存在两个问题:
1.某些线程可能会获得一个不完全构造的obj对象。当第一个线程运行到第二步,当Objcet对象还没有构造完成,obj对象引用已经对于其他线程可见时,若其他线程此时运行到第一步,就会获取到一个还没有完全构造的obj对象(参考final关键字内存实现)。
对象构造过程的解释[1]
对于上述对象构造的一行代码obj = new Object();可以分解成3个步骤运行:
memory = allocate(); //1,分配对象的内存空间
ctorInstance(memory); //2,初始化对象
Instance= memory; //3,设置instance指向刚分配的内存
根据java语言规范,在保证不改变单线程程序执行结果的前提下,可以允许编译器/处理器进行重排序。故上述执行顺序可能是1–>3–>2。这样,对象在初始化完成之前就可见。
2.造成多个线程所获取的obj对象不一致的情况。当多个线程同时运行到第一步时,obj==null,那么各个线程会获得一个构造对象,造成多个线程获得obj对象不一致的问题。
三,解决办法
1,利用同步到方式解决
造成上述问题的原因是在并发程序交替执行的时候,多个线程同时执行getInstance函数导致。那么很显然可以利用java锁机制(sychronized,显示锁lock)可以解决,将该方法同步。
package ceshi;
public class LazyInit {
private Object obj=null;
public synchronized Object getInstance() {
if(obj==null)
obj = new Object();
return obj;
}
}
在同一时刻,只能有一个线程获取锁对象,并执行getInstace方法。这样在同步的情况下可以解决延迟初始化不安全的问题。但是,在getInstace()方法在被多个线程频繁调用的情况下会造成程序性能下降。若被调用的次数不多,这中解决方法可以得到令人满意的效果。
2,双重检测锁
若在getInstace()方法被多个线程频繁调用的时候,上述解决方法就会明显降低性能。当使用如下双重检测方法时:会存在上述两个问题中的第一种问题。
package ceshi;
public class LazyInit {
private Object obj=null;
public Object getInstance() {
if (obj == null) { //1
synchronized (LazyInit.class) {
if (obj == null)
obj = new Object(); //2
}
}
return obj;
}
}
虽然在构造object对象的时候使用了同步,但是由于在第2步存在编译器/处理器重排序,可能会导致未完全初始化的对象可见。那么,当线程A执行第2步,对象还未完全初始化之前对象引用就可见,那么线程B在执行第一步时,就会获得一个未完全初始化的对象引用。
解决方法:
在jdk1.5之后,增强了volatile关键字,加入了禁止编译器/处理器对volatile变量重排序的功能。并保证对volatile类型变量的读发生在写之后。故可以使用volatile关键字来解决该问题。将上述obj引用改为volatile修饰:
package ceshi;
public class LazyInit {
private volatile Object obj=null;
public Object getInstance() {
if (obj == null) {
synchronized (LazyInit.class) {
if (obj == null)
obj = new Object(); //2
}
}
return obj;
}
}
这样,会禁止编译器/处理器在第2步的重排序。
四:DCL的替代方法
在目前的jdk实现中,DCL方法并不是一种高效的提高初始化性能的优化措施,可以使用延迟初始化占位模式[2]来代替。如下:
/** - @author lecky - */
class Resource{
private static class ResourceHolder{
public static Object resouce = new Object();
}
public static Object getResource(){
return ResourceHolder.resouce;
}
}
该类使用static来保证达到延迟初始化以及并发安全。
– 延迟初始化保证:只包含static修饰的块或者域的类只有在第一次使用该类或者域的时候才会被加载,即上述类只有在第一给调用getResource()方法的时候,才会被加载。
– 并发安全保证:static块或者域的执行时机为类被加载之后且在类被其他线程调用之前。
[1]. Java并发编程的艺术 P69
[2]. Java并发编程实战