文章目录
1. 什么是java内存模型
缓存导致可见性问题,编译优化导致有序性问题,解决方案是按需禁用缓存和编译优化。java内存模型是个很复杂的规范,规范了jvm如何提供按需禁用缓存和编译优化的方法。方法包含volatile、synchronized和final三个关键字,以及六个Happens-Before规则。
2. 使用volatile的困惑
语义含义:例如volatile int x=0;告诉编译器不能使用CPU缓存,对变量x的读写,必须在内存中进行。
使用的困惑,先上代码。
class VolatileExample {
int x = 0;
volatile boolean v = false;
public void writer() {
x = 42;
v = true;
}
public void reader() {
if (v == true) {
// 这里 x 会是多少呢?
}
}
}
- 假设线程A执行writer()方法,按照volatile语义,v=true写到内存;
- 同样的,假设线程B执行reader()方法,从内存中读取到v==true, 此时x=0还是42?
- jdk1,5版本之前x可能是0,1.5之后对volatile语义增强,所以是42,这里涉及到Happens-Before规则。
3. Happens-Before 规则
规则含义:前面一个操作结果对后续操作是可见的。Happens-Before 约束了编译器的优化行为,虽允许编译器优化,但是要求编译器优化后一定遵守 Happens-Before 规则。
3.1 程序的顺序性规则
指在一个线程中,按照代码顺序,前面的操作Happens-Before后续任意操作。比如上述代码x=42就发生在v=true之前,所以x变量对后续操作是可见的。
3.2 volatile变量规则
指一个volatile变量的写操作,Happens-Before后续对这个变量的读操作。关联3.3的规则。
3.3 传递性
指A Happens-Before B, B Happens-Before C, 那么A Happens-Before C。
从图中看到:
- x=42 Happens-Before v=true, 这是规则1;
- 写变量v=true Happens-Before 读变量v=true,这是规则2;
- 根据规则3,x=42 Happens-Before 读变量v=true, 换句话说,线程B可以读到x=42,x对线程B是可见的。
3.4 管程中锁的规则
指的是:对一个锁的解锁Happens-Before后续对这个锁的加锁。
管程:一种通用的同步术语,在java中的实现是synchronized。
管程中的锁在java中是隐式实现的,例如下面的代码:进入代码块,自动加锁;执行完代码块自动释放锁。加锁和释放锁都是编译器实现的。
synchronized (this) { // 此处自动加锁
// x 是共享变量, 初始值 =10
if (this.x < 12) {
this.x = 12;
}
} // 此处自动解锁
结合此规则:线程A执行完代码块,x=12,线程A释放锁;此时线程B获得锁,线程B能看到线程A对x的写操作,即B看到x=12。
3.5 线程start()规则
指的是:主线程A启动子线程B,那么子线程B可以看到主线程启动子线程B之前的操作。
Thread B = new Thread(()->{
// 主线程调用 B.start() 之前
// 所有对共享变量的修改,此处皆可见
// 此例中,var==77
});
// 此处对共享变量 var 修改
var = 77;
// 主线程启动子线程
B.start();
3.6 线程join()规则
指的是:主线程A等待子线程B完成(A调用B的join()方法),B完成后,A能看到B的操作。
Thread B = new Thread(()->{
// 此处对共享变量 var 修改
var = 66;
});
// 例如此处对共享变量修改,
// 则这个修改结果对线程 B 可见
// 主线程启动子线程
B.start();
B.join()
// 子线程所有对共享变量的修改
// 在主线程调用 B.join() 之后皆可见
// 此例中,var==66
4. 被我们忽视的final
参考文档:JSR 133 (Java Memory Model) FAQ。
final修饰的变量,变量一旦赋值之后就不再变化。两种情况如下:
- final int a = 1; a的值不再变化;
- final MyTest test = new MyTest(); test的引用不再变化。
class FinalFieldExample {
final int x;
int y;
static FinalFieldExample f;
public FinalFieldExample() {
x = 3;
y = 4;
}
static void writer() {
f = new FinalFieldExample();
}
static void reader() {
if (f != null) {
int i = f.x;
int j = f.y;
}
}
}
例子展示了final变量如何使用,当某个线程执行reader(), 可以保证线程看到f.x是3,但不保证f.y是4。
类似双重检查创建单例,构造函数的错误重排可能看到final的值发生变化。
在 1.5 以后 Java 内存模型对 final 类型变量的重排进行了约束。现在只要我们提供正确构造函数没有“逸出”,就不会出问题了。
public FinalFieldExample() { // bad!
x = 3;
y = 4;
// bad construction - allowing this to escape
// 此处就是 this 逸出,
global.obj = this;
}
构造函数逸出,在构造函数里面将 this 赋值给了全局变量 global.obj,这就是“逸出”。其他线程通过global.obj读取x不一定是3,有可能是0.
5.总结
在 Java 语言里面,Happens-Before 的语义本质上是一种可见性,A Happens-Before B 意味着 A 事件对 B 事件来说是可见的,无论 A 事件和 B 事件是否发生在同一个线程里。例如 A 事件发生在线程 1 上,B 事件发生在线程 2 上,Happens-Before 规则保证线程 2 上也能看到 A 事件的发生。
Java 内存模型主要分为两部分,一部分面向你我这种编写并发程序的应用开发人员,另一部分是面向 JVM 的实现人员的,我们可以重点关注前者,也就是和编写并发程序相关的部分,这部分内容的核心就是 Happens-Before 规则。
6.课后思考
有一个共享变量 abc,在一个线程里设置了 abc 的值 abc=3
,你思考一下,有哪些办法可以让其他线程能够看到abc==3?
我的回答:
- 加volatile修饰
- 读写时加同一把锁