最近一直在看多线程的一些知识,看了一些书和一些博客,收获还是挺多的,最近看了《java并发编程的艺术》这本书感觉收获很大也推荐给各位,同时也结合以前看的博客就好好的总结一下自己所学的东西吧,有不足的地方欢迎各位指正,这篇文章主要是讲volatile关键字的知识。
关键的地方就来了,因为每个线程然都是在自己的内存里进行操作,然而每个线程的工作内存之间都是相互不可见的,所以对共享变量的修改并不会马上被其他线程看到,所以就会造成多个线程操作同一个数据但是最后结果并不是我们期望的结果。当线程1去首先从主存中加载一个volatile变量到自己的工作内,然后对这个volatile变量进行写操作,写入操作结束之后,volatile变量的最新值会立马刷新到主内存,同时其他线程中的这个volatile变量会立马失效,会被强迫从主内存中重新读取volatile变量的最新值,这就是volatile变量的可见性实现的过程。同时这也可以看做是一个线程和其他线程通信的一个过程。
简单解释下指令重排序,重排序指的是编译器和处理器为了优化程序的性能会对指令序列进行重新排序的一种手段。在单线程的程序里,指令的重排序会保证执行结果的正确性,但是在多线程中指令的重排序对程序的执行结果的正确性就得不到保障(指令重排序的一些规则各位可以去查阅一下,这里不赘述)。volatile变量防止指令重排序请先看下面的内存屏障介绍。(其实synchronized也对指令的重排序有一些限制,在后面我会写一篇讲解synchronized的博文会详细讲解一下,欢迎各位阅读)
JMM把内存屏障分为四类(摘自Java并发编程艺术)
屏障类型 | 指令示例 | 说明 |
---|---|---|
LoadLoad Barriers | Loadl; LoadLoad; Load2 | 确保Loadl数据的装载先于Load2及所有后续装载指令 |
StoreStore Barriers | Storel; StoreStore; Store2 | 确保Store1数据对其他处理器可见(刷新到内存)先于Store2及所有后续存储指令的存储 |
LoadStore Barriers | Loadl; LoadStore; Store2 | 确保Loadl数据装载先于Store2及所有后续的存储指令刷新到内存 |
StoreLoad Barriers | Storel; StoreLoad; Load2 | 确保Storel数据对其他处理器变得可见(指刷新到内存)先于Load2及所有后续装载指令的装载。StoreLoad Barriers会使该屏障之前的所有内存访问指令(存储和装载指令)完成之后,才执行该屏障之后的内存访问指令 |
看完这个相信各位对volatile防止指令重排序就有一个比较清楚的认识了,解释一下上面的四条策略,
很多博客中基本上都说了volatile变量一般运用于不依附当前值的操作,比如自增,我的理解是这样的,如果volatile变量进行依附当前操作的值的运算,那么就会涉及到读volatile变量和写volatile变量这两个操作,volatile变量的读操作(从主内存读取到线程的工作内存)和写操作(将变量写到主内存中去)时,这两个组合起来的操作就是一个非原子性的操作,所以这种情况下使用volatile关键字就不合适,对于基本变量的一些非原子性操作(如自增)可以考虑使用java.util.concurrent.atomic包下的一些类,或者使用锁来进行。volatile变量一般用作比如一个标志变量这种单个读写的操作。 附上个人觉得volatile变量应用的一个讲得比较好的博客(https://www.ibm.com/developerworks/cn/java/j-jtp06197.html) 同时各位也可以去看一下另外一篇讲解volatile的博文,同样比较棒(http://kwsir.cn/2017/10/12/Java%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B-volatile%E5%8F%AF%E8%A7%81%E6%80%A7%E7%9A%84%E4%BB%8B%E7%BB%8D/)。
鉴于本人水平有限,所以如果文章中有不对的地方,十分欢迎各位在评论留言指点,或者发送到本人的qq邮箱549005114@qq.com通知一下本人。谢谢大家。