缓存导致可见性问题,编译优化导致有序性问题,解决方案是按需禁用缓存和编译优化。java内存模型是个很复杂的规范,规范了jvm如何提供按需禁用缓存和编译优化的方法。方法包含volatile、synchronized和final三个关键字,以及六个Happens-Before规则。
语义含义:例如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规则。规则含义:前面一个操作结果对后续操作是可见的。
指在一个线程中,按照代码顺序,前面的操作Happens-Before后续任意操作。比如上述代码x=42就发生在v=true之前,所以x变量对后续操作是可见的。
指一个volatile变量的写操作,Happens-Before后续对这个变量的读操作。关联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是可见的。指的是:对一个锁的解锁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。
指的是:主线程A启动子线程B,那么子线程B可以看到主线程启动子线程B之前的操作。
Thread B = new Thread(()->{ // 主线程调用 B.start() 之前 // 所有对共享变量的修改,此处皆可见 // 此例中,var==77 }); // 此处对共享变量 var 修改 var = 77; // 主线程启动子线程 B.start();指的是:主线程A等待子线程B完成(A调用B的join()方法),B完成后,A能看到B的操作。
Thread B = new Thread(()->{ // 此处对共享变量 var 修改 var = 66; }); // 例如此处对共享变量修改, // 则这个修改结果对线程 B 可见 // 主线程启动子线程 B.start(); B.join() // 子线程所有对共享变量的修改 // 在主线程调用 B.join() 之后皆可见 // 此例中,var==66参考文档: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的值发生变化。
public FinalFieldExample() { // bad! x = 3; y = 4; // bad construction - allowing this to escape global.obj = this; }构造函数逸出,其他线程通过global.obj读取x不一定是3。
