1.volatile
两大特征:保证可见性;禁止指令重排序;
1.1volatile是如何保证共享变量中的“可见性”的?
1.1.1什么是可见性?
当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。
谁保证可见性?
Java线程内存模型保证(JMM)。
有volatile变量修饰的共享变量进行写操作的时候会多出第二行汇编代码,Lock前缀的指令;
Lock前缀有什么作用?
1)Lock前缀指令会引起处理器缓存回写到内存。
2)一个处理器的缓存回写到内存会导致其他处理器的缓存无效。
1.1.2处理器缓存回写到内存时如何保证原子性?
使用缓存一致性机制来确保修改的原子性,此操作被称为“缓存锁定”,缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据。
1.1.3如何保证多核处理器缓存的数据在总线的一致性?
处理器使用嗅探技术保证它的内部缓存、系统内存和其他处理器的缓存的数据在总线上保持一致。
1.1.4JSR-133使用什么保证两个操作之间的内存可见性。
JSR-133使用happens-before的概念来阐述操作之间的内存可见性。
在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。
happens-before要求什么?
happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,
且前一个操作按顺序排在第二个操作之前。
1.1.5volatile变量有什么特性?
1)可见性。对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
2)原子性:对任意单个volatile变量的读/写具有原子性,volatile++这种复合操作不具有原子性。
1.1.6volatile写-读的内存语义
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。
当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
对volatile写和volatile读的内存语义做个总结。
1)线程A写一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程发出了(其对共享变量所做修改的)消息。 2)线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile变量之前对共享变量所做修改的)消息。 3)线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息。
1.1.7线程A和线程B是如何进行消息的传递的?
Java线程之间的通信由Java内存模型(本文简称为JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。
从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。
1.1.8JMM控制哪些共享变量?哪些不控制?
所有实例域、静态域和数组元素都存储在堆内存中(统称共享变量),堆内存在线程之间共享;
局部变量(Local Variables),方法定义参数和异常处理器参数(Exception Handler Parameters)不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响。
1.2volatile是如何禁止指令重排序的?
1.2.1为什么要进行指令重排序?
重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。
对于处理器重排序,JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障(Memory Barriers,Intel称之为Memory Fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序。
1.2.2as-if-serial语义
as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。
1.2.3volatile内存语义的实现
volatile重排序规则表
编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
JMM采取保守策略。下面是基于保守策略的JMM内存屏障插入策略。
-
在每个volatile写操作的前面插入一个StoreStore屏障。
-
在每个volatile写操作的后面插入一个StoreLoad屏障。
-
在每个volatile读操作的后面插入一个LoadLoad屏障。
-
在每个volatile读操作的后面插入一个LoadStore屏障。
1.3CAS为什么同时具有volatile读和volatile写的内存语义?
AQS使用一个整型的volatile变量(命名为state)来维护同步状态;
加锁方法首先读volatile变量state。
释放锁的最后写volatile变量state。
根据volatile的happens-before规则,释放锁的线程在写volatile变量之前可见的共享变量,在获取锁的线程读取同一个volatile变量后将立即变得对获取锁的线程可见。
sun.misc.Unsafe类的compareAndSwapInt()方法的源代码调用的c++代码为:unsafe.cpp;处理器会根据情况加lock前缀;
1.4无volatile双重检查锁有什么缺陷?
双重检查锁
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=new Singleton();)创建了一个对象。这一行代码可以分解为如下的3行伪代码。
memory = allocate(); // 1:分配对象的内存空间 ctorInstance(memory); // 2:初始化对象 instance = memory; // 3:设置instance指向刚分配的内存地址
2和3之间重排序之后的执行时序如下。
memory = allocate(); // 1:分配对象的内存空间 instance = memory; // 3:设置instance指向刚分配的内存地址 // 注意,此时对象还没有被初始化! ctorInstance(memory); // 2:初始化对象
如果发生重排序,另一个并发执行的线程B就有可能在第4行判断instance不为null。线程B接下来将访问instance所引用的对象,但此时这个对象可能还没有被A线程初始化!
1.4.1基于volatile解决双重检查锁缺陷
把instance声明为volatile型;就可以实现线程安全的延迟初始化。
当声明对象的引用为volatile后,代码中的2和3之间的重排序,在多线程环境中将会被禁止。
1.4.2基于类初始化的解决方案
JVM在类的初始化阶段(即在Class被加载后,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。
Java语言规范规定,对于每一个类或接口C,都有一个唯一的初始化锁LC与之对应。