volatile、synchronized、final原理浅析

1. 前言

只会使用,不明白原理,就不能灵活运用,深入理解这几个关键字,对于并发编程来讲颇有帮助。html

2. volatile

2.1 volatile 的做用

volatile 的做用有一下两点:编程

  • 修改当即可见
  • 禁止指令重排序

可见性是指当一个线程修改了共享变量的值,其它线程可以适时得知这个修改。segmentfault

可见性

致使线程可见性问题有两个缘由:多线程

  1. 线程对变量进行修改未同步到主内存,那么这个线程对改变量的修改就是不可见的。
  2. 重排序。为了提升程序的执行效率,编译器在生成指令序列时和CPU执行指令序列时,都有可能对指令进行重排序。

2.2 原理

实现可见性: 禁用工做内存和 Happens-Before 规则的前三条并发

实现可见性

由 JMM 可知通常的变量写是先写到工做内存,而后由 buffer 刷到主存中的。volatile 标记的变量禁用了工做内存,即直接写到主存中。其余线程读取该变量时,直接从主内存中读取。app

Happens-Before 规则的前三条:函数

  1. 程序顺序规则:一个线程中的每一个操做,happens-before于该线程中的任意后续操做。
  2. 监视器锁规则:对一个锁的解锁,happens-before 于随后对这个锁的加锁。
  3. volatile变量规则:对一个 volatile 域的写,happens-before于任意后续对这个volatile 域的读。

Happens-Before 规则可参考:post

Java内存模型以及happens-before规则优化

禁止重排序的实现其实也依赖了 happen-before 原则。操作系统

JVM底层是经过一个叫作“内存屏障”的东西来完成。内存屏障,也叫作内存栅栏,是一组处理器指令,用于实现对内存操做的顺序限制。

屏障类型 指令示例 说明
LoadLoad Barriers Load1;LoadLoad;Load2 该屏障确保Load1数据的装载先于Load2及其后全部装载指令的的操做
StoreStore Barriers Store1;StoreStore;Store2 该屏障确保Store1马上刷新数据到内存(使其对其余处理器可见)的操做先于Store2及其后全部存储指令的操做
LoadStore Barriers Load1;LoadStore;Store2 确保Load1的数据装载先于Store2及其后全部的存储指令刷新数据到内存的操做
StoreLoad Barriers Store1;StoreLoad;Load2 该屏障确保Store1马上刷新数据到内存的操做先于Load2及其后全部装载装载指令的操做。它会使该屏障以前的全部内存访问指令(存储指令和访问指令)完成以后,才执行该屏障以后的内存访问指令

基于保守策略的 JMM 内存屏障插入策略:

  • 在每一个 volatile 写操做的前面插入一个 StoreStore 屏障。该屏障用来保证在 volatile 写以前,其前面全部的普通写操做已经对任意处理器可见
  • 在每一个 volatile 写操做的后面插入一个 StoreLoad 屏障。避免 volatile 写操做可能与后面可能有的 volatile 写操做重排序
  • 在每一个 volatile 读操做的前面插入一个 LoadLoad 屏障。用来避免将 volatile 读和前面的普通读重排序
  • 在每一个 volatile 读后面插入一个 LoadStore 屏障。避免后面的普通写和 volatile 读重排序

所谓的保守策略即保证在任何处理器上都能获得正确的语意,实际上编译器会自动优化以省略某些语意(好比由于 X86 不会对读读、读写、写写重排序,就能够省下这三种屏障)

编译器不会对 volatile 读与 volatile 读后面的任意内存操做(包括对普通变量的读写)重排序,也不会对 volatile 写与 volatile 写前面的任意内存操做重排序

2.3 总结

简而言之,volatile 变量自身具备下列特性:

  1. 可见性。对一个 volatile 变量的读,总能看到(任意线程)对这个 volatile 变量最后的写入。
  2. 原子性。对任意单个 volatile 变量的读/写具备原子性,但相似于 volatile++ 这种复合操做不具备原子性。

3. final

final 修饰变量、修饰方法、修饰类,都有什么做用就不详细讲解了,讲讲原理。

对于final域,编译器和处理器要遵照两个重排序规则:

  • 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操做之间不能重排序。(先写入final变量,后调用该对象引用)

缘由:编译器会在final域的写以后,插入一个StoreStore屏障

  • 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操做之间不能重排序(先读对象的引用,后读final变量)

缘由:编译器会在读final域操做的前面插入一个LoadLoad屏障

详细讲解参考:

Java并发(十九):final实现原理

4. synchronized

synchronized 的底层是使用操做系统的 mutex lock 实现的。

  • **内存可见性:**同步快的可见性是由“若是对一个变量执行 lock 操做,将会清空工做内存中此变量的值,在执行引擎使用这个变量前须要从新执行 load 或 assign 操做初始化变量的值”、“对一个变量执行 unlock 操做以前,必须先把此变量同步回主内存中(执行 store 和 write 操做)”这两条规则得到的。
  • **操做原子性:**持有同一个锁的两个同步块只能串行地进入

synchronized 用的锁是存在 Java 对象头里的。

JVM基于进入和退出 Monitor 对象来实现方法同步和代码块同步。代码块同步是使用 monitorentermonitorexit 指令实现的,monitorenter 指令是在编译后插入到同步代码块的开始位置,而 monitorexit 是插入到方法结束处和异常处。任何对象都有一个 monitor 与之关联,当且一个 monitor 被持有后,它将处于锁定状态。`

根据虚拟机规范的要求,在执行 monitorenter 指令时,首先要去尝试获取对象的锁,若是这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1;相应地,在执行 monitorexit 指令时会将锁计数器减1,当计数器被减到 0 时,锁就释放了。若是获取对象锁失败了,那当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止。

注意两点:

一、synchronized 同步快对同一条线程来讲是可重入的,不会出现本身把本身锁死的问题;

二、同步块在已进入的线程执行完以前,会阻塞后面其余线程的进入。

想要详细了解,下面这篇文章讲德特别棒:

Java synchronized原理总结

5. 小结&参考资料

小结

这三个关键字在 JMM 中起着相当重要的做用,JMM 规范的保证依靠这些关键字实现。同时,这几个关键字的熟练使用也很是很是重要,得深入理解。

参考资料

Java 并发编程:volatile的使用及其原理

Java并发编程的艺术

从Java多线程可见性谈Happens-Before原则

谈乱序执行和内存屏障

Java final关键字及其内存语义

Final of Java,这一篇差很少了

Java并发(十九):final实现原理

Java synchronized原理总结

相关文章
相关标签/搜索