信息发布→ 登录 注册 退出

详解Java并发编程基础之volatile

发布时间:2026-01-11

点击量:
目录
  • 一、volatile的定义和实现原理
    • 1、Java并发模型采用的方式
    • 2、volatile的定义
    • 3、volatile的底层实现原理
  • 二、volatile的内存语义
    • 1、volatile的特性
    • 2、volatile写-读建立的happens-before关系
    • 3、volatile的写/读内存语义
  • 三、volatile内存语义的实现
    • 1、volatile重排序规则
    • 2、内存屏障  
    • 3、内存屏障示例
  • 四、volatile与死循环问题
    • 五、volatile对于复合操作非原子性问题

      一、volatile的定义和实现原理

      1、Java并发模型采用的方式

      a)线程通信的机制主要有两种:共享内存和消息传递。

      ①共享内存:线程之间共享程序的公共状态,通过写-读共享内存中的公共状态来进行隐式通信;

      ②消息传递:线程之间没有公共状态,线程之间 必须通过发送消息来显式通信。

      b)同步:用于控制不同线程之间操作发生相对顺序。在

      共享内存模型中,同步是显式的进行的,需要显示的指定某个方法或者代码块在线程执行期间互斥进行。

      消息传递模型中,由于消息的发送必定在消息的接受之前,所以同步是隐式的进行的。

      c)Java并发采用的是共享内存模型,线程之间通信总是隐式的进行,而且这个通信是对程序员透明的。那么我们需要了解的是这个隐式通信的底层工作机制。

      2、volatile的定义

      Java编程语言中允许线程访问共享变量,为了确保共享变量能够被准确和一致性的更新,线程应该确保通过排它锁单独获得这个变量。

      3、volatile的底层实现原理

      a)在编写多线程程序中,使用volatile修饰的共享变量在进行写操作的时候,编译器生成的汇编代码中会多出一条lock指令,这条lock指令的作用:

      • ①将当前处理器缓存行中的数据写回到系统内存
      • ②这个写回内存的操作会使得其他CPU里缓存了该内存地址的数据无效

      b)参考下面的这张图理解

      二、volatile的内存语义

      1、volatile的特性

      a)首先我们来看对单个变量的读/写的实现(单个变量的情况可以看做是对同一个锁对这个变量的读/写进行了同步),看下面的例子

      package cn.jvm.test;
      
      public class TestVolatile1 {
      
          volatile long var1 = 0L;
      
          public void set(long l) {
              // TODO Auto-generated method stub
              var1 = l;
          }
      
          public void getAndIncrement() {
              // TODO Auto-generated method stub
              var1 ++; //注意++操作
          }
      
          public long get() {
              return var1;
          }
      }

      上面的set和get操作在语义上和使用synchronized修饰后一样,即下面的这种写法

      package cn.jvm.test;
      
      public class TestVolatile1 {
      
          volatile long var1 = 0L;
      
          public synchronized void set(long l) {
              // TODO Auto-generated method stub
              var1 = l;
          }
      
          public synchronized long get() {
              return var1;
          }
      }

      b)但是在上面的用例中,我们使用的var1++操作,整体上没有原子性,所以如果使用多线程方粉getAndIncrement方法的话,会导致读出的数据和主存中不一致的情况。

      c)volatile变量的特性

      ①可见性:对一个volatile变量的读操作,总是能够看到对这个volatile变量最后的写入

      ②原子性:对任意单个volatile变量的读写具有原子性,但是对于volatile变量的复合型操作并不具备原子性

      2、volatile写-读建立的happens-before关系

      a)看下面的代码实例

      package cn.jvm.test;
      
      public class TestVolatile2 {
      
          int a = 0;
          volatile boolean flag = false;
      
          public void writer() {
              a = 1;
              flag = true;
          }
      
          public void reader() {
              if(flag) {
                  int i =a;
                  //...其他操作
              }
          }
      }

      b)在上面的程序中,假设线程A执行write方法,线程B执行reader方法,根据happens-before规则有下面的关系:

      程序次序规则:①happens-before②; ③happens-before④

      volatile规则:②happens-before③

      传递性规则:①happens-before④

      所以可以得到下面的这个状态图

      3、volatile的写/读内存语义

      a)下面是volatile的写/读内存语义

      ①当写一个volatile变量时候,JMM会将线程对应的本地内存中的共享变量值刷新到主内存中

      ②当读一个volatile变量的时候,JMM会将线程对应的本地内存置为无效,然后从主内存中读取共享变量

      b)还是参照上面的程序示例,参考视图的模型来进行说明

      ①写内存语义的示意图:假设线程A执行writer方法,线程B执行reader方法,初始状况下线程A和B中的变量都是初始状态

      ②写内存语义的示意图:

      三、volatile内存语义的实现

      我们上面说到的基本上从宏观上而言都是说明了volatile保证内存可见性问题,volatile的另一个语义就是禁止指令重排序的优化。下面说一下volatile禁止指令重排序的实现细节

      1、volatile重排序规则

      ①当第二个操作是volatile写的时候,不管第一个操作是什么,都不能进行指令重排序。这个规则确保volatile写之前的操作都不会被重排序到volatile写之后。也是为了保证volatile写对其他线程可见

      ②当第一个操作为volatile读的时候,不管第二个操作是什么,都不能进行重排序。确保volatile读之后的操作不会被重排序到volatile读之前

      ③当第一个操作是volatile写,第二个操作是volatile读的时候,不能进行重排序

      如下所示,上面的是下表中的总结。

      2、内存屏障  

      编译器在生成字节码的时候,会在指令序列中插入内存屏障来禁止对特定类型的处理器重排序。下面是集中策略,后面会说明这几种情况

      ①在每个volatile写操作之前插入StoreStore屏障

      ②在每个volatile写操作之后插入StoreLoad屏障

      ③在每个volatile读操作之后插入LoadLoad屏障

      ④在每个volatile读操作之后插入LoadStore屏障

      3、内存屏障示例

      a)volatile写插入内存屏障之后的指令序列图

      b)volatile读插入内存屏障后的指令序列图

      四、volatile与死循环问题

      1、先看下面的示例代码,观察运行结果,当共享变量isRunning没有被声明为volatile的时候,main线程会在2秒之后将共享变量isRunning置为false并且输出修改信息,这样新建的线程应该结束运行,但是实际上并没有,控制台中会一直保持运行的状态,并且不会打印线程结束执行;如下所示

      package cn.jvm.test;
      
      class ThreadDemo extends Thread {
          private  boolean isRunning = true;
          @Override
          public void run() {
              System.out.println(Thread.currentThread().getName() + " 开始执行");
              while(isRunning) {
      
              }
              System.out.println(Thread.currentThread().getName() + " 结束执行");
          }
          public boolean isRunning() {
              return isRunning;
          }
          public void SetIsRunning(boolean isRunning) {
              this.isRunning = isRunning;
          }
      }
      
      public class TestVolatile4 {
          public static void main(String[] args) {
              ThreadDemo td = new ThreadDemo();
              td.start();
              try {
                  Thread.sleep(2000);
                  td.SetIsRunning(false);
                  System.out.println(Thread.currentThread().getName() + " 线程将共享变量值修改为false");
              } catch (Exception e) {
                  // TODO: handle exception
                  e.printStackTrace();
              }
          }
      }

      2、分析出现上面结果的原因

      在启动线程ThreadDemo之后,变量isRunning被存在公共堆栈以及线程的私有堆栈中,后//续中线程一直在私有堆栈中取出isRunning的值,虽然main线程执行SetIsRunning方法修改了isRunning的值,但是这个值并没有被Thread-//0线程所知,就像上面说的Thread-0取得值一直都是私有堆栈中的,所以不会知道isRunning被修改,也就不会退出循环

      3、按照上面的原因分析一下执行的时候的工作内存和主内存的情况,按照下面的分析我们很容易得出结论

      上面的问题就是因为工作内存(私有堆栈)和主内存(公共堆栈)中的值不同步。而按照我们上面说到的volatile使得单个变量保证线程可见性,就可以对程序修改保证共享变量在main线程中的修改对Thread-0线程可见(结合volatile的实现原理)

      4、修改之后的结果

      package cn.jvm.test;
      
      class ThreadDemo extends Thread {
          private volatile boolean isRunning = true;
          @Override
          public void run() {
              System.out.println(Thread.currentThread().getName() + " 开始执行");
              while(isRunning) {
                  
              }
              System.out.println(Thread.currentThread().getName() + " 结束执行");
          }
          public boolean isRunning() {
              return isRunning;
          }
          public void SetIsRunning(boolean isRunning) {
              this.isRunning = isRunning;
          }
      }
      
      public class TestVolatile4 {
          public static void main(String[] args) {
              ThreadDemo td = new ThreadDemo();
              td.start();
              try {
                  Thread.sleep(2000);
                  td.SetIsRunning(false);
                  System.out.println(Thread.currentThread().getName() + " 线程将共享变量值修改为false");
              } catch (Exception e) {
                  // TODO: handle exception
                  e.printStackTrace();
              }
          }
      }

      五、volatile对于复合操作非原子性问题

      1、volatile能保证对单个变量在多线程之间的可见性问题,但是对于单个变量的复合操作不能保证原子性,如下代码示例,运行结果为

      当然这个结果是随机的,但是不能保证运行结果是100000

      在没有使用同步操作之前,虽然count变量是volatile的,但是由于count++操作是个复合操作

      ①从内存中取出count的值

      ②计算count的值

      ③将count的值写到内存中

      这个复合操作由于volatile不能保证原子性,所以就会出现错误

      package cn.jvm.test;
      
      import java.util.ArrayList;
      import java.util.List;
      
      public class TestVolatile5 {
          volatile int count = 0;
          /*synchronized*/ void m(){
              for(int i = 0; i < 10000; i++){
                  count++;
              }
          }
      
          public static void main(String[] args) {
              final TestVolatile5 t = new TestVolatile5();
              List<Thread> threads = new ArrayList<>();
              for(int i = 0; i < 10; i++){
                  threads.add(new Thread(new Runnable() {
                      @Override
                      public void run() {
                          t.m();
                      }
                  }));
              }
              for(Thread thread : threads){
                  thread.start();
              }
              for(Thread thread : threads){
                  try {
                      thread.join();
                  } catch (InterruptedException e) {
                      // TODO Auto-generated catch block
                      e.printStackTrace();
                  }
              }
              System.out.println(t.count);
          }
      }

      2、下面按照JVM的内存工作来分析一下,即当前一个线程在计算count变量的时候,另一个线程已经修改了count变量的值,这样就必然会出现错误。所以对于这种复合操作就需要使用原子类或者使用synchronized来保证原子性(保证同步)

      3、修改后的synchronized和使用原子类如下所示

      package cn.jvm.test;
       
       import java.util.ArrayList;
       import java.util.List;
       
       public class TestVolatile5 {
           int count = 0;
           synchronized void m(){
               for(int i = 0; i < 10000; i++){
                   count++;
               }
           }
       
           public static void main(String[] args) {
               final TestVolatile5 t = new TestVolatile5();
               List<Thread> threads = new ArrayList<>();
               for(int i = 0; i < 10; i++){
                   threads.add(new Thread(new Runnable() {
                       @Override
                       public void run() {
                           t.m();
                       }
                   }));
               }
               for(Thread thread : threads){
                   thread.start();
               }
               for(Thread thread : threads){
                   try {
                       thread.join();
                   } catch (InterruptedException e) {
                       // TODO Auto-generated catch block
                       e.printStackTrace();
                   }
               }
               System.out.println(t.count);
           }
       }
      package cn.jvm.test;
       
       import java.util.ArrayList;
       import java.util.List;
       import java.util.concurrent.atomic.AtomicInteger;
       
       public class TestVolatile5 {
           AtomicInteger count = new AtomicInteger(0);
           void m(){
               for(int i = 0; i < 10000; i++){
                   count.getAndIncrement();
               }
           }
       
           public static void main(String[] args) {
               final TestVolatile5 t = new TestVolatile5();
               List<Thread> threads = new ArrayList<>();
               for(int i = 0; i < 10; i++){
                   threads.add(new Thread(new Runnable() {
                       @Override
                       public void run() {
                           t.m();
                       }
                   }));
               }
               for(Thread thread : threads){
                   thread.start();
               }
               for(Thread thread : threads){
                   try {
                       thread.join();
                   } catch (InterruptedException e) {
                       // TODO Auto-generated catch block
                       e.printStackTrace();
                   }
               }
               System.out.println(t.count);
           }
       }

      以上就是详解Java并发编程基础之volatile的详细内容,更多关于Java 并发编程 volatile的资料请关注其它相关文章!

      在线客服
      服务热线

      服务热线

      4008888355

      微信咨询
      二维码
      返回顶部
      ×二维码

      截屏,微信识别二维码

      打开微信

      微信号已复制,请打开微信添加咨询详情!