只会使用,不明白原理,就不能灵活运用,深刻理解这几个关键字,对于并发编程来说很有帮助。
可见性是指当一个线程修改了共享变量的值,其它线程能够适时得知这个修改。
实现可见性:禁用工作内存和 Happens-Before 规则的前三条
由 JMM 可知一般的变量写是先写到工作内存,然后由 buffer 刷到主存中的。volatile 标记的变量禁用了工作内存,即直接写到主存中。其他线程读取该变量时,直接从主内存中读取。
Happens-Before 规则可参考:
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 变量自身具有下列特性:
final 修饰变量、修饰方法、修饰类,都有什么作用就不详细讲解了,讲讲原理。
对于final域,编译器和处理器要遵守两个重排序规则:
详细讲解参考:
Java并发(十九):final实现原理
synchronized 的底层是使用操作系统的 mutex lock 实现的。
JVM基于进入和退出 Monitor
对象来实现方法同步和代码块同步。代码块同步是使用 monitorenter
和 monitorexit
指令实现的,monitorenter 指令是在编译后插入到同步代码块的开始位置,而 monitorexit 是插入到方法结束处和异常处。任何对象都有一个 monitor 与之关联,当且一个 monitor 被持有后,它将处于锁定状态。`
根据虚拟机规范的要求,在执行 monitorenter 指令时,首先要去尝试获取对象的锁,如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1;相应地,在执行 monitorexit 指令时会将锁计数器减1,当计数器被减到 0 时,锁就释放了。如果获取对象锁失败了,那当前线程就要阻塞等待,直到对象锁被另一个线程释放为止。
注意两点:
1、synchronized 同步快对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;
2、同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入。
想要详细了解,下面这篇文章讲德特别棒:
Java synchronized原理总结