1.happens-before简介
从JDK 5开始,Java使用新的JSR-133内存模型(除非特别说明,本文针对的都是JSR-133内存模型)。JSR-133使用happens-before的概念来阐述操作之间的内存可见性。在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。与程序员密切相关的happens-before规则如下。
·程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
·监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
·volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
·传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
注意 两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行,重排序(包括编译器重排序和处理器重排序)可能会打乱执行顺序!happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前。
1.1.数据依赖性:编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。只要重排序两个操作的执行顺序,程序的执行结果就会被改变。
1.2 as-if-serial语义:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。
1.3顺序一致性内存模型:是一个理论参考模型,在设计的时候,处理器的内存模型和编程语言的内存模型都会以顺序一致性内存模型作为参照。
1)一个线程中的所有操作必须按照程序的顺序来执行。 2)(不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序。
1.4JMM对正确同步的多线程程序的内存一致性做了如下保证。如果程序是正确同步(synchronized、volatile和final的正确使用 )的,程序的执行将具有顺序一致性——即程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同。
1.5final域重排序规则:1)在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。2)初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。
这两点能够保证在读一个对象的final域之前,一定会先读包含这个final域的对象的引用,jmm最小安全模型只能保证被引用变量被初始化成null 0 false。
1.6双重检查锁定依然不安全
memory = allocate(); // 1:分配对象的内存空间
ctorInstance(memory); // 2:初始化对象
instance = memory; // 3:设置instance指向刚分配的内存地址
以上步骤2和3可能被重排序,改进方法是在
instance变量前加volatile关键字,禁止步骤2和3重排序
也可以基于类初始化的解决方案。JVM在类的初始化阶段(即在Class被加载后,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁(类对象)。这个锁可以同步多个线程对同一个类的初始化。
如果确实需要对实例字段使用线程安全的延迟初始化,请使用上面介绍的基于volatile的延迟初始化的方案;如果确实需要对静态字段使用线程安全的延迟初始化,请使用上面介绍的基于类初始化的方案。
1.6JMM的内存可见性保证
按程序类型,Java程序的内存可见性保证可以分为下列3类。
单线程程序。单线程程序不会出现内存可见性问题。编译器、runtime和处理器会共同确保单线程程序的执行结果与该程序在顺序一致性模型中的执行结果相同。
正确同步的多线程程序。正确同步的多线程程序的执行将具有顺序一致性(程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同)。这是JMM关注的重点,JMM通过限制编译器和处理器的重排序来为程序员提供内存可见性保证。
·未同步/未正确同步的多线程程序。JMM为它们提供了最小安全性保障:线程执行时读取到的值,要么是之前某个线程写入的值,要么是默认值(0、null、false)。