请选择 进入手机版 | 继续访问电脑版
零一零零 门户 IT技术 Java并发 查看内容

Java并发编程之深入理解volatile

2020-7-1 15:03| 发布者: x0100| 查看: 691| 评论: 0

摘要: 1. 保证可见性 volatile保证了不同线程对volatile修饰变量进行操作时的可见性。 对一个volatile变量的读,(任意线程)总是能看到对这个volatile变量最后的写入。 一个线程修改volatile变量的值时,该变量的新值会 ...


1. 保证可见性

volatile保证了不同线程对volatile修饰变量进行操作时的可见性。

对一个volatile变量的读,(任意线程)总是能看到对这个volatile变量最后的写入。

  1. 一个线程修改volatile变量的值时,该变量的新值会立即刷新到主内存中,这个新值对其他线程来说是立即可见的。

  2. 一个线程读取volatile变量的值时,该变量在本地内存中缓存无效,需要到主内存中读取。

举例:

中断线程时常采用这种标记办法。

  1. boolean stop = false;// 是否中断线程1标志
  2. //Tread1
  3. new Thread() {
  4. public void run() {
  5. while(!stop) {
  6. doSomething();
  7. }
  8. };
  9. }.start();
  10. //Tread2
  11. new Thread() {
  12. public void run() {
  13. stop = true;
  14. };
  15. }.start();

目的: Tread2设置stop=true时,Tread1读取到stop=true,Tread1中断执行。

问题: 虽然大多数时候可以达到中断线程1的目的,但是有可能发生Tread2设置stop=true后,Thread1未被中断的情况,而且这种情况引发的都是比较严重的线上问题,排查难度很大。

问题分析: Tread2设置stop=true时,并未将stop=true刷到主内存,导致Tread1到主内存中读取到的仍然是stop=false,Tread1就会继续执行。也就是有内存可见性问题。

解决: stop变量用volatile修饰。
Tread2设置stop=true时,立即将volatile修饰的变量stop=true刷到主内存;
Tread1读取stop的值时,会到主内存中读取最新的stop值。

2. 保证有序性

volatile关键字能禁止指令重排序,保证了程序会严格按照代码的先后顺序执行,即保证了有序性。

volatile的禁止重排序规则:

  1. 1)当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。
  2. 2)当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
  3. 3)当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。

举例:

  1. boolean inited = false;// 初始化完成标志
  2. //线程1:初始化完成,设置inited=true
  3. new Thread() {
  4. public void run() {
  5. context = loadContext(); //语句1
  6. inited = true; //语句2
  7. };
  8. }.start();
  9. //线程2:每隔1s检查是否完成初始化,初始化完成之后执行doSomething方法
  10. new Thread() {
  11. public void run() {
  12. while(!inited){
  13. Thread.sleep(1000);
  14. }
  15. doSomething(context);
  16. };
  17. }.start();

目的: 线程1初始化配置,初始化完成,设置inited=true。线程2每隔1s检查是否完成初始化,初始化完成之后执行doSomething方法。

问题: 线程1中,语句1和语句2之间不存在数据依赖关系,JMM允许这种重排序。如果在程序执行过程中发生重排序,先执行语句2后执行语句1,会发生什么情况?

当线程1先执行语句2时,配置并未加载,而inited=true设置初始化完成了。线程2执行时,读取到inited=true,直接执行doSomething方法,而此时配置未加载,程序执行就会有问题。

解决: volatile修饰inited变量。
volatile修饰inited,“当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。”,保证线程1中语句1与语句2不能重排序。

3. 不保证原子性

volatile是不能保证原子性的。

原子性是指一个操作是不可中断的,要全部执行完成,要不就都不执行。

举例:

  1. public class VolatileTest {
  2.     public volatile int a = 0;
  3.     public void increase({
  4.         a++;
  5.     }
  6.     public static void main(String[] args{
  7.         final VolatileTest test = new VolatileTest();
  8.         for (int i = 0; i < 10; i++) {
  9.             new Thread() {
  10.                 public void run({
  11.                     for (int j = 0; j < 1000; j++)
  12.                         test.increase();
  13.                 };
  14.             }.start();
  15.         }
  16.         while (Thread.activeCount() > 1) {
  17.             // 保证前面的线程都执行完
  18.             Thread.yield();
  19.         }
  20.         System.out.println(test.a);
  21.     }
  22. }

 

目的: 10个线程将inc加到10000。

结果: 每次运行,得到的结果都小于10000。

原因分析:

  1. 首先来看a++操作,其实包括三个操作:
  2. ①读取a=0;
  3. ②计算0+1=1;
  4. ③将1赋值给a;
  5. 保证a++的原子性,就是保证这三个操作在一个线程没有执行完之前,不能被其他线程执行。

一个可能的执行时序图如下:

关键一步:线程2在读取a的值时,线程1还没有完成a=1的赋值操作,导致线程2读取到当前a=0,所以线程2的计算结果也是a=1。

问题在于没有保证a++操作的原子性。如果保证a++的原子性,线程1在执行完三个操作之前,线程2不能执行a++,那么就可以保证在线程2执行a++时,读取到a=1,从而得到正确的结果。

解决:

  1. synchronized保证原子性,用synchronized修饰increase()方法。

  2. CAS来实现原子性操作,AtomicInteger修饰变量a。

4. volatile实现原理

volatile保证有序性原理

前文介绍过,JMM通过插入内存屏障指令来禁止特定类型的重排序。

java编译器在生成字节码时,在volatile变量操作前后的指令序列中插入内存屏障来禁止特定类型的重排序。

volatile内存屏障插入策略:

  1. 在每个volatile写操作的前面插入一个StoreStore屏障。
  2. 在每个volatile写操作的后面插入一个StoreLoad屏障。
  3. 在每个volatile读操作的后面插入一个LoadLoad屏障。
  4. 在每个volatile读操作的后面插入一个LoadStore屏障。

内存屏障

Store:数据对其他处理器可见(即:刷新到内存中)
Load:让缓存中的数据失效,重新从主内存加载数据

volatile保证可见性原理

volatile内存屏障插入策略中有一条,“在每个volatile写操作的后面插入一个StoreLoad屏障”。

StoreLoad屏障会生成一个Lock前缀的指令,Lock前缀的指令在多核处理器下会引发了两件事:

  1. 1. 将当前处理器缓存行的数据写回到系统内存。
  2. 2. 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。

volatile内存可见的写-读过程:

  1. volatile修饰的变量进行写操作。

  2. 由于编译期间JMM插入一个StoreLoad内存屏障,JVM就会向处理器发送一条Lock前缀的指令。

  3. Lock前缀的指令将该变量所在缓存行的数据写回到主内存中,并使其他处理器中缓存了该变量内存地址的数据失效。

  4. 当其他线程读取volatile修饰的变量时,本地内存中的缓存失效,就会到到主内存中读取最新的数据。

总结

并发编程中,常用volatile修饰变量以保证变量的修改对其他线程可见。

volatile可以保证可见性和有序性,不能保证原子性。

volatile是通过插入内存屏障禁止重排序来保证可见性和有序性的。


鲜花

握手

雷人

路过

鸡蛋

最新评论


QQ|Archiver|手机版|小黑屋| 零一零零 ( 京ICP备20003964号 ) |网站地图

GMT+8, 2020-10-21 04:28 , Processed in 0.035946 second(s), 17 queries .

Powered by Discuz! X3.4

© 2001-2013 Comsenz Inc.

返回顶部