一、构造方法溢出问题
考虑下面的代码:
num3和num4的值是否一定是1和2? num3、num4不见得一定等于1,2。
和DCL的例子类似,也就是构造方法溢出问题。 myClass = new MyClass()这行代码,分解成三个操作: 1. 分配一块内存; 2. 在内存上初始化i=1,j=2; 3. 把myClass指向这块内存。 操作2和操作3可能重排序,因此线程B可能看到未正确初始化的值。对于构造方法溢出,就是一个 对象的构造并不是“原子的”,当一个线程正在构造对象时,另外一个线程却可以读到未构造好的“一半对 象”。
(资料图片仅供参考)
二、final的happen-before语义
要解决这个问题,不止有一种办法。
办法1:给num1,num2加上volatile关键字。
办法2:为read/write方法都加上synchronized关键字。
如果num1,num2只需要初始化一次,还可以使用final关键字。
之所以能解决问题,是因为同volatile一样,final关键字也有相应的happen-before语义:
1. 对final域的写(构造方法内部),happen-before于后续对final域所在对象的读。
2. 对final域所在对象的读,happen-before于后续对final域的读。 通过这种happen-before语义的限定,保证了final域的赋值,一定在构造方法之前完成,不会出现 另外一个线程读取到了对象,但对象里面的变量却还没有初始化的情形,避免出现构造方法溢出的问题。
三、happen-before规则总结
1. 单线程中的每个操作,happen-before于该线程中任意后续操作。
2. 对volatile变量的写,happen-before于后续对这个变量的读。
3. 对synchronized的解锁,happen-before于后续对这个锁的加锁。
4. 对final变量的写,happen-before于final域对象的读,happen-before于后续对final变量的 读。
四个基本规则再加上happen-before的传递性,就构成JMM对开发者的整个承诺。在这个承诺以外 的部分,程序都可能被重排序,都需要开发者小心地处理内存可见性问题。
标签: