Java并发编程基础之volatile

  首先简单介绍一下volatile的应用,volatile做为Java多线程中轻量级的同步措施,保证了多线程环境中“共享变量”的可见性。这里的可见性简单而言能够理解为当一个线程修改了一个共享变量的时候,另外的线程可以读到这个修改的值。下面就是volatile的具体定义和实现原理。上一篇Java内存模型html

1、volatile的定义和实现原理

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

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

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

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

  b)同步:用于控制不一样线程之间操做发生相对顺序。在缓存

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

  消息传递模型中,因为消息的发送一定在消息的接受以前,因此同步是隐式的进行的。并发

  c)Java并发采用的是共享内存模型,线程之间通讯老是隐式的进行,并且这个通讯是对程序员透明的。那么咱们须要了解的是这个隐式通讯的底层工做机制。app

二、volatile的定义

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

三、volatile的底层实现原理

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

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

  b)参考下面的这张图理解编程语言

2、volatile的内存语义

一、volatile的特性

  a)首先咱们来看对单个变量的读/写的实现(单个变量的状况能够看作是对同一个锁对这个变量的读/写进行了同步),看下面的例子

 1 package cn.jvm.test;
 2 
 3 public class TestVolatile1 {
 4 
 5     volatile long var1 = 0L;
 6     
 7     public void set(long l) {
 8         // TODO Auto-generated method stub
 9         var1 = l;
10     }
11     
12     public void getAndIncrement() {
13         // TODO Auto-generated method stub
14         var1 ++; //注意++操做
15     }
16     
17     public long get() {
18         return var1;
19     }
20 }

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

 1 package cn.jvm.test;
 2 
 3 public class TestVolatile1 {
 4 
 5     volatile long var1 = 0L;
 6     
 7     public synchronized void set(long l) {
 8         // TODO Auto-generated method stub
 9         var1 = l;
10     }
11     
12     public synchronized long get() {
13         return var1;
14     }
15 }

  b)可是在上面的用例中,咱们使用的var1++操做,总体上没有原子性,因此若是使用多线程方粉getAndIncrement方法的话,会致使读出的数据和主存中不一致的状况。

  c)volatile变量的特性

①可见性:对一个volatile变量的读操做,老是可以看到对这个volatile变量最后的写入
②原子性:对任意单个volatile变量的读写具备原子性,可是对于volatile变量的复合型操做并不具有原子性

二、volatile写-读创建的happens-before关系

  a)看下面的代码实例

 1 package cn.jvm.test;
 2 
 3 public class TestVolatile2 {
 4 
 5     int a = 0;
 6     volatile boolean flag = false;
 7     
 8     public void writer() {
 9         a = 1;
10         flag = true;
11     }
12     
13     public void reader() {
14         if(flag) {
15             int i =a;
16             //...其余操做
17         }
18     }
19 }

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

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

volatile规则:②happens-before③

传递性规则:①happens-before④

  因此能够获得下面的这个状态图

三、volatile的写/读内存语义

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

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

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

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

   ②写内存语义的示意图:

 

3、volatile内存语义的实现

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

一、volatile重排序规则

①当第二个操做是volatile写的时候,无论第一个操做是什么,都不能进行指令重排序。这个规则确保volatile写以前的操做都不会被重排序到volatile写以后。
 也是为了保证volatile写对其余线程可见 ②当第一个操做为volatile读的时候,无论第二个操做是什么,都不能进行重排序。确保volatile读以后的操做不会被重排序到volatile读以前 ③当第一个操做是volatile写,第二个操做是volatile读的时候,不能进行重排序

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

 

二、内存屏障  

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

①在每一个volatile写操做以前插入StoreStore屏障
②在每一个volatile写操做以后插入StoreLoad屏障
③在每一个volatile读操做以后插入LoadLoad屏障
④在每一个volatile读操做以后插入LoadStore屏障

 

 三、内存屏障示例

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

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

4、volatile与死循环问题

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

 1 package cn.jvm.test;
 2 
 3 class ThreadDemo extends Thread {
 4     private  boolean isRunning = true;
 5     @Override
 6     public void run() {
 7         System.out.println(Thread.currentThread().getName() + " 开始执行");
 8         while(isRunning) {
 9             
10         }
11         System.out.println(Thread.currentThread().getName() + " 结束执行");
12     }
13     public boolean isRunning() {
14         return isRunning;
15     }
16     public void SetIsRunning(boolean isRunning) {
17         this.isRunning = isRunning;
18     }
19 }
20 
21 public class TestVolatile4 {
22     public static void main(String[] args) {
23         ThreadDemo td = new ThreadDemo();
24         td.start();
25         try {
26             Thread.sleep(2000);
27             td.SetIsRunning(false);
28             System.out.println(Thread.currentThread().getName() + " 线程将共享变量值修改成false");
29         } catch (Exception e) {
30             // TODO: handle exception
31             e.printStackTrace();
32         }
33     }
34 }

  二、分析出现上面结果的缘由

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

  三、按照上面的缘由分析一下执行的时候的工做内存和主内存的状况,按照下面的分析咱们很容易得出结论

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

  四、修改以后的结果

 1 package cn.jvm.test;
 2 
 3 class ThreadDemo extends Thread {
 4     private volatile boolean isRunning = true;
 5     @Override
 6     public void run() {
 7         System.out.println(Thread.currentThread().getName() + " 开始执行");
 8         while(isRunning) {
 9             
10         }
11         System.out.println(Thread.currentThread().getName() + " 结束执行");
12     }
13     public boolean isRunning() {
14         return isRunning;
15     }
16     public void SetIsRunning(boolean isRunning) {
17         this.isRunning = isRunning;
18     }
19 }
20 
21 public class TestVolatile4 {
22     public static void main(String[] args) {
23         ThreadDemo td = new ThreadDemo();
24         td.start();
25         try {
26             Thread.sleep(2000);
27             td.SetIsRunning(false);
28             System.out.println(Thread.currentThread().getName() + " 线程将共享变量值修改成false");
29         } catch (Exception e) {
30             // TODO: handle exception
31             e.printStackTrace();
32         }
33     }
34 }
将isRunning修改成volatile

 

5、volatile对于复合操做非原子性问题

  一、volatile能保证对单个变量在多线程之间的可见性问题,可是对于单个变量的复合操做不能保证原子性,以下代码示例,运行结果为,固然这个结果是随机的,可是不能保证运行结果是100000

在没有使用同步操做以前,虽然count变量是volatile的,可是因为count++操做是个复合操做
①从内存中取出count的值
②计算count的值
③将count的值写到内存中
这个复合操做因为volatile不能保证原子性,因此就会出现错误
 1 package cn.jvm.test;
 2 
 3 import java.util.ArrayList;
 4 import java.util.List;
 5 
 6 public class TestVolatile5 {
 7     volatile int count = 0;
 8     /*synchronized*/ void m(){
 9         for(int i = 0; i < 10000; i++){
10             count++;
11         }
12     }
13 
14     public static void main(String[] args) {
15         final TestVolatile5 t = new TestVolatile5();
16         List<Thread> threads = new ArrayList<>();
17         for(int i = 0; i < 10; i++){
18             threads.add(new Thread(new Runnable() {
19                 @Override
20                 public void run() {
21                     t.m();
22                 }
23             }));
24         }
25         for(Thread thread : threads){
26             thread.start();
27         }
28         for(Thread thread : threads){
29             try {
30                 thread.join();
31             } catch (InterruptedException e) {
32                 // TODO Auto-generated catch block
33                 e.printStackTrace();
34             }
35         }
36         System.out.println(t.count);
37     }
38 }

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

  三、修改后的synchronized和使用原子类以下所示

 1 package cn.jvm.test;
 2 
 3 import java.util.ArrayList;
 4 import java.util.List;
 5 
 6 public class TestVolatile5 {
 7     int count = 0;
 8     synchronized void m(){
 9         for(int i = 0; i < 10000; i++){
10             count++;
11         }
12     }
13 
14     public static void main(String[] args) {
15         final TestVolatile5 t = new TestVolatile5();
16         List<Thread> threads = new ArrayList<>();
17         for(int i = 0; i < 10; i++){
18             threads.add(new Thread(new Runnable() {
19                 @Override
20                 public void run() {
21                     t.m();
22                 }
23             }));
24         }
25         for(Thread thread : threads){
26             thread.start();
27         }
28         for(Thread thread : threads){
29             try {
30                 thread.join();
31             } catch (InterruptedException e) {
32                 // TODO Auto-generated catch block
33                 e.printStackTrace();
34             }
35         }
36         System.out.println(t.count);
37     }
38 }
使用synchronized

 

 1 package cn.jvm.test;
 2 
 3 import java.util.ArrayList;
 4 import java.util.List;
 5 import java.util.concurrent.atomic.AtomicInteger;
 6 
 7 public class TestVolatile5 {
 8     AtomicInteger count = new AtomicInteger(0);
 9     void m(){
10         for(int i = 0; i < 10000; i++){
11             count.getAndIncrement();
12         }
13     }
14 
15     public static void main(String[] args) {
16         final TestVolatile5 t = new TestVolatile5();
17         List<Thread> threads = new ArrayList<>();
18         for(int i = 0; i < 10; i++){
19             threads.add(new Thread(new Runnable() {
20                 @Override
21                 public void run() {
22                     t.m();
23                 }
24             }));
25         }
26         for(Thread thread : threads){
27             thread.start();
28         }
29         for(Thread thread : threads){
30             try {
31                 thread.join();
32             } catch (InterruptedException e) {
33                 // TODO Auto-generated catch block
34                 e.printStackTrace();
35             }
36         }
37         System.out.println(t.count);
38     }
39 }
使用原子类型

参考自《Java并发编程的艺术》 《Java多线程编程核心技术》

相关文章
相关标签/搜索