[多线程] - volatile关键字的特性
文章目录
一、volatile关键字是什么
在日常的面试中,volatile无疑是被提到频率非常高的一个问题,在各大公众号中也充满了对volatile关键字的解析,那么volatile究竟是什么呢???都被应用在了哪些地方呢???
首先在理解关键字方面,我觉得理解这个关键字的中文含义是有助于我们记忆的,那么打开百度翻译搜索volatile可以得到如下结果:
通过百度翻译可以知道volatile作为一个形容词,主要是用来形容易改变的东西,作为Java语言的关键字,volatile常被用来修饰一个易变的参数。那么我们接下来还要搞清楚一个问题就是被volatile修饰的变量会产生哪些变化?也就是volatile关键字的作用是什么?
二、volatile的引入
话不多说,我们先看一段Demo:
public class VolatileTest {
private static boolean flag;
// 违反可见性验证
public static void main(String[] args) {
flag = true;
new Thread(() -> {
try {
//通过线程睡眠达到延时修改flag标记的作用
Thread.currentThread().sleep(10l);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = false;
}).start();
System.out.println("启动循环");
// 如果上面flag修改生效 程序将退出循环
while (flag) {
}
System.out.println("退出循环");
}
}
实验结果:
上面的实验结果可以看出,新建的线程虽然改变了flag的值,但是main线程并没有感知到,因此不能退出循环,这里要注意点是,while循环体内不要有任何代码,否则会影响结果。那么我们要怎么做才能够让主线程感知到flag值得变化呢?那肯定都不用猜,这章讲的就是volatile,上述在声明变量时作如下修改:
private volatile static boolean flag;
是的你没有看错,只需要加一个单词就可以改变程序的运行结果:
这里我们可以看到程序已经正常的退出了,那么volatile在这里都做了些什么呢??
三、volatile的特性
1.volatile与可见性
什么是可见性呢?可加性简单的解释就是当某个线程对共享变量进行改变的时候,其他线程可以得知变量更改的过程和变量最新的状态。
上述Demo中,我们对于共享变量flag增加了volatile关键字进行描述,当某个线程在尝试更改flag的值得时候,JVM会通知其他线程他们之前读取的变量缓存已经失效,需要去共享内存区中取最新的变量值。
关于volatile的实现原理
关于线程如何通知互相更新缓存的问题涉及了计算机原理的部分知识,如果感兴趣可以去学习下内存屏障和CPU的缓存一致性协议,这里就不过多赘述
2.volatile与原子性
我们通过之前的Demo证明了volatile具备可见性,那么并发编程三大特性中的原子性volatile支持吗?话不多说,上DEMO:
public class VolatileAtomicity {
public volatile int count = 0;
private void addCount() {
//count 自增
for (int i = 0; i < 100; i++) {
count++;
}
System.out.println("count = " + count);
}
public static void main(String[] args) {
VolatileAtomicity volatileAtomicity = new VolatileAtomicity();
// 循环创建1000个新线程 每个线程count自增100次 期望结果为100000
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
volatileAtomicity.addCount();
}).start();
;
}
}
}
期望结果:100000
运行结果:
这里我们看到 虽然我们为共享变量count增加了volatile关键字修饰,但是最后的运行结果并没有符合我们的期望值,这是为什么呢?其实主要是count++在jvm解析的时候是将他做了拆解:
- 首先读取count的值,此时由于volatile具备可见性,所以此时count的值是最新值
- 对此时count的值进行运算,这里需要注意的是在这个时候其他线程可能已经对count的值作了修改,所以此时count+1的值已经违背了线程安全的策略
- 将count+1的运算结果重新赋值给count,此时count的值已经偏离预期
从上面的例子可以看出,volatile只能在读取值得时候保证值得可见性,但是后续运算的时候并不能保证并发线程引起的线程安全问题,所以volatile并不具备原子性。
这里需要对volatile的可见性做一个补充说明: volatile的可见性是体现在线程读取变量的时候获取到的是最新变量,而读取后做运算操作时不会再去获取最新的变量,这部分概念设计内存的读写屏障,感兴趣的同学可以自行百度。
3.volatile与有序性
在了解有序性的之前我们首先要了解一个概念就是CPU的指令重排序
CPU在对代码进行执行的时候,并不会严格的按照代码的编写顺序进行执行,而是在遵守as-if-serial的原则下,对指令进行适当的重排序后才进行执行,这主要是为了提升CPU的执行效率,减少空置时间。
那么首先我们要做的就是证明CPU存在指令重排序,话不多说上Demo:
public class VolatileOrderliness {
private static long a = 0;
private static long b = 0;
private static long c = 0;
private static long d = 0;
private static long e = 0;
private static long f = 0;
private static long g = 0;
private static long h = 0;
public static long count = 0;
public static void main(String[] args) {
// 由于cpu指令重排发生存在概率 所以使用死循环调用 然后再出现的时候通过break跳出循环
for (;;) {
a = 0;
b = 0;
c = 0;
d = 0;
e = 0;
f = 0;
g = 0;
h = 0;
count++;
Thread t1 = new Thread(() -> {
a = 1;
c = 101;
d = 102;
g = b;
});
Thread t2 = new Thread(() -> {
b = 1;
e = 201;
f = 202;
h = a;
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
String result = "count = " + count + " g = " + g + ", h=" + h;
if (g == 0 && h == 0) {
// 当g 和h 都出现0的时候 一定是发生了指令重排序
System.err.println(result);
break;
} else {
System.out.println(result);
}
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
}
}
运行结果:
首先我们分析下Demo,无论是线程t1先执行亦或是线程t2先执行,他们的第一行代码都是为a和b赋值为1,也就是说正常的代码逻辑下,作为每个线程最后执行的赋值g和h一定有一个的值为1,但是此时g和h的值都为0,说明了CPU在执行代码的时候打乱了代码的执行顺序进行了重新排序,排序如下:
CPU将g和h的赋值操作调换到了a和b赋值操作之前,解决这个问题其实也很简单,我们只需要为a和b加上volatile关键字就可以了。
四、volatile面试相关
在这里我也总结了一些volatile常见的面试题分享给大家。
1 . 谈一谈平时哪些场景应用到了volatile
答:volatile最简单的应用场景就是用在double-check-lock方式实现的单例对象中,在DCL单例模式中volatile主要是为了防止线程拿到因为指令重排序造成的半初始化变量。
2. volatile和synchronized有哪些异同
答:
1)volatile主要用来修饰变量,synchronized主要用来修饰代码块。
2)volatile不能保证原子性,synchronized可以
3)volatile不会造成线程堵塞,synchronized会造成
好的,到此如果还有疑问或者觉得作者有错误的地方,欢迎留言!
祝好!
本文地址:https://blog.csdn.net/xiaoai1994/article/details/110437416
上一篇: StringBuilder简介