引言:
在 Java 编程中,
volatile
是一个非常重要的关键字,用于修饰变量。它主要用于确保多个线程之间对共享变量的修改能够被及时地更新到主内存,并且能够保证可见性。然而,需要注意的是,volatile
关键字并不具备原子性,这就意味着在某些情况下,仅仅使用volatile
是不足够的。本文将深入探讨volatile
关键字的使用及其特性。
一、volatile 能保证内存可见性
代码在写⼊ volatile 修饰的变量的时候,
- 改变线程⼯作内存中volatile变量副本的值
- 将改变后的副本的值从⼯作内存刷新到主内存
代码在读取 volatile 修饰的变量的时候,
- 从主内存中读取volatile变量的最新值到线程的⼯作内存中
- 从⼯作内存中读取volatile变量的副本
前⾯我们讨论内存可⻅性时说了, 直接访问⼯作内存(实际是 CPU 的寄存器或者 CPU 的缓存), 速度⾮常快, 但是可能出现数据不⼀致的情况.
加上 volatile , 强制读写内存. 速度是慢了, 但是数据变的更准确了.
代码示例:
- 创建两个线程 t1 和 t2
- t1 中包含⼀个循环, 这个循环以 flag == 0 为循环条件.
- t2 中从键盘读⼊⼀个整数, 并把这个整数赋值给 flag
- 预期当⽤⼾输⼊⾮ 0 的值的时候, t1 线程结束
static class Counter {public int flag = 0;}public static void main(String[] args) {Counter counter = new Counter();Thread t1 = new Thread(() -> {while (counter.flag == 0) {// do nothing}System.out.println("循环结束!");});Thread t2 = new Thread(() -> {Scanner scanner = new Scanner(System.in);System.out.println("输⼊⼀个整数:");counter.flag = scanner.nextInt();});t1.start();t2.start();} // 执⾏效果 // 当⽤⼾输⼊⾮0值时, t1 线程循环不会结束. (这显然是⼀个 bug)
t1 读的是⾃⼰⼯作内存中的内容.当 t2 对 flag 变量进⾏修改, 此时 t1 感知不到 flag 的变化.
如果给 flag 加上 volatile
static class Counter {public volatile int flag = 0;
}
// 执⾏效果
// 当⽤⼾输⼊⾮0值时, t1 线程循环能够⽴即结束.
二、volatile 不保证原子性
虽然 volatile
能够保证内存可见性,但它并不具备原子性。这意味着,当多个线程同时修改一个 volatile
变量时,可能会出现竞态条件,导致最终结果不符合预期。因此,在需要进行原子性操作时,还需要额外的同步机制,比如 synchronized
关键字或者 java.util.concurrent.atomic
包中提供的原子类。
代码示例:
- 给 increase ⽅法去掉 synchronized
- 给 count 加上 volatile 关键字.
static class Counter {volatile public int count = 0;void increase() {count++;}}public static void main(String[] args) throws InterruptedException {final Counter counter = new Counter();Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.increase();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.increase();}});t1.start();t2.start();t1.join();t2.join();System.out.println(counter.count);}
此时可以看到, 最终 count 的值仍然⽆法保证是 100000.
总结:
volatile
关键字在多线程编程中扮演着重要的角色,它能够确保共享变量的可见性,但并不保证原子性。因此,在编写多线程程序时,需要根据具体情况综合考虑是否需要额外的同步措施,以确保程序的正确性和稳定性。