JMM可确保声明为volatile的字段,写入操作之后,其值对其他所有线程可见。对于volatile引用变量,虽然可以确保该引用本身将及时对其他线程可见,但对于引用对象的成员变量而言,情况并非如此。如果单独访问,不能保证对象中包含的数据将始终可见。
1、看一个例子:
public class VolatileTest {
private static volatile Data data;
public static void setData(int a, int b) {
data = new Data(a, b);
}
private static class Data {
private int a;
private int b;
public Data(int a, int b) {
this.a = a;
this.b = b;
}
public int getA() {
return a;
}
public int getB() {
return b;
}
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10000; i++) {
final int ii = i;
int a = i;
int b = i;
// writer
Thread writerThread = new Thread(() -> {
setData(a, b);
});
// reader
Thread readerThread = new Thread(() -> {
while (data == null) {
}
int x = data.getA();
int y = data.getB();
if (x != y) {
System.out.printf("readerThread2:a = %s, b = %s, i = %s%n", x, y,ii);
}
});
writerThread.start();
readerThread.start();
writerThread.join();
readerThread.join();
}
System.out.println("finished");
}
}
执行后输出:
readerThread2:a = 93964, b = 93965, i = 93965
finished
说明:
虽然Data实例是volatile,单a和b的更新不具有原子性。如果需要多个变量的原子性,我们应该同步我们的代码。原因是因为读线程中获取变量a和b,和写线程之间没有happen-before关系。
解决方法:写入a、b的方法和获取a、b的方法进行同步。在Data类中增加两个方法,这两个方法用sychronized修饰,读写线程分别调用这两个方法
public synchronized void setValues(int a, int b) {
this.a = a;
try {
TimeUnit.MICROSECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.b = b;
}
public synchronized int[] getValues() {
return new int[]{a, b};
}
注意:对于多变量可见性问题、单变量复合操作问题,只能用“锁”的方式来解决。
2、”双缓冲+引用赋值“的方法:
上述示例很常见,在线业务系统中通常这么做:一个写线程异步更新某个对象,线上web线程实时的访问(读取)该对象。根据前面知识,这种场景下会出现数据争用,又由于是引用类型,所以使用volatile关键字是无法解决的。除了上面加锁的方式解决,通常采用“双缓冲+引用赋值”的方法来解决。
可以看到写线程中调用的setData方法每次都会创建一个新的对象实例,然后赋值给data引用,这种手法就叫“双缓冲”。在读线程中由于data是引用变量,并且和写线程之间没有happen-before关系,所以从data引用中获取各种属性的时候不是原子的操作,这样就会造成一次读取过程中,data所引用的数据不一致。
可以简单在读线程中,加一个临时引用的赋值,来解决这个问题,请看:
// reader
Thread readerThread = new Thread(() -> {
Data myData = data;
while (myData == null) {
myData = data;
}
int x = myData.getA();
int y = myData.getB();
if (x != y) {
System.out.printf("a = %s, b = %s, i=%s%n", x, y, ii);
}
});
说明:在读线程中首先使用临时变量myData来引用data变量的实例,接下来的操作都是对myData变量的操作,因为:
- 变量的赋值jvm会保证原子性;
- 写线程使用了双缓冲的方式来改变data变量的引用,即使data引用被改变了也没关系,因为myData变量依然会引用之前的实例;
所以,经过这样处理后,读线程中使用到的Data实例数据是一致的。
3、原子引用:
参考:
https://www.logicbig.com/tutorials/core-java-tutorial/java-multi-threading/volatile-ref-object.html