只会使用,不明白原理,就不能灵活运用,深入理解这几个关键字,对于并发编程来讲颇有帮助。html
volatile 的做用有一下两点:编程
可见性是指当一个线程修改了共享变量的值,其它线程可以适时得知这个修改。segmentfault
致使线程可见性问题有两个缘由:多线程
实现可见性: 禁用工做内存和 Happens-Before 规则的前三条并发
由 JMM 可知通常的变量写是先写到工做内存,而后由 buffer 刷到主存中的。volatile 标记的变量禁用了工做内存,即直接写到主存中。其余线程读取该变量时,直接从主内存中读取。app
Happens-Before 规则的前三条:函数
Happens-Before 规则可参考:post
禁止重排序的实现其实也依赖了 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 内存屏障插入策略:
所谓的保守策略即保证在任何处理器上都能获得正确的语意,实际上编译器会自动优化以省略某些语意(好比由于 X86 不会对读读、读写、写写重排序,就能够省下这三种屏障)
编译器不会对 volatile 读与 volatile 读后面的任意内存操做(包括对普通变量的读写)重排序,也不会对 volatile 写与 volatile 写前面的任意内存操做重排序
简而言之,volatile 变量自身具备下列特性:
final 修饰变量、修饰方法、修饰类,都有什么做用就不详细讲解了,讲讲原理。
对于final域,编译器和处理器要遵照两个重排序规则:
缘由:编译器会在final域的写以后,插入一个StoreStore屏障
缘由:编译器会在读final域操做的前面插入一个LoadLoad屏障
详细讲解参考:
synchronized 的底层是使用操做系统的 mutex lock 实现的。
synchronized 用的锁是存在 Java 对象头里的。
JVM基于进入和退出 Monitor
对象来实现方法同步和代码块同步。代码块同步是使用 monitorenter
和 monitorexit
指令实现的,monitorenter 指令是在编译后插入到同步代码块的开始位置,而 monitorexit 是插入到方法结束处和异常处。任何对象都有一个 monitor 与之关联,当且一个 monitor 被持有后,它将处于锁定状态。`
根据虚拟机规范的要求,在执行 monitorenter 指令时,首先要去尝试获取对象的锁,若是这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1;相应地,在执行 monitorexit 指令时会将锁计数器减1,当计数器被减到 0 时,锁就释放了。若是获取对象锁失败了,那当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止。
注意两点:
一、synchronized 同步快对同一条线程来讲是可重入的,不会出现本身把本身锁死的问题;
二、同步块在已进入的线程执行完以前,会阻塞后面其余线程的进入。
想要详细了解,下面这篇文章讲德特别棒:
这三个关键字在 JMM 中起着相当重要的做用,JMM 规范的保证依靠这些关键字实现。同时,这几个关键字的熟练使用也很是很是重要,得深入理解。