Java 多线程之 volatile(可见性/重排序)

一、概述

  • 在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();
              }
          }
      
      }
      
      
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

QIFU

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值