文章目录
- 一、概述
- 二、使用方法
- 三、测试程序
- 3.1 验证可见性的示例
- 3.2 验证指令重排序的示例
一、概述
-
在Java中,volatile 关键字用于修饰变量,其作用是确保多个线程之间对该变量的可见性和禁止指令重排序优化。
-
当一个变量被声明为volatile时,线程在读取和写入该变量时会直接操作主内存中的值,而不会使用线程自己的工作内存。这意味着当一个线程修改了一个volatile变量的值时,其他线程将立即看到这个变化,而不会使用缓存中的旧值。底层原理应该是实现了多CPU缓存一致性协议(如MESI),保证了线程可见性。如下图
-
volatile关键字还可以防止指令重排序优化。在多线程环境下,为了提高执行效率,编译器和处理器可能会对指令进行重排序。然而,这种重排序可能会导致多线程程序出现意想不到的结果。通过使用volatile关键字,可以确保特定操作的执行顺序与程序中的顺序一致,从而避免了指令重排序可能引发的问题。底层原理应该是使用了屏障(如loadfence、storefence原语指令),禁止指令重排序。
-
volatile 关键字只能保证单个变量的原子性操作和可见性,并不能替代synchronized关键字或Lock接口来实现更复杂的操作。如果需要进行复合操作,例如原子性的读取-修改-写入操作,仍然需要使用synchronized关键字或Lock接口来保证线程安全性。如 x = y++; 则需要使用 synchronized 将整个语句加锁。
二、使用方法
-
使用时方法简单,直接在变量定义时添加 volatile 关键字即可,如下
volatile int count1 = 1; private volatile int count2 = 2; volatile boolean flag1 = false; private volatile boolean flag2 = false;
三、测试程序
3.1 验证可见性的示例
-
在下面示例中,Counter 类有一个 count 变量用于计数,如果不使用 volatile 关键字修饰。在 increment 方法中,两个线程分别对 count 进行自增操作。然后在 Main 类的 main 方法中,创建了两个线程并启动它们,每个线程分别对 Counter 对象的 count 执行1000次自增操作。
-
由于没有使用 volatile 关键字,线程在执行自增操作时,会将 count 的值从主内存复制到各自的线程工作内存中,进行自增操作后再将结果写回主内存。这可能导致一个线程对 count 的修改无法被另一个线程立即感知到,从而导致计数不准确。
-
因此,当运行示例时,输出的最终计数结果可能小于2000,因为两个线程之间的自增操作并没有得到正确同步和可见性保证。
-
相反,如果给 count 变量上添加 volatile 关键字修饰符,可以确保线程之间对该变量的读写操作具有可见性和一致性,从而解决问题。
package top.yiqifu.study.p004_thread; public class Test051_VolatileVisible { public static class Counter { // 不使用 volatile 关键字 private int count = 0; // 使用 volatile 关键字 // private volatile int count = 0; public void increment() { count++; } public int getCount() { return count; } } public static void main(String[] args) { Counter counter = new Counter(); Thread thread1 = new Thread(() -> { for (int i = 0; i < 1000; i++) { counter.increment(); } }); Thread thread2 = new Thread(() -> { for (int i = 0; i < 1000; i++) { counter.increment(); } }); thread1.start(); thread2.start(); try { thread1.join(); thread2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("最终结果: " + counter.getCount()); } }
3.2 验证指令重排序的示例
-
在下面示例中,Test052_VolatileReorderingExample 类有一个 writer 方法和一个 reader 方法。
- 在 writer 方法中,首先对变量 x 赋值为1,然后将 flag 设置为 true。
- 在 reader 方法中,如果 flag 的值为 true,则打印变量 x 的值。
-
创建一个测试方法 test,在这个方法中创建了两个线程,一个线程执行 writer 方法,另一个线程执行 reader 方法。然后在 main 方法中,创建一个线程用死循环去执行他。
-
由于变量x和flag没有使用 volatile 关键字,编译器和处理器可能会对指令进行重排序。在不保证顺序性的情况下,可能会发生以下两种重排序情况:
- 写入操作的重排序:编译器和处理器可能会将写操作2(flag = true)重排序到写操作1(x = 1)之前。
- 读取操作的重排序:编译器和处理器可能会将读操作2(int a = x)重排序到读操作1(if (flag))之前。
-
这种重排序可能导致在 reader 方法中打印的变量 a 的值为0,即使在 writer 方法中已经将其设置为1。这是因为在没有足够同步保证的情况下,读操作可能先于写操作执行。
-
要解决这个问题,可以通过在 x 和 flag 变量上添加 volatile 关键字修饰符,可以防止指令重排序,从而避免这种问题。
-
下面是测试程序,我在测试时执行了35万次时出现了指令重排序,出现这个问题的概念不是固定的,您测试时需要耐心等待。
-
Test052_VolatileReorderingExample.java 文件内容
package top.yiqifu.study.p004_thread; public class Test052_VolatileReorderingExample { // 不使用 volatile 关键字 private int x = 0; private boolean flag = false; // // 使用 volatile 关键字 // private volatile int x = 0; // private volatile boolean flag = false; public void writer() { x = 1; // 写操作1 flag = true; // 写操作2 } public void reader() { if (flag) { // 读操作1 int a = x; // 读操作2 if(a == 0) { System.out.println("出现了指令重排序,说明先执行了 flag = true, x = 1 还没有执行"); } } } }
-
测试类 Test052_VolatileOrder.java 内容
package top.yiqifu.study.p004_thread; public class Test052_VolatileOrder { private static void test(){ Test052_VolatileReorderingExample example = new Test052_VolatileReorderingExample(); Thread thread1 = new Thread(() -> { example.writer(); }); Thread thread2 = new Thread(() -> { example.reader(); }); thread1.start(); thread2.start(); try { thread1.join(); thread2.join(); } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] args) { Thread thread = new Thread(()->{ long count = 0; while (true){ test(); Thread.yield(); count++; if(count%10000 == 0){ System.out.println("主线程还活着,已执行"+count+"次"); } } }); thread.start(); try { thread.join(); } catch (InterruptedException e) { e.printStackTrace(); } } }
-