关键字volatile是Java虚拟机提供的最轻量级的同步机制。
当一个变量定义为volatile时,它将具备两种特性: (1)可见性;(2)禁止指令重排序。
在volatile变量与happens – before 之间是什么关系呢,我们通过一个示例说明:
class VolatileExample { int a = 0; volatile boolean flag = false; public void writer() { a = 1; //1 flag = true; //2 } public void reader() { if (flag) { //3 int i = a; //4 } } }
说明:假定线程A先执行writer方法,线程B后执行reader方法,那么根据happens – before关系,我们可以知道:
具体的happens – before图形化如下:
前面讲到,volatile变量会禁止编译器、处理器重排序。下面是volatile具体的排序规则表:
为了实现 volatile 的内存语义,编译器在生成字节码时,会在指令序列中插入 内存屏障 来禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能,为此,JMM 采取保守策略。下面是基于保守策略的 JMM 内存屏障插入策略:
下面通过一个示例展示volatile的内存语义:
class VolatileBarrierExample { int a; volatile int v1 = 1; volatile int v2 = 2; void readAndWrite() { int i = v1; // 第一个 volatile 读 int j = v2; // 第二个 volatile 读 a = i + j; // 普通写 v1 = i + 1; // 第一个 volatile 写 v2 = j * 2; // 第二个 volatile 写 } }
根据程序和插入屏障的规则,最后的指令序列如下图所示:
说明:编译器、处理器会根据上下文进行优化,并不是完全按照保守策略进行插入相应的屏障指令。
锁是Java并发编程中最重要的同步机制。
锁除了让 临界区 互斥执行外,还可以让释放锁的线程向获取同一个锁的线程发送消息。
下面一个示例展示了锁的使用:
class MonitorExample { int a = 0; public synchronized void writer() { // 1 a++; // 2 } // 3 public synchronized void reader() { // 4 int i = a; // 5 } // 6 }
说明:假设线程 A 执行 writer()方法,随后线程 B 执行 reader()方法。该程序的happens – before关系如下:
图形化表示如下:
1. 当线程释放锁时,JMM会把该线程对应的工作内存中的共享变量刷新到主内存中,以确保之后的线程可以获取到最新的值。
2. 当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须要从主内存中去读取共享变量。
###2.4 锁内存语义的实现
锁的内存语义的具体实现借助了volatile变量的内存语义的实现。
对于 final 域,编译器和处理器要遵守两个重排序规则:
如下面示例展示了final两种重排序规则:
public final class FinalExample { final int i; public FinalExample() { i = 3; // 1 } public static void main(String[] args) { FinalExample fe = new FinalExample(); // 2 int ele = fe.i; // 3 } }
说明: 操作1与操作2符合重排序规则1,不能重排,操作2与操作3符合重排序规则2,不能重排。
由下面的示例我们来具体理解final域的重排序规则。
public class FinalExample { int i; // 普通变量 final int j; // final变量 static FinalExample obj; // 静态变量 public void 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域 } }
说明:假设线程A先执行writer()方法,随后另一个线程B执行reader()方法。下面我们通过这两个线程的交互来说明这两个规则。
写 final 域的重排序规则禁止把 final 域的写重排序到构造函数之外。这个规则的实现包含下面两个方面:
writer方法的obj = new FinalExample();其实包括两步,首先是在堆上分配一块内存空间创建FinalExample对象,然后将这个对象的地址赋值给obj引用。假设线程 B 读对象引用与读对象的成员域之间没有重排序,则可能的时序图如下:
说明:写普通域的操作被编译器重排序到了构造函数之外,读线程 B 错误的读取了普通变量 i 初始化之前的值。而写 final 域的操作,被写 final 域的重排序规则 “限定”在了构造函数之内,读线程 B 正确的读取了 final 变量初始化之后的值。写 final 域的重排序规则可以确保:在对象引用为任意线程可见之前,对象的 final 域已经被正确初始化过了,而普通域不具有这个保障。以上图为例,在读线程 B “看到”对象引用 obj 时,很可能 obj 对象还没有构造完成(对普通域 i 的写操作被重排序到构造函数外,此时初始值 2 还没有写入普通域 i)。
读 final 域的重排序规则如下:
在一个线程中,“初次读对象引用”与”初次读该对象包含的 final 域”,JMM 禁止处理器重排序这两个操作(注意,这个规则仅仅针对处理器)。
编译器会在读 final 域操作的前面插入一个 LoadLoad 屏障。
初次读对象引用与初次读该对象包含的 final 域,这两个操作之间存在间接依赖关系。由于编译器遵守间接依赖关系,因此编译器不会重排序这两个操作。大多数处理器也会遵守间接依赖,大多数处理器也不会重排序这两个操作。但有少数处理器允许对存在间接依赖关系的操作做重排序(比如 alpha 处理器),这个规则就是专门用来针对这种处理器。
reader方法包含三个操作: ① 初次读引用变量 obj。② 初次读引用变量 obj 指向对象的普通域 i。③ 初次读引用变量 obj 指向对象的 final 域 j。
假设写线程 A 没有发生任何重排序,同时程序在不遵守间接依赖的处理器上执行,下面是一种可能的执行时序:
上面我们的例子中,final域是基本数据类型,如果final与为引用类型的话情况会稍微不同。 对于引用类型,写 final 域的重排序规则对编译器和处理器增加了如下约束:
在构造函数内对一个 final 引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
在引用变量为任意线程可见之前,该引用变量指向的对象的 final 域已经在构造函数中被正确初始化过了。
在构造函数内部,不能让这个被构造对象的引用为其他线程可见,也就是对象引用不能在构造函数中“逸出”。
转载:http://www.cnblogs.com/leesf456/p/5291484.html
来源: https://blog.csdn.net/hbtj_1216/article/details/76407826