错误的双重检查锁

错误的双重检查锁

    public static Test4 test4 = null;
    public static void main(String[] args) {
        if (test4 != null) {
            synchronized (Thread.class) {
                if (test4 != null) {
                    test4 = new Test4();
                }
            }
        }

按照期望的,就是通过外层判断减少持锁判断的次数,以增加性能.

预期中最差的执行情况是,由于test4没有被volatile修饰,处理器缓存依旧可用,导致对于运行在其它处理器的线程来说,

如果在test4被赋值之前就读取了变量作为缓存,那么互相的test4引用变量的修改情况不可见且为空,所有线程都需要进入synchronized持锁再判断.

使用synchronized持有monitor lock,保证内层判断每次只有一个线程可以访问.

这样,当一个线程赋值之后,这些持有缓存的线程再进入就会通过内层判断知道已经存在test4然后直接退出.

但是实际情况会更糟糕,因为存在指令重排序的问题,test4 = new Test4();编译出的字节码可能存在乱序执行的情况,导致test4变量出现引用不为null但是构造方法还未执行的情况.

    public static Test4 test4 = null;
    public static void main(String[] args) {
        synchronized (Test4.class) {
            test4 = new Test4();
        }
    }
    //javap -c Test4.class
       4: monitorenter
       5: new           #2                  // class com/example/threadCoreKnowledge/ThreadStateShow/Test4
       8: dup
       9: invokespecial #3                  // Method "<init>":()V
      12: putstatic     #4                  // Field test4:Lcom/example/threadCoreKnowledge/ThreadStateShow/Test4;
      15: aload_1
      16: monitorexit

可以看到test4 = new Test4();这行代码编译为字节码指令包含new,dup,invokespecial,putstatic,

重排序后,由于test4不是volatile变量语义上没有禁止重排序,且invokespecial和putstatic之间不存在happen-before关系,也就是单线程中两个操作谁先执行都不影响后面代码的运行结果,所以执行顺序可能被打乱.

也就存在// Method "<init>":()V的构造函数还没执行完成,
就已经将引用赋值(putstatic)给了静态变量test4,
这使得其它线程有可能访问到一个错误的对象(构造函数未执行/未执行完).
往往导致程序的执行不符合预期且可能产生错误.

为什么会发生重排序:
经过如下因素处理,可以提升程序的执行速度.
①在编译器中生成的指令顺序,可以与源代码中的顺序不同,此外编译器还会把变量保存在寄存器而不是内存中;

②处理器可以采用乱序或并行等方式来执行指令;

③缓存可能会改变将写入变量提交到主内存的次序;

④保存在处理器本地缓存中的值,对于其它处理器来说是不可见的;

如果没有使用正确的同步,这些因素都会导致一个线程无法看到变量的最新值,并且导致其它线程中的内存操作似乎在乱序执行.

为什么已经通过synchronized确保操作有序性,操作还是会出错?

因为synchronized的有序性是通过monitor锁的机制
确保代码块在同一时刻只有一个线程访问

而JVM根据Java语言规范对于"一个代码块在同一时刻只有一个线程访问"的情况,会维护一种类似串行的语义:即代码块程序执行结果与在严格串行环境中执行结果相同的情况下,上面指令重排序的因素[①②③④]就都是允许的.

正确的双重检查锁

这种模式避免了在域被初始化之后访问这个域时的锁定开销

    public static volatile Test4 test4 = null;
    //添加局部变量,确保变量在被初始化后,每次方法执行时只被读取一次(Test4 temp=test4)
	//因为字段添加了volatile关键字,确保每次读写都去内存中进行,这
	//抛弃了CPU自带的高速多级缓存,使字段可以被在其它cpu中执行的线程
	//察觉到变化.通过减少读取的次数,更多的利用到了高速多级缓存,使程序
	//获得硬件上的速度提升
    public static void main(String[] args) {
    	Test4 temp=test4;
        if (temp != null) {
            synchronized (Thread.class) {
                if (test4 != null) {
                    test4 = temp = new Test4();
                }
            }
        }

双重检查锁的起因和不再推荐使用的原因

由于早期的JVM在性能上存在一些有待优化的地方,因此延迟初始化经常被用来避免不必要的高开销操作,或者降低程序的启动时间.在编写正确的延迟初始化方法中需要使用同步.但在JDK5以及之前,同步操作是很慢的.

DCL(double check lock)则能两全其美,在常见代码路径上的延迟初始化中不存在同步开销.

然而,DCL这种使用方法已经被广泛地废弃了
促使该模式出现的驱动力(同步竞争执行速度很慢,以及JVM启动速度很慢)已经不复存在,因而它不时一种高效的优化措施.

延迟初始化站位模式能带来同样的优势,且更容易理解

/**
 * ResourceFactory
 * <p/>
 * Lazy initialization holder class idiom
 *
 * @author Brian Goetz and Tim Peierls
 */
@ThreadSafe
public class ResourceFactory {
    private static class ResourceHolder {
        public static Resource resource = new Resource();
    }

    public static Resource getResource() {
        return ResourceFactory.ResourceHolder.resource;
    }

    static class Resource {
    }
}
//使用了一个专门的类来初始化Resource.JVM将推迟ResourceHolder的初始化操作,直到开始使用这个类才初始化,并且因为是通过一个静态初始化来初始化Resource的,JVM保证全局只在类初始化时执行一次所以不需要额外的同步操作.
//当任何一个线程第一次调用getResource时,都会使ResourceHolder被加载并被初始化,此时静态初始化器将执行Resource的初始化操作.

参考文献

Java并发编程实战:16.1什么是内存模型,为什么需要它

Java并发编程实战:16.2.4双重检查加锁

Effective Java 3:Item83,慎用延迟初始化

深入理解Java虚拟机第三版:12.3.5原子性、可见性与有序性-3.有序性

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值