解决什么问题
Java内存模型解决了并发程序中的可见性问题和有序性问题
Java内存模型
按需禁用
- CPU缓存会导致可见性问题,编译优化会导致有序性问题
- 解决可见性和有序性最直接的办法:_禁用CPU缓存和编译优化_
- 合理的方案:_按需禁用CPU缓存和编译优化_
- 为了解决可见性和有序性的问题,只需要给程序员提供按需禁用CPU缓存和编译优化的方案即可
程序员视角
- Java内存模型规范了_JVM如何提供按需禁用CPU缓存和编译优化的方法_
- 具体方法包括:
volatile
、synchronized
和final
关键字,以及六个Happens-Before
规则
volatile
- volatile关键字不是Java语言的特产,在古老的C语言也有,最原始的意义就是_禁用CPU缓存_
volatile int x = 0
:告诉编译器,对这个变量的读写,不能使用CPU缓存,必须从内存中读取或者写入
Java代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public class VolatileExample { private int x = 0; private volatile boolean v = false;
public void writer() { x = 42; v = true; }
public void reader() { if (v) { } } }
|
- 假设线程A执行writer(),按照volatile语义,会把变量
v=true
写入内存
- 假设线程B执行reader(),同样按照volatile语义,线程B会从内存读取变量v
- 如果线程B看到
v==true
- 如果Java低于1.5,x可能是42,也可能是0
- 如果Java高于等于1.5,x是42
- JMM在Java 1.5通过Happens-Before对volatile语义进行了增强
Happens-Before规则
理解
- 望文生义的理解:前面一个操作发生在后续操作的前面
- 正确的理解:_前面一个操作的结果对后续操作是可见的_
- 正式的说法:Happens-Before_约束了编译器的优化行为_
- 虽然允许编译器优化,但要求编译器优化后一定要遵循Happens-Before规则
- Happens-Before规则是JMM里面比较难理解的内容,与程序员相关的规则有六项,都与可见性相关
程序的顺序性规则
在同一个线程中,按照程序顺序,前面的操作Happens-Before于后面的任意操作
volatile变量规则
- 对一个volatile变量的写操作,Happens-Before于后续对这个volatile变量的读操作
- 对一个volatile变量的写操作相对于后续对这个volatile变量的读操作_可见_
传递性规则
- 如果A Happens-Before B,并且B Happens-Before C,那么A Happens-Before C
- 程序的顺序性规则:
x=42
Happens-Beforev=true
- volatile变量规则:写变量
v=true
Happens-Before读变量v=true
- 传递性规则:
x=42
Happens-Before读变量v=true
- 如果线程B读到了
v=true
,那么线程A设置的x=42
对线程B是可见的,即线程B能看到x==42
- 这就是Java 1.5对volatile语义的增强,该版本的JUC就是靠volatile语义实现可见性的
管程中锁的规则
1 2 3 4 5 6 7 8
| public void fun() { synchronized (this) { if (this.x < 12) { this.x = 12; } } }
|
- 对一个锁的解锁Happens-Before于后续对这个锁的加锁
- 管程是一种通用的同步原语,Java中的
synchronized
就是Java对管程的实现
- 管程中的锁在Java里是隐式实现的
- 在进入代码块之前,会自动加锁,而在代码块执行完会自动释放锁
- 加锁和释放锁都是编译器帮我们实现的
- x的初始值是10,线程A执行完代码块后,x的值变成了12(执行完自动释放锁)
- 线程B进入代码块时,能够看到线程A对x的写操作,即线程B能够看到x==12
线程start()规则
1 2 3 4 5 6 7 8 9 10
| Thread B = new Thread(() -> { System.out.println(var); });
var = 77;
B.start();
|
- 主线程A启动子线程B后,子线程B能够看到主线程A在启动子线程B之前的操作
- 即线程A调用线程B的start()方法,那么该start()方法Happens-Before于B中的任意操作
线程join()规则
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| Thread B = new Thread(() -> { System.out.println(var); var = 66; });
var = 77; B.start();
B.join();
System.out.println(var);
|
- 主线程A等待子线程B完成(主线程A通过调用子线程B的join()方法来实现)
- 当子线程B完成后(主线程A中join()方法返回),主线程A能够看到子线程B的操作
- 即如果在线程A中调用线程B的join()并成功返回,那么线程B中的任意操作Happens-Before于该join()方法的返回
final
- volatile的目的:禁用CPU缓存(可见性)和编译优化(有序性)
- final修饰变量时,初衷是告诉编译器,该变量是一直不变的,可以尽量优化
- 曾经优化过度,导致异常
- 例如在利用双重检查创建单例时,构造函数的错误重排列会导致线程可能会看到final变量的值会变化
- 在Java 1.5的JMM对final类型变量的重排进行了约束
逸出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class EscapeExample {
public static Object global_obj; final int x; final int y;
public EscapeExample() { x = 3; y = 4; global_obj = this; } }
|
小结
- Happens-Before的语义是一种_因果关系_
- 如果事件A是导致B事件的起因,那么事件A一定Happens-Before事件B
- 在Java里,Happens-Before的语义本质是一种可见性
- A Happens-Before B意味着A对B来说是可见的,不论A和B是否发生在同一个线程里
- Java内存模型的受众
参考资料
Java并发编程实战