Java架构师学习之路之并发编程二: volatile关键字
经过上一章的学习,我们知道了JMM是围绕原子性、可见性、有序性进行设计的,但是JMM本身并不是一个实际存在的东西,他只是一种规范。那么Java种是如何实现原子性、可见性和有序性的呢?本章将会和大家一起学习volatile关键字,实现可见性和有序性。
volatile关键字、指令重排和内存屏障
volatile关键字
1. 什么是volatile关键字
volatile关键字用于解决java各个线程间数据不共享的问题,即:线程1和线程2同时对同一个值进行操作,则两个线程都会基于初始值进行操作,而不会将两个操作结果叠加。这在并发环境下是一个致命的问题,因此我们需要使用volatile关键将各个线程副本的值变成线程间可见。或者我们也可以使用锁进行串行化操作。
2. volatile关键字的作用
- 实现线程间的变量可见性。
- 阻止上下文的指令重排(懵比…)
3. volatile关键字的原理
可见性:volatile算是缓存一致性协议的具体实现,加上volatile则表示对改变量的操作遵从缓存一致性协议。线程1和线程2将flag = false读取到工作副本中,此时线程1进行flag = true赋值操作,同时线程2将会一致监听(嗅探)缓存一致性协议。在线程1中,该变量将会被标记“已修改”,并将该标记通过缓存一致性协议通知给线程2,线程2则会舍弃旧值,直接从主内存读取。但是,线程1修改了值以后只会先写回三级缓存,而不会及时写回主内存,因此线程2即便从主内存重新读取也会发现值仍然是旧值。此时线程2也不会一直等待线程1写回主内存,而是会进行指令重排,优先执行后续的指令。
验证可见性的代码如下:
private boolean flag = false;
public void load() {
System.out.println("load: 准备尝试获取更新后的flag...");
while (!flag) {
}
System.out.println("load: 成功嗅探到更新后的flag...");
}
public void refresh() {
System.out.println("refresh: 开始更新flag");
flag = true;
System.out.println("refresh: 更新flag完毕: " + flag);
}
public static void main(String[] args) throws InterruptedException {
TestController test = new TestController();
Thread tLoad = new Thread(() -> {
test.load();
});
Thread tRefresh = new Thread(() -> {
test.refresh();
});
tLoad.start();
Thread.sleep(2000);
tRefresh.start();
}
线程1执行load方法,线程2执行refresh方法。先启动线程1,线程1会循环进行!flag的判断,如果我们不使用volatile,则此时不会遵循缓存一致性协议,因此将load方法将一直循环下去。
如果我们使用volatile定义flag:
private volatile boolean flag = false;
此时,由于使用了volatile,所以会遵守缓存一致性协议,线程1会舍弃工作副本的值,转而从主内存中查询最新值。在示例代码中,在循环中判断!flag后没有其他的操作,因此不会产生指令重排。而是此时会一直等待线程1将最新的值同步回主内存。
最终,将会输出load: 成功嗅探到更新后的flag...
4. volatile关键字引发的问题——总线风暴
volatile最关键的特性就是,会一直通过缓存一致性协议进行监听或嗅探,只要其他线程没有将最新值同步回主内存,嗅探就会一直占用总线。而我们上一章中讲述了一个知识:总线是有带宽限制的 (详情请参见上一章:Java架构师学习之路之并发编程一: 缓存一致性协议和JMM)
因此如果在高并发场景下,滥用volatile关键字则会出现总线被占满的问题。这个问题叫做——总线风暴。
5. 如何解决总线风暴
- 不滥用volatile关键字
- 在某些业务场景下选择使用同步锁
拓展:单例模式与volatile
大家可能会奇怪,单例模式和volatile有什么关系。别着急,一起来看看下面的代码:
class TestSingleton {
private static Object object;
public static Object getObject() {
if (object == null) {
synchronized (TestSingleton.class) {
if (object == null) {
object = new Object();
}
}
}
return object;
}
}
从平常咱们的视野来看,这个懒汉单例模式是没问题的。
但是仔细考虑一下object = new Object()这行代码的执行原理就能发现问题:
执行原理如下:
// 1. 申请内存
allocate();
// 2. 创建对象
new Object();
// 3. 赋值
assign();
如果是超高并发场景下,new Object()和assign()可能会发生指令重排问题,因此执行顺序可能会颠倒,导致申请完内存后直接赋值。如果此时有别的线程进行if (object == null)的判断,则此时由于allocate()已经申请了内存地址,并且内存地址通过assign()操作赋值给了object,所以object不为null,直接返回该object,如果紧接着调用object中的方法,则会抛出空指针问题,因为这个object指向的是空地址。
解决方法:
- 禁止指令重排。使用volatile修饰object变量则可以禁止指令重排
- 或者在2和3两步操作之间调用Unsafe类里面的这三个方法,手动添加内存屏障,以禁止指令重排(本例不适用)
public native void loadFence();
public native void storeFence();
public native void fullFence();
下一章:Java架构师学习之路之并发编程三: Java同步器之Syncronized&Lock&AQS,让我们一起朝着架构师努力吧~~
本文深入探讨了Java中的volatile关键字,详细解释了其作用、原理及带来的总线风暴问题。volatile确保线程间变量的可见性,防止指令重排,但在高并发场景可能导致总线风暴。解决方案包括合理使用锁。此外,文中还讨论了volatile在单例模式中的应用及其避免指令重排的重要性。

579

被折叠的 条评论
为什么被折叠?



