我看很多人说对volatile的读操作一定是去主存中读取。
而我的理解是:写操作本身会有一个lock指令或者说StoreLoad屏障,意味着立即将写入的值刷入主存并通过MESI协议告知其它线程对于该变量缓存的无效,从而重新去主存中获取。
也就是说读操作只有在MESI将缓存行置为无效的时候才会去主存读取啊,如果没有volatile写的话,应该是不会每次都去主存中获取吧?
关于这一点有个另外的问题我需要问一下:
很多的文章中都有说到,如果volatile修饰一个引用类型变量,那么对该变量指向的对象的域是不具有volatile的性质。这一点我没意见。从汇编来看确实没有对volatile对象的域的操作有lock指令。
但是比如有如下定义:
package test;
public class Test implements Runnable {
static class ObjectA {
private boolean flag = true;
}
private volatile ObjectA a;
public Test(ObjectA a) {
this.a = a;
}
@Override
public void run() {
while (a.flag) {
// i++;
// System.out.println(a.flag);
}
}
public void stop() {
a.flag = false;
}
public static void main(String[] args) throws InterruptedException {
Test test = new Test(new ObjectA());
Thread t = new Thread(test);
t.start();
Thread.sleep(1000);// 保证线程已启动
test.stop();
t.join();
}
}
可以看到如果a是volatile的话线程可以结束。
但是如果a不是volatile的话,线程将一直无法结束(如果你的CPU很慢还是有可能会结束的),因为没有volatile修饰的话a.flag一直读的是缓存,而非主存值,除非cpu有空才会去重新获取flag的值,比如在循环体内加一些复杂操作,new个对象sysout以下什么的。
当然确实因为flag这个变量不是volatile,所以a.flag = false这个写操作并不能保证可见性这一点我没有疑问。
但是为什么a.flag能够得知flag的修改呢?按照一开始我说的,难道不应该是只有对volatile的写操作才会导致MESI协议通知其他CPU该缓存无效,从而去主存中获取值吗?而a.flag = false并没有改变a本身引用,那么实际上while(a.flag)中对a引用的读取应该不会去主存当中读取,而对flag的读取自然也就是读的缓存,换句话说它不应该能够得知其域值的改变。
那么只有1个解释:
每一次对a引用的读取都会无条件的去主存当中获取a的引用地址也就是对象,然后将CPU中缓存的flag域值更新,最终可以得到正确的flag域值。但这样一来就表示对于volatile读来说一定不会读缓存,无论是否有其它的线程对该变量有过修改。
当然上面的解释就和问题一开始我对于volatile写的理解产生冲突了
按照上面这种说法我是不是可以理解为:volatile引用类型变量的域(对于对象)或元素(对于数组),如果没有声明volatile的话,其写操作是不具备volatile写特性的,但是其读操作却具有volatile读特性。所以严格来说,实际上这些域和元素是不具备完全的可见性的(只有等写操作线程CPU心情好了,将写入值写回主存的时候,读线程可以立即读取到)。
多谢指教!如有不明请指出,谢谢!