java中的volatile关键字

Posted by 张伟真 on 2024-11-13
Estimated Reading Time 3 Minutes
Words 838 In Total
Viewed Times

java中的volatile关键字

定义

volatile是java中的关键字,主要修饰变量,提供了可见性和禁止指令重排的特性,不保证原子性

1. 可见性

多线程下,每个线程有自己的工作内存,线程对共享变量读写不一定是直接在主存中进行的,可能在线程自己的工作内存.当多线程下有线程改了共享变量的值,而这个值对其他线程来说是不可见的,导致脏读现象
volatile关键字就是解决这个问题的
volatile的作用保证:

  1. 写操作时,当线程修改了volatile修饰的变量值时,值将立即刷新主存
  2. 读操作时,从主存中读取最新的值,而不是读取工作内存里面的值

实现原理

volatile通过内存屏障(Memory Barrier)实现可见性, 内存屏障是CPU的指令,可以阻止编译器和CPU对指令进行重新排序,并且强制将工作内存的值刷新到主存,或者主存中读取数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private static volatile boolean state = false;
public void demo() {

Thread t1 = new Thread(() -> {
while(!state) {
// do something
}
log.info("Thread 1 stopped");
});

Thread t2 = new Thread(() -> {
try{
Thread.sleep(2000);
} catch(Exception e) {
log.error("error");
}
state = true;
log.info("Thread 2 set state true");
});
t1.start();
t2.start();
}

2. 禁止指令重排

编译器和CPU为了优化性能,在执行指令前有可能对指令的执行顺序进行排序在不影响最终结果的情况下.但是多线程场景的指令重排会导致数据错乱的可能
具体为以下三种情况

  1. 写后读: 不能将volatile修饰的变量的写操作重排到读操作之后
  2. 写后写: 不能将volatile修饰的变量的写操作重排到另一个volatile修饰的变量的写操作之后
  3. 读后读/写: 不能将volatile修饰的变量的读操作,重排到另一个volatile变量的读操作或者写操作之后

实现原理

volatile通过插入一个特定类型的内存屏障禁止指令重排,例如StoreStore屏障,在读之前插入LoadLoad屏障,以及可能的StoreLoad屏障

使用场景

volatile经常结合单例模式用于双重检查(Double-Checked Locking),防止重排导致的数据创建问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class SingletonDemo {
private static volatile SingletonDemo instance;

private SingletonDemo() {}

public static SingletonDemo getInstance() {
if(instance == null) { 第一次检查
synchronized(SingletonDemo) {
// 做double check 防止多个线程并发都进入这里
if (instance == null) {
instance = new SingletonDemo();
}
}
}
return instance;
}
}

3. 不保证原子性

说白了就是不保证操作失败会回滚,例如修饰的变量进行自增 i++,这里有3个操作 ,读 i, 将i+1, 新值回写,多线程下,某个操作被打断都会造成数据错误
解决办法就是加锁

总结

  1. volatile 修饰变量,提供可见性和禁止指令重排的特点
  2. 可见性保证线程对volatile修饰的变量的读写操作直接是操作主存
  3. volatile不保证原子性
  4. volatile禁止编译器和cpu对volatile修饰的变量相关的操作指令进行重排
  5. volatile通常用于状态标识和双重检查的场景

如果您喜欢此博客或发现它对您有用,则欢迎对此发表评论。 也欢迎您共享此博客,以便更多人可以参与。 如果博客中使用的图像侵犯了您的版权,请与作者联系以将其删除。 谢谢 !