一、Synchronized 原理是什么
-
在jdk1.6之前synchronized重量级锁,基于Monitor(管程是指管理共享变量以及对共享变量操作的过程,让它们支持并发)机制实现,依赖底层操作系统的Mutex Lock互斥锁实现的
-
由于权限隔离的关系,应用程序去调用系统方法的时候需要切换到内核状态去执行,这样就涉及到了用户态到内核态的切换,这个切换对性能有较大的影响
-
所以在jdk1.6之后呢,synchronized增加了锁升级的机制来平衡数据安全性和性能。synchronized会根据线程竞争的情况,先去尝试在不加重量级锁的情况下去保证线程的安全性,所以引入了偏向锁和轻量级锁的机制
Synchronized 是由 JVM 实现的一种实现互斥同步的一种方式,如果你查看被 Synchronized 修饰过的程序块编译后的字节码,会发现,被 Synchronized 修饰过的程序块,在编译前后被编译器生成了 monitorenter 和 monitorexit 两个字节码指令。
这两个指令是什么意思呢?
- 在虚拟机执行到 monitorenter 指令时,首先要尝试获取对象的锁: 如果这个对象没有锁定,或者当前线程已经拥有了这个对象的锁,把锁的计数器 +1;当执行 monitorexit 指令时将锁计数器 -1;
- 当计数器为 0 时,锁就被释放了。 如果获取对象失败了,那当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止。
锁的状态是如何记录的?
在 对象头
的 Mark Work 有 2bit
来记录锁的状态标位,由于无锁和偏向锁的锁标识都是 01,没办法区分,这里引入 1bit
的偏向锁标识位来记录是否偏向锁
二、偏向锁原理,为什么引入偏向锁
- 大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,如果每次都要竞争锁会增大很多没有必要付出的代价,为了
降低获取锁的代价
,才引入的偏向锁。 - 对于没有锁竞争的场合,偏向锁有很好的优化效果。
2.1 偏向锁如何获取的?
- 在偏向锁状态的时候,对象头的 Mark Word 中记录了当前偏向的
Thread Id
- 首先会先获取 Mark Word 中记录的偏向的Thread Id,判断当前对象是否处于可偏向状态(即当前没有对象获得偏向锁)。
- 如果是可偏向状态,则通过
CAS
操作,把当前线程的ID,写入到 Mark Word 如果成功,表示获得偏向锁成功 - 如果CAS失败则说明当前有其他线程获得了偏向锁,这时候就需要将已获得偏向锁的线程执行
偏向锁撤销
,并升级为轻量级锁 - 偏向锁的撤销需要修改 Mark Word 中的锁状态标志位为无锁,并且需要
等待全局安全点
,即在这个时间点上没有正在执行的字节码(有性能问题)。
当多个线程争夺一个资源时,JVM 会尝试用偏向锁来提高效率。偏向锁的原理是:当一个线程获得锁时,它将会“偏向”到该线程,并在后续竞争时,会优先分配给这个线程,从而避免线程竞争,但如果某个线程竞争竞争到了锁,而该线程未被偏向,则 JVM 会撤销该锁,并重新分配锁
,这会导致性能下降。
三、轻量级锁的获取锁过程
-
首先会判断当前是否为无锁状态,也就是Mark Work中是否偏向的标志位为0
-
如果是的话开始加锁操作,加锁的的过程首先是将Mark Word 拷贝一份到线程栈,紧接着利用CAS操作尝试更新Mark Word 中指向栈中锁记录的指针的指向
-
如果更新成功,则表示成功获取锁,否则直接开始执行锁膨胀(这里轻量级锁没有自旋的操作)
四、重量级锁获取锁的过程
- 在重量级的获取的时候会使用
CAS自旋
的方式获取锁,直到自旋到一定的次数以后开始执行入队操作(_crq队列),入队成功后也会再次尝试是否能够获取锁 - 如果获取失败,才真正的将线程
挂起
,这个反复的CAS自旋的方式去尝试获取锁是为了尽可能的减少用户态
到内核态
的操作
五、锁粗化
- 假设一系列的连续操作都会对同一个对象反复加锁及解锁,甚至加锁操作是出现在循环体中的,即使没有出现线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。
- 如果JVM检测到有一连串零碎的操作都是对同一对象的加锁,将会
扩大加锁同步的范围
(即锁粗化)到整个操作序列的外部。
StringBuffer buffer = new StringBuffer();
/**
* 锁粗化
*/
public void append(){
buffer.append("aaa").append(" bbb").append(" ccc");
}
- 上述代码每次调用
buffer.append
方法都需要加锁和解锁,如果JVM检测到有一连串的对同一个对象加锁和解锁的操作,就会将其合并成一次范围更大的加锁和解锁操作 - 即在第一次append方法时进行加锁,最后一次append方法结束后进行解锁
六、锁消除
锁消除即删除不必要的加锁操作。锁消除是Java虚拟机在JIT编译期间,通过 逃逸分析
,去除不可能存在共享资源竞争的锁
,通过锁消除,可以节省毫无意义的请求锁时间。
逃逸分析(Escape Analysis)
- 逃逸分析,是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。
- 通过逃逸分析,Java Hotspot编译器能够分析出一个新的
对象的引用的使用范围
从而决定是否要将这个对象分配到堆上。逃逸分析的基本行为就是分析对象动态作用域
。
方法逃逸(对象逃出当前方法)
当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中。
线程逃逸((对象逃出当前线程)
这个对象甚至可能被其它线程访问到,例如赋值给类变量或可以在其它线程中访问的实例变量。
使用逃逸分析,编译器可以对代码做如下优化:
-
锁粗化或锁消除
:如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。 -
将堆分配转化为栈分配
:如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。 -
分离对象或标量替换
:有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。
jdk6才开始引入该技术,jdk7开始默认开启逃逸分析。在Java代码运行时,可以通过JVM参数指定是否开启逃逸分析:
-XX:+DoEscapeAnalysis //表示开启逃逸分析 (jdk1.8默认开启)
-XX:-DoEscapeAnalysis //表示关闭逃逸分析。
-XX:+EliminateAllocations //开启标量替换(默认打开)