java并发编程实践 3.1 可见性

缘起

《java并发编程实践》 3.1节

分析

【1】中我们提及了synchronized互斥锁的一个重要作用——就是维护原子性. 但是synchronized的作为英语单词的意思是同步,为什么需要同步? 一言以蔽之——为了线程(注意,是线程,不是进程,进程之间是无法通过进程占有的内存进行通信的,因为进程占有的内存是进程私有的, 是无法通信的, 进程通信只能靠管道、信号量、共享内存、tcp/ip协议、socket等)之间的通信.

切记:线程之间的通信靠的就是同步!!!别无二法

为什么会有通(同)信(步)这个话题? 其实在【2】中我们已经谈及了这个话题——因为时钟频率已经无法经济的提高,所以多核处理器架构势在必行. 所以线程成为时序调度的基本单元,所以一根线程知道别的线程在干什么是需要的,但是其实大部分时候每根线程都在自己高效的工作,并不需要知道其他线程在干什么,只在必要的时候做出沟通,线程之间的沟通就是所谓的线程之间的通(同)信(步).

言归正传,synchronized关键字仅仅是为了维护原子性吗? 非也,原子性只是并发中的一个主题,另一个比较subtitle的主题是”内存可见性”. 而synchronized关键字另一个目的就是维护内存可见性.

问一个看上去比较愚蠢的问题

1
2
3
假设一根线程为变量avariable做出如下赋值语句
int avariable = 3;
那么什么时候别的线程能看到(或者说感知到)这个avariable变量的值已经变成3了?

这个听上去愚蠢的问题,如果少了同步,则少了JMM能做出的保证(【2】),则很多因素都将导致线程无法立即——甚至永远看到另一根线程操作产生的结果。这些因素很多, 不完全列举如下

  1. 编译器级别的指令重排序
  2. 编译器编译出来的程序可能指示CPU把变量存储在寄存器中而不是主内存中.
  3. 处理器级别的指令重排序(乱序或者并行执行指令)
  4. 缓存的存在会改变写入提交到进程主内存中变量的次序.
  5. 存储在处理器本地缓存中的值,对于其他处理器并不可见.

…….

上面N多因素都会妨碍到另一根线程看到一个变量的最新值.

但是线程之间的必要的同步就是需要知道一个变量的最新的值!!!

所以我们需要使用JMM提供的同步,也就是按照JMM提供的happens-before规则(或者说JMM做出的保证)来进行同步(对于不满足happens-before规则的两个动作,JMM是不做任何可见性保证的,这个在【2】中已经说过了).

【2】中的happens-before规则就知道了 synchronized关键字是可以被JMM保证可见性的.

所以 synchronized关键字不仅仅可以维护原子性,也可以维护可见性.

但是通过【3】我们知道,本质上synchronized锁是通过底层系统调用实现的,比较重. 所以有没有比较轻量级的实现内存可见性的呢? 有! 就是volatile关键字. 我们在【4】中给出了它的内存语义,并且在【5】中给出了它在JDK并发容器——CopyOnWriteArrayList上的应用.

但是其实volatile关键字最早的应用并不在CopyOnWriteArrayList这种东东上面. 而在long、double这种64位值的读写上. 因为在JVM规范完成的时候,大多主流的处理器架构并不能有效的支持64位数据的读写的原子操作。所以JMM规定除了”没有声明为volatile的64位的变量,如long、double”之外,读写操作都要求是原子操作. 对于非volatile的long和double变量,JMM允许将64位的读写划分为2个32位的操作,所以就知道如果读和写发生在不同线程的话,读取到的非volatile的long可能就是高32位是一个变量的,低32位却是另一个long变量的. 所以 对于long、double型的变量如果要在线程之间读写共享的话,就要加volatile或者用synchronized锁保护起来.

根据【4】(搜索”增强了volatile的内存语义”),我们知道了volatile关键字其实是一种轻量的同步机制——因为它仅仅需要加内存屏障(所以对于当今大多数处理器架构而言,读取volatile变量的开销只比读取非volatile变量的开销略高而已),而不需要底层加锁这种系统调用(锁会导致线程执行上下文的切换的,比较耗性能).

那么是不是volatile能代替synchronized呢? 不是,别忘了,synchronized还有一枚大杀器——维护原子性. volatile关键字可没有这个功能哦~ 类似于++ 这种自增操作 volatile关键字照样无法维护该操作的原子性的。也就是volatile不做原子性的保证.

总结一下

volatile只保证可见性,synchronized不仅仅保证可见性,还保证原子性

所以有人会说 java.util.concurrent.atomic包下的诸如AtomicInteger是更好的”volatile变量”. 因为它也不加锁(通过CAS原子操作实现无锁,在硬件层面,大部分的现代处理器都已经支持原子化的CAS指令)也维护了原子性、可见性(因为比如AtomicInteger的value属性就是volatile的). 关于这种原子变量,后面我会继续写文章说明的

总结一下,什么情况下才能使用volatile, 只有同时满足下面3个条件的时候才能使用

  1. 写入变量不依赖变量的当前值(例如++自增操作),除非你能保证只有单一的线程修改当前值
  2. 变量不能与其他状态变量之间有不变约束
  3. 访问此变量的时候,不需要加锁

参考

【1】https://yfsyfs.github.io/2019/06/22/java%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B%E5%AE%9E%E8%B7%B5-2-3-%E9%94%81/#more

【2】https://yfsyfs.github.io/2019/06/24/java-%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B%EF%BC%88JMM%EF%BC%89-%E4%B9%8B%E6%88%91%E4%B9%8B%E8%A7%81/

【3】https://yfsyfs.github.io/2019/06/22/java%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B%E5%AE%9E%E8%B7%B5-2-3-%E9%94%81/

【4】https://yfsyfs.github.io/2019/06/24/volatile%E5%85%B3%E9%94%AE%E5%AD%97%E7%9A%84%E5%86%85%E5%AD%98%E8%AF%AD%E4%B9%89/

【5】https://yfsyfs.github.io/2019/06/24/CopyOnWriteArrayList-%E6%BA%90%E7%A0%81%E8%A7%A3%E8%AF%BB/