多线程进阶--volatile关键字解析
来源:互联网 发布:数据直报系统 编辑:程序博客网 时间:2024/06/18 17:46
What
volatile关键字修饰变量(基本类型,引用类型),在运行期可以保证修饰的变量的内存可见性和禁止指令重排序;也被称为轻量级synchronized,在能够保证无并发问题产生的场景下使用volatile比使用synchronized的性能更好.注意volatile关键字无法保证程序的原子性,如果要保证原子性,可以直接换synchronized或者配合CAS保证;
How
[volatile 变量名],在多线程环境下不能使用 [volatile 变量++]这种编码,因为该字段不保证原子性;比较经典的应用场景为:修饰作为状态开关的布尔值 volatile boolean flag; //线程一 while(!flag){ dosomething... } //线程二 flag=true; 以上代码如果没有volatile修饰的话,有可能造成程序的while死循环 具体代码:
public class VolatileDemo { public static volatile boolean isStopA = false; public static boolean isStopB = false; public static void main(String[] args) throws InterruptedException { Thread a = new Thread(new Runnable() { public void run() { while(!isStopA){} } },"Thread-A"); Thread b = new Thread(new Runnable() { public void run() { while(!isStopB){ //System.out.print(""); ---代码1 } } },"Thread-B"); a.start(); b.start(); TimeUnit.MILLISECONDS.sleep(100); isStopA = true;----代码2 isStopB = true;----代码3 TimeUnit.MILLISECONDS.sleep(100); System.out.println("Thread-A isAlive:"+a.isAlive()); System.out.println("Thread-B isAlive:"+b.isAlive()); } }
运行结果:(题主用了公司电脑和家里电脑做以上代码的测试两个结果不一致,其中一台b线程能够跳出死循环,另一台则不能,jdk1.6)Thread-A isAlive:falseThread-B isAlive:true但是如果把代码1的注释放开,线程b就能够跳出死循环,有两种可能: 1为main线程更改的isStopB变量只是在工作内存更改了,没有写进主内存; 2为b线程读取的isStopB变量一直是b线程的工作内存中的值,当main线程变量值变更时,b线程无法感知isStopB的变化因为有了代码1的特例调试,以及将代码2和代码3换了位置时(volatile的禁止重排序规则使isStopA更改之后isStopB的更改也对所有处理器可见),还是有可能出现死循环,题主觉得应该是b线程的无法感知主内存中该值的变化
Why
—-内存可见性分析start
jvm在开启多线程时,即调用start方法时,即表示一个线程的开始,jvm会为这个线程分配一个工作内存,该线程在运行过程中,该线程的所有变量的操作(读取,赋值)都必须在工作内存中进行,若要使用某个全局变量或静态变量,则需要工作内存向jvm的主内存获取该值,并将获取到的变量值拷贝一份到工作内存中,在线程对某个变量进行了写操作时,会先写进该线程的工作内存,然后工作内存在同步到主内存中去.可见,每个线程只能访问自己的工作内存,不能访问其他线程的工作内存。即对于共享变量来说,多个线程的共享变量只能通过主内存才能进行“交换”。我们在分析volatile变量和普通变量的区别之前先看下JMM的协议内容:(一下内容摘自深入理解Java虚拟机) lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占的状态 unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定 read(读取):作用主内存的变量,它把一个变量的值从主内存中传输到线程的工作内存中,以便随后的工作内存使用 load(载入):作用工作内存的变量,它把read操作从主内存得到的变量值放入工作内存的变量副本中 use(使用):作用于工作内存的变量,它把工作内存中的一个变量的值传递个执行引擎,每当虚拟机遇到一个需要使用到该变量的的值的字节码指令时会执行这个操作 assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作 store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送给主内存,以便随后的write操作使用 write(写入):作用主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中那么使用volatile的时候与不使用volatile的时候到底有哪些区别呢? 一个普通共享变量被某个A线程read-load之后肯定会只用这个变量即use,若另一个B线程在A线程load之后对这个共享变量进行了assign-store-write连续操作(有可能assign之后不会马上进行store和write),这时A线程中的变量值肯定是旧的值了。现在再来看看JMM对volatile多了哪些特殊性: 1.volatile变量的use动作必须和read-load同时出现;(保证了线程只要要用到volatile变量时都是从主内存中拿到的最新的值) 2.volatile变量的assign动作必须和store-write同时出现;(保证了只要某个线程对volatile变量的更改都会立即刷新到主内存) 以上就能解释为何volatile能保证内存可见性了,那么它能取代synchronized吗?答案是不行的,volatile修饰的不能保证原子性操作,我们就拿比较经典的 volatile int a++ 来说有多个线程执行a++操作,结合前文我们知道这个a++是把read-load-use-assign-store-write都执行完了,为啥还不能保证原子性呢,在jvm编译后的文件可看出use动作和assign动作执行期间,cpu还有其他指令需要执行,即这一连串的动作其实并不是连续的,即使use-assign中间没有其他指令,即直接进行assign赋值操作也会出现并发的问题
public static volatile int inc = 0; public static CountDownLatch cdl = new CountDownLatch(1); @Test public void test1() throws InterruptedException{ System.out.println(inc); for(int i = 0;i<20;i++){ new Thread(new tt(i)).start(); } cdl.countDown(); while(Thread.activeCount()>2){ Thread.yield(); } System.out.println(inc); } static class tt implements Runnable{ private int temp; public tt(int temp) { this.temp = temp; } @Override public void run() { try { cdl.await(); } catch (InterruptedException e) { e.printStackTrace(); } inc = temp; } }
代码中使用了一个同步计数器,便于开启的线程同时执行,执行的结果inc 的值是随机的,可证明单纯的assign-store-write操作也不是线程安全的;另比较经典的反例a++在网上很多大牛的博客中都有提到,这里就不贴代码分析了
—-内存可见性分析end
—-禁止指令重排序分析start
指令重排序是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理。但并不是说指令任意重拍,CPU需要能正确处理指令依赖情况以保障程序能够得到正确的结果。
public class MemoryModel { private int count; private boolean stop; public void initCountAndStop() { count = 1; stop = false; } public void doLoop() { while(!stop) { count++; } } public void printResult() { System.out.println(count); System.out.println(stop); }}
上面这段代码执行时我们可能认为count = 1会在stop = false前执行完成,这在上面的CPU执行图中显示的理想状态下是正确的,但是要考虑上寄存器、缓存缓冲的时候就不正确了, 例如stop本身在缓存中但是count不在,则可能stop更新后再count的write buffer写回之前刷新到了内存。 另外CPU、编译器(对于Java一般指JIT)都可能会修改指令执行顺序,例如上述代码中count = 1和stop = false两者并没有依赖关系,所以CPU、编译器都有可能修改这两者的顺序,而在单线程执行的程序看来结果是一样的,这也是CPU、编译器要保证的as-if-serial(不管如何修改执行顺序,单线程的执行结果不变)。由于很大部分程序执行都是单线程的,所以这样的优化是可以接受并且带来了较大的性能提升。但是在多线程的情况下,如果没有进行必要的同步操作则可能会出现令人意想不到的结果。例如在线程T1执行完initCountAndStop方法后,线程T2执行printResult,得到的可能是0, false, 可能是1, false, 也可能是0, true。如果线程T1先执行doLoop(),线程T2一秒后执行initCountAndStop, 则T1可能会跳出循环、也可能由于编译器的优化永远无法看到stop的修改。 由于上述这些多线程情况下的各种问题,多线程中的程序顺序已经不是底层机制中的执行顺序和结果,编程语言需要给开发者一种保证,这个保证简单来说就是一个线程的修改何时对其他线程可见,因此Java语言提出了JavaMemoryModel即Java内存模型。
以下为JMM针对编译器制定的volatile重排序规则表
为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
JMM基于保守策略的JMM内存屏障插入策略: 1.在每个volatile写操作的前面插入一个StoreStore屏障 2.在每个volatile写操作的后面插入一个SotreLoad屏障 3.在每个volatile读操作的后面插入一个LoadLoad屏障 4.在每个volatile读操作的后面插入一个LoadStore屏障
上图的StoreStore屏障可以保证在volatile写之前,其前面的所有普通写操作已经对任意处理器可见了
因为StoreStore屏障将保障上面所有的普通写在volatile写之前刷新到主内存
另附happens-before规则(不在规则中的代码jvm会对其进行重排序)
1. 程序顺序规则: 如果程序中操作A在操作B之前,那么同一个线程中操作A将在操作B之前进行2. 监视器锁规则: 在监视器锁上的锁操作必须在同一个监视器锁上的加锁操作之前执行3. volatile变量规则: volatile变量的写入操作必须在该变量的读操作之前执行4. 线程启动规则: 在线程上对Thread.start的调用必须在该线程中执行任何操作之前执行5. 线程结束规则: 线程中的任何操作都必须在其他线程检测到该线程已经结束之前执行6. 中断规则: 当一个线程在另一个线程上调用interrupt时,必须在被中断线程检测到interrupt之前执行7. 传递性: 如果操作A在操作B之前执行,并且操作B在操作C之前执行,那么操作A在操作C之前执行。
—-禁止指令重排序分析end
参考资料
深入理解Java虚拟机
Java并发编程的艺术
以上内容为题主个人整合学习资料加上个人理解,转载请注明出处
阅读全文
1 0
- 多线程进阶--volatile关键字解析
- Java多线程探究-关键字volatile解析
- Java多线程并发编程:volatile关键字解析
- JAVA多线程技术学习笔记——volatile关键字解析
- Java多线程 -- volatile关键字
- java多线程--volatile关键字
- 多线程之volatile关键字
- Java 多线程:volatile关键字
- volatile 多线程同步关键字
- 多线程-关键字Volatile
- 多线程之volatile关键字
- 多线程---volatile关键字
- java 多线程 volatile 关键字
- 多线程中的volatile关键字
- 多线程 volatile关键字
- 【java多线程 关键字】volatile
- 多线程 说说volatile关键字
- 多线程之volatile关键字
- 每日一发Python---Python中的__name__和类
- CSS基础(二)基础样式
- c# 利用WaveOut播放音频流
- Bridge模式。
- 最长公共子序列Lcs
- 多线程进阶--volatile关键字解析
- Inception 初探
- 封装、静态
- Linux 系统管理-进程管理
- 计算机系统的虚拟内存
- table表头固定表体滚动
- 光学标定 (非计算机)
- Sagheer and Nubian Market
- oralce优化