1.对于final域,编译器和处理器要遵照两个重排序规则:java
(1)在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操做之间不能重排序。程序员
(2)初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操做之间不能重排序。编程
如下列代码为例进行解释,假设线程A执行执行writer方法,线程B执行reader方法。数组
public class FinalExample { int i; // 普通变量 final int j; // final变量 static FinalExample obj; public FinalExample() { // 构造函数 i = 1; // 写普通域 j = 2; // 写final域 } public static void writer() { // 写线程A执行 obj = new FinalExample(); } public static void reader() { // 读线程B执行 FinalExample object = obj; // 读对象引用 int a = object.i; // 读普通域 int b = object.j; // 读final域 } }
1. 写final域的重排序规则禁止把final域的写重排序到构造函数以外。这个规则的实现包含下面2个方面:安全
(1)JMM禁止编译器把final域的写重排序到构造函数以外。多线程
(2)编译器会在final域的写以后,构造函数return以前,插入一个StoreStore屏障。这个屏障禁止处理器把final域的写重排序到构造函数以外。并发
写final域的重排序规则能够确保:在对象引用为任意线程可见以前,对象的final域已经被正确初始化过了,而普通域不具备这个保障。因此在上面的代码中就可能会发生写普通域的操做被编译器重排序到了构造函数以外,读线程B错误地读取了普通变量i初始化以前的值。而写final域的操做,被写final域的重排序规则“限定”在了构造函数以内,读线程B正确地读取了final变量初始化以后的值。app
1. 读final域的重排序规则是:在一个线程中,初次读对象引用与初次读该对象包含的final域,JMM禁止处理器重排序这两个操做(注意,这个规则仅仅针对处理器)。编译器会在读final域操做的前面插入一个LoadLoad屏障。函数
初次读对象引用与初次读该对象包含的final域,这两个操做之间存在间接依赖关系。因为编译器遵照间接依赖关系,所以编译器不会重排序这两个操做。大多数处理器也会遵照间接依赖,也不会重排序这两个操做。但有少数处理器容许对存在间接依赖关系的操做作重排序 (好比alpha处理器),这个规则就是专门用来针对这种处理器的。性能
读final域的重排序规则能够确保:在读一个对象的final域以前,必定会先读包含这个final域的对象的引用,可是读普通域就没有这个保证。因此,在这个示例程序中,若是该引用不为null,那么引用对象的final域必定已经被A线程初始化过了,而若是读普通域的指令被排在了读obj对象引用以前,就会致使空指针异常,由于此时对于局部变量引用object尚未赋予对象。
如下列代码为例
public class FinalReferenceExample { final int[] intArray; // final是引用类型 static FinalReferenceExample obj; public FinalReferenceExample() { // 构造函数 intArray = new int[1]; // 1 intArray[0] = 1; // 2 } public static void writerOne() { // 写线程A执行 obj = new FinalReferenceExample(); // 3 } public static void writerTwo() { // 写线程B执行 obj.intArray[0] = 2; // 4 } public static void reader() { // 读线程C执行 if (obj != null) { // 5 int temp1 = obj.intArray[0]; // 6 } } }
1.在上例代码中,final域为一个引用类型,它引用一个int型的数组对象。对于引用类型:
(1)写final域的重排序规则对编译器和处理器增长了以下约束:在构造函数内对一个final引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操做之间不能重排序。也就是说,1是对final域的写入,2是对这个final域引用的对象的成员域的写入,3是把被 构造的对象的引用赋值给某个引用变量。这里除了前面提到的1不能和3重排序外,2和3也不能重排序。
(2)假设线程A先执行,执行完成后再执行线程B和C,那么,JMM能够确保读线程C至少能看到写线程A在构造函数中对final引用对象的成员域的写入。即C至少能看到数组下标0的值为1。而写线程B对数组元素的写入,读线程C可能看获得, 也可能看不到。JMM不保证线程B的写入对读线程C可见,由于写线程B和读线程C之间存在数据竞争,此时的执行结果不可预知。 若是想要确保读线程C看到写线程B对数组元素的写入,写线程B和读线程C之间须要使用同步原语(lock或volatile)来确保内存可见性。
写final域的重排序规则能够确保:在引用变量为任意线程可见以前,该引用变量指向的对象的final域已经在构造函数中被正确初始化过了。其实,要获得这个效果, 还须要一个保证:在构造函数内部,不能让这个被构造对象的引用为其余线程所见,也就是对象引用不能在构造函数中“逸出”。若是没有这个保证会发生什么?以下面代码所示
public class FinalReferenceEscapeExample { final int i; static FinalReferenceEscapeExample obj; public FinalReferenceEscapeExample() { i = 1; // 1写final域 obj = this; // 2 this引用在此"逸出" } public static void writer() { new FinalReferenceEscapeExample(); } public static void reader() { if (obj != null) { // 3 int temp = obj.i; // 4 } } }
假设一个线程A执行writer()方法,另外一个线程B执行reader()方法。这里的操做2使得对象还未完成构造前就为线程B可见。即便这里的操做2是构造函数的最后一步,且在程序中操做2排在操做1后面,执行read()方法的线程仍然可能没法看到final域被初始化后的值,由于这里的操做1和操做2之间可能被重排序。
因此,在构造函数返回前,被构造对象的引用不能为其余线程所见,由于此时的final域可能尚未被初始化。在构造函数返回后,任意线程都将保证能看到final域正确初始化以后的值。
写final域的重排序规则会要求编译器在final域的写以后,构造函数return以前插入一个StoreStore屏障。读final域的重排序规则要求编译器在读final域的操做前面插入 一个LoadLoad屏障。
可是某些处理器,以X86处理器为例,它不支持写-写重排序,因此写final域操做后插入的StoreStore屏障会被省略。其次X86处理器不会对存在间接关系依赖的数据操做进行重排序,因此读final域前的LoadLoad屏障也会被省略。也就是说X86处理器中,final的读写不须要插入任何内存屏障。
1.在设计JMM时,须要考虑两个关键因素:
(1)程序员对内存模型的使用。程序员但愿内存模型易于理解、易于编程。程序员但愿基于一个强内存模型来编写代码。
(2)编译器和处理器对内存模型的实现。编译器和处理器但愿内存模型对它们的束缚越少越好,这样它们就能够作尽量多的优化来提升性能。编译器和处理器但愿实现一个弱内存模型。
因此,设计JMM时,必定要在以上两个因素之间找到一个平衡点,一方面,要为程序员提供足够强的内存可见性保证;另外一方面,对编译器和处理 器的限制要尽量地放松。
2. 如下列代码为例:
double pi = 3.14; // A double r = 1.0; // B double area = pi * r * r; // C
上面计算圆的面积的示例代码存在3个happens-before关系:
(1)A happens-before B。
(2)B happens-before C。
(3)A happens-before C。
在3个happens-before关系中,2和3是必需的,但1是没必要要的。所以,JMM把happens-before 要求禁止的重排序分为了下面两类:
(1)会改变程序执行结果的重排序。
(2)不会改变程序执行结果的重排序。
JMM对这两种不一样性质的重排序,采起了不一样的策略:
(1)对于会改变程序执行结果的重排序,JMM要求编译器和处理器必须禁止这种重排序。
(2)对于不会改变程序执行结果的重排序,JMM对编译器和处理器不作要求(JMM容许这种重排序)。
也就是说
(1)JMM向程序员提供的happens-before规则能知足程序员的需求。JMM的happens-before规则不但简单易懂,并且也向程序员提供了足够强的内存可见性保证(有些内存可见性保证其实并不必定真实存在,好比上面的A happens-before B)。
(2)JMM对编译器和处理器的束缚已经尽量少。从上面的分析能够看出,JMM实际上是在遵循一个基本原则:只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序), 编译器和处理器怎么优化都行。例如,若是编译器通过细致的分析后,认定一个锁只会被单个线程访问,那么这个锁能够被消除。再如,若是编译器通过细致的分析后,认定一个volatile变 量只会被单个线程访问,那么编译器能够把这个volatile变量看成一个普通变量来对待。
1.定义以下:
(1)若是一个操做happens-before另外一个操做,那么第一个操做的执行结果将对第二个操做可见,并且第一个操做的执行顺序排在第二个操做以前。
(2)两个操做之间存在happens-before关系,并不意味着Java平台的具体实现必需要按照 happens-before关系指定的顺序来执行。若是重排序以后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM容许这种重排序)。
对于程序员来讲,定义(1)的关系表示:若是A happens-before B,那么Java内存模型将向程序员保证——A操做的结果将对B可见, 且A的执行顺序排在B以前。
定义(2)是JMM对编译器和处理器重排序的约束原则。正如前面所言,JMM实际上是在遵 循一个基本原则:只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序), 编译器和处理器怎么优化都行。JMM这么作的缘由是:程序员对于这两个操做是否真的被重 排序并不关心,程序员关心的是程序执行时的语义不能被改变(即执行结果不能被改变)。因 此,happens-before关系本质上和as-if-serial语义是一回事。
2. as-if-serial与happens-before对比:
(1)as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变。
(2)as-if-serial语义给编写单线程程序的程序员一个假象,即单线程程序是按程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员一个假象,即正确同步的多线程程序是按happens-before指定的顺序来执行的。
1. happens-before是JMM最核心的概念。其四个规则分别是
(1)程序顺序规则:一个线程中的每一个操做,happens-before于该线程中的任意后续操做。
(2)监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
(3)volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
(4)传递性:若是A happens-before B,且B happens-before C,那么A happens-before C。
(5)start()规则:若是线程A执行操做ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操做happens-before于线程B中的任意操做。
(6)join()规则:若是线程A执行操做ThreadB.join()并成功返回,那么线程B中的任意操做happens-before于线程A从ThreadB.join()操做成功返回。
2. 以volatile写-读创建的happens-before关系为例:
首先,在volatile写操做前会插入一个StoreStore屏障,保证在volatile变量写操做以前的其余写操做必定会先被执行,也就是:操做1happens-before 操做2。
而在volatile变量写操做以后,会插入一个StoreLoad屏障,其保证了在volatile变量写操做以后的全部读写操做必定会在volatile变量写操做执行完毕以后在执行,也就是:操做2 happens-before 操做3
在每一个volatile读操做的后面插入一个LoadLoad屏障,保证了读操做必定会在其后的读操做以前执行,也就是 操做3 happens-before 操做4。
根据happens-before规则的传递性,能够得出,操做1 happens-before 操做4。也就是操做1必定对操做4可见。
3.以线程方法 Thread.start() 创建的happens-before关系为例:假设线程A在执行的过程当中,经过执行ThreadB.start()来启动线程B。同时,假设线程A在执行ThreadB.start()以前修改了一些共享变量,线程B在开始执行后会读这些共享变量。
1 happens-before 2由程序顺序规则产生。2 happens-before 4由start()规则产生。根据传递性,将有1 happens-before 4。这实意味着,线程A在执行ThreadB.start()以前对共享变量所作的修改,接下来在线程B开始执行后都将确保对线程B可见。
在Java多线程程序中,有时候须要采用延迟初始化来下降初始化类和建立对象的开销。双重检查锁定是常见的延迟初始化技术,但它是一个错误的用法。分析双重检查锁定的错误根源,以及两种线程安全的延迟初始化方案。
1. 在Java程序中,有时候可能须要推迟一些高开销的对象初始化操做,而且只有在使用这些对象时才进行初始化。此时,程序员可能会采用延迟初始化。但要正确实现线程安全的延迟初始化须要一些技巧,不然很容易出现问题。好比,下面是非线程安全的延迟初始化对象的示例代码。
public class UnsafeLazyInitialization { private static Instance instance; public static Instance getInstance() { if (instance == null) // 1:A线程执行 instance = new Instance(); // 2:B线程执行 return instance; } }
在UnsafeLazyInitialization类中,假设A线程执行代码1的同时,B线程执行代码2。此时,线程A可能会看到instance引用的对象尚未完成初始化。咱们能够对getInstance()方法作同步处理来实现线程安全 的延迟初始化。示例代码以下。
public class SafeLazyInitialization { private static Instance instance; public synchronized static Instance getInstance() { if (instance == null) instance = new Instance(); return instance; } }
因为对getInstance()方法作了同步处理,synchronized将致使性能开销。若是getInstance()方法被多个线程频繁的调用,将会致使程序执行性能的降低。反之,若是getInstance()方法不会被多个线程频繁的调用,那么这个延迟初始化方案将能提供使人满意的性能。
为了不锁带来的巨大开销,双重检查锁定便出现了(Double-Checked Locking),经过双重检查锁定来下降同步的开销。下面是使用双重检查锁定来实现延迟初始化的示例代码。
public class DoubleCheckedLocking { // 1 private static Instance instance; // 2 public static Instance getInstance() { // 3 if (instance == null) { // 4:第一次检查 synchronized (DoubleCheckedLocking.class) { // 5:加锁 if (instance == null) // 6:第二次检查 instance = new Instance(); // 7:问题的根源出在这里 } // 8 } // 9 return instance; // 10 } // 11 }
若是第一次检查instance不为null,那么就不须要执行下面的加锁和初始化操做。所以,能够大幅下降synchronized带来的性能开销。可是,问题会出如今第7行代码处。若一个在另外一个线程执行到第4行,代码读取到instance不为null时,instance引用的对象有可能尚未完成初始化。
对于instance = new Instance()的这行代码的过程为:
(1)分配内存空间
(2)初始化对象
(3)将instance的引用指向开辟的内存地址
可是,对于2,3步,编译器可能会进行重排序,也就是说会先将instance的引用指向刚开辟的内存地址,这个行为就意味着instance = new Instance()这行代码执行完毕,接下来会释放锁(这也就会致使其余线程此时就有可能开始执行,判断instance引用不为空,此时,其余线程就会看到一个未被初始化的对象),而后再初始化对象。
对于2,3步骤的重排序,只要在初次访问对象以前执行完两个步骤,单线程内的执行结果并不会改变,可是多线程时,就有可能形成其余线程看到的是一个未被初始化的对象。
所以,有两个办法来实现线程安全的延迟初始化:
(1)不容许2和3重排序。
(2)容许2和3重排序,但不容许其余线程“看到”这个重排序。
如下的两个解决方法依据这两个原理。
1. 对于前面的基于双重检查锁定来实现延迟初始化的方案(指DoubleCheckedLocking示例代 码),只须要作一点小的修改(把instance声明为volatile型),就能够实现线程安全的延迟初始化。请看下面的示例代码:
public class SafeDoubleCheckedLocking { private volatile static Instance instance; public static Instance getInstance() { if (instance == null) { synchronized (SafeDoubleCheckedLocking.class) { if (instance == null) instance = new Instance(); // instance为volatile,如今没问题了 } } return instance; } }
这个解决方案须要JDK 5或更高版本(由于从JDK 5开始使用新的JSR-133内存模型规范,这个规范加强了volatile的语义)。
当声明对象的引用为volatile后,instance = new Instance()这行代码的三个步骤中的2和3之间的重排序,在多线程环境中将会被禁止。
1. JVM在类的初始化阶段(即在Class被加载后,且被线程使用以前),会执行类的初始化。在 执行类的初始化期间,JVM会去获取一个锁。这个锁能够同步多个线程对同一个类的初始化。
public class InstanceFactory { private static class InstanceHolder { public static Instance instance = new Instance(); } public static Instance getInstance() { return InstanceHolder.instance ; // 这里将致使InstanceHolder类被初始化 } }
假设两个线程并发执行getInstance()方法,下面是执行的示意图
这个方案的实质是:容许2和3重排序,但不容许非构造线程(这里指线程B)“看到”这个重排序。
2. 初始化一个类,包括执行这个类的静态初始化和初始化在这个类中声明的静态字段。根据Java语言规范,在首次发生下列任意一种状况时,一个类或接口类型T将被当即初始化。
(1)T是一个类,并且一个T类型的实例被建立。
(2)T是一个类,且T中声明的一个静态方法被调用。
(3)T中声明的一个静态字段被赋值。
(4)T中声明的一个静态字段被使用,并且这个字段不是一个常量字段。
(5)T是一个顶级类,并且一个断言语句嵌套在T内部被执行。 在InstanceFactory示例代码中,首次执行getInstance()方法的线程将致使InstanceHolder类被初始化(符合状况4)。
3. 对于类或接口的初始化,Java语言规范制定了精巧而复杂的类初始化处理过程。Java初始化一个类或接口的处理过程以下:
(1)经过在Class对象上同步(即获取Class对象的初始化锁),来控制类或接口的初始化。这个获取锁的线程会一直等待,直到当前线程可以获取到这个初始化锁。假设Class对象当前尚未被初始化(初始化状态state,此时被标记为state=noInitialization),且有两个线程A和B试图同时初始化这个Class对象。如图
(2)线程A执行类的初始化,同时线程B在初始化锁对应的condition上等待。线程A执行初始化后,线程B检查到state已经初始化后,则释放初始化锁。线程A执行初始化的过程当中就包括对静态字段的初始化,以及对静态内部类中静态字段的初始化,在对静态字段的对象初始化步骤中的2,3步步骤能够被重排序,可是没法被其余线程看到。
(3)线程A设置state=initialized,而后唤醒在condition中等待的全部线程。
(4)线程B结束类的初始化处理。
(5)线程C执行类的初始化的处理。
在第3阶段以后,类已经完成了初始化。所以线程C在第5阶段的类初始化处理过程相对简单一些(前面的线程A和B的类初始化处理过程都经历了两次锁获取-锁释放,而线程C的类初 始化处理只须要经历一次锁获取-锁释放)。
若是确实须要对实例字段使用线程安全的延迟初始化,请使用上面介绍的基于volatile的延迟初始化的方案;若是确实须要对静态字段使用线程安全的延迟初始化,请使用上面介绍的基于类初始化的方案。