持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第27天,点击查看活动详情
正确理解volatile保证线程间的可见性的功能
今天我们一起来学习一下,volatile的其中一个功能,也是一个比较容易弄混的功能。 我们都知道volatile最主要的功能,有以下几个:
1.保证线程间可见性
2.禁止指令重排序
相信各位对于volatile的第二个功能(禁止指令重排序)已经很熟悉了,其实就是高级代码里划分的多个指令,在真正运行的时候,有可能进行重新排序,也就是和原来排序的顺序不一致;这样的情况下,可能会出现某些难以解释的问题,而volatile的出现,就可以很好地解决了这个问题。
那对于volatile的第一个功能,大家伙是不是也是同样熟悉呢?有没有理解全面??
下面咱们一起来验证一下!
volatile保证线程间基本数据类型的可见性
我们一般情况下,是用volatile来修饰基本数据类型的变量,这样来保证该变量的数据在各线程之间可见。 下面我们一起来看个例子: 新建一个线程 t1 ,t1调用一个testMethod方法一直在空转;这时候,使用主线程将 mark 的值改为false,看线程t1是否可以成功执行完毕;如果是线程间不可见(没有加volatile),则线程t1无法完毕。
public class T10_Visibleness {
// 这里使用和不使用volatile的结果是完全不同的
// 使用了volatile线程 t1 才能执行完毕
/*volatile*/ boolean mark = true;
void testMethod() {
System.out.println("start...");
while (mark) {
// System.out.println("recusion...");
}
System.out.println("end...");
}
public static void main(String[] args) {
T10_Visibleness t10 = new T10_Visibleness();
new Thread(t10::testMethod, "t1").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (Exception e) {
e.printStackTrace();
}
t10.mark = false;
}
}
volatile修饰引用对象时
下面这个例子就是讲述volatile修饰引用对象时的一个经典例子,我们一起来分析一下: 下面自定义了一个引用对象MyData,由一个setData线程通过for循环不断地创建新的对象,而这时候由getData线程去读取,这时候就会发现引用对象还是MyData,但是MyData里面的成员变量实际上已经改变了,可是这个点是别的线程无法感知到的。
public class T11_VolatileReference {
private static class MyData {
int a, b;
public MyData(int a, int b) {
this.b = b;
this.a = a;
}
}
volatile static MyData myData;
public static void main(String[] args) {
// 在这里不停地创建 data 实例
Thread setData = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
myData = new MyData(i, i);
}
});
// 引用对象本身可见,
// 但是其内部字段的可见性并不保证,
// 从而导致了 a 和 b 的值可能出现不同
Thread getData = new Thread(() -> {
while (myData == null) {
}
int x = myData.a;
int y =myData.b;
if (x != y) {
System.out.printf("a = %s, b=%s%n", x, y);
}
});
setData.start();
getData.start();
try {
getData.join();
setData.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("end");
}
}