目录
1. synchronized在jdk 1.6中的优化
1.1 锁消除
1.2 锁粗化
1.2 锁升级/锁膨胀
1.2.1 锁升级原理
1.2.2 自适应自旋锁
2. synchronized实现原理
3. synchronized和Lock的对比
1. synchronized在jdk 1.6中的优化
在JDK1.5的时候,Doug Lee推出了ReentrantLock,lock的性能远高于synchronized,所以JDK团队就在JDK1.6中,对synchronized做了大量的优化。
1.1 锁消除
在synchronized修饰的代码中,如果不存在操作临界资源的情况,会触发锁消除,你即便写了synchronized,他也不会触发。
public synchronized void method() {
//没有操作临界资源
// 此时这个方法的synchronized你可以认为木有~~了
}
1.2 锁粗化
如果检测到同一个对象执行了连续的加锁和解锁的操作,则会将这一系列操作合并成一个更大的锁,从而提升程序的执行效率。
例如:在一个循环中,频繁的获取和释放做资源,这样带来的消耗很大,锁膨胀就是将锁的范围扩大,避免频繁的竞争和获取锁资源带来不必要的消耗。
public void method() {
for (int i = 0; i < 999999; i++) {
synchronized (对象) {
}
}
//这时上面的代码会触发锁膨胀
synchronized (对象) {
for (int i = 0; i < 999999; i++) {
}
}
}
1.2 锁升级/锁膨胀
ReentrantLock的实现,是先基于乐观锁的CAS尝试获取锁资源,如果拿不到锁资源,才会挂起线程。synchronized在JDK1.6之前,完全就是获取不到锁,立即挂起当前线程,所以synchronized性能比较差。
synchronized就在JDK1.6做了锁升级的优化。
1.2.1 锁升级原理
在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。
- 无锁、匿名偏向:当前对象没有作为锁存在。
- 偏向锁:如果当前锁资源,只有一个线程在频繁的获取和释放,那么这个线程过来,只需要判断,当前指向的线程是否是当前线程。
- 如果是,直接拿着锁资源走。
- 如果当前线程不是我,基于CAS的方式,尝试将偏向锁指向当前线程。
- 如果获取不到,触发锁升级,升级为轻量级锁。(偏向锁状态出现了锁竞争的情况)
- 轻量级锁:会采用自旋锁的方式去频繁的以CAS的形式获取锁资源(采用的是自适应自旋锁)。
- 如果成功获取到,拿着锁资源走。
- 如果自旋了一定次数,没拿到锁资源,锁升级。
- 重量级锁:就是最传统的synchronized方式,拿不到锁资源,就挂起当前线程。(用户态&内核态)
1.2.2 自适应自旋锁
自旋锁优点在于它避免一些线程的挂起和恢复操作,因为挂起线程和恢复线程都需要从用户态转入内核态,这个过程是比较慢的,所以通过自旋的方式可以一定程度上避免线程挂起和恢复所造成的性能开销。
但是,如果长时间自旋还获取不到锁,那么也会造成一定的资源浪费,所以我们通常会给自旋设置一个固定的值来避免一直自旋的性能开销。然而对于 synchronized 关键字来说,它的自旋锁更加的“智能”,synchronized 中的自旋锁是自适应自旋锁,这就好比之前一直开的手动挡的三轮车,而经过了 JDK 1.6 的优化之后,我们的这部“车”,一下子变成自动挡的兰博基尼了。
自适应自旋锁是指,线程自旋的次数不再是固定的值,而是一个动态改变的值,这个值会根据前一次自旋获取锁的状态来决定此次自旋的次数。比如上一次通过自旋成功获取到了锁,那么这次通过自旋也有可能会获取到锁,所以这次自旋的次数就会增多一些,而如果上一次通过自旋没有成功获取到锁,那么这次自旋可能也获取不到锁,所以为了避免资源的浪费,就会少循环或者不循环,以提高程序的执行效率。简单来说,如果线程自旋成功了,则下次自旋的次数会增多,如果失败,下次自旋的次数会减少。
2. synchronized实现原理
synchronized是基于对象实现的。
先要对Java中对象在堆内存的存储有一个了解。
展开MarkWord,MarkWord中标记着四种锁的信息:无锁、偏向锁、轻量级锁、重量级锁
ObjectMonitor() {
_header = NULL; // header存储着MarkWord
_count = 0; //竞争锁的线程个数
_waiters = 0, //wait的线程个数
_recursions = 0; //标识当前synchronized锁重入的次数
_object = NULL;
_owner = NULL; //持有锁的线程
_WaitSet = NULL; //保存wait的线程信息,双向链表
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ; //获取锁资源失败后,线程要放到当前的单向链表中
FreeNext = NULL ;
_EntryList = NULL ; //_cxq以及被唤醒的waitSet中的线程,在一定机制下,会放到班ntryList中
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
3. synchronized和Lock的对比
特性 | synchronized | ReentrantLock |
---|---|---|
易用性 | 简单 | 略复杂 |
锁释放 | 自动 | Lock.unLock() |
响应中断 | 不支持 | 支持 Lock.lockInterruptibly() |
公平/非公平 | 非公平 | 公平/非公平都支持 |
条件 | 单个 | 通过Condition可以绑定多个条件 |
底层实现 | JVM 层面通过监视器(Monitor)实现 | 通过 AQS程序级别的 API 实现 |
待补充
//锁消除
参考:
23年阿里最新出版,阿里三面多线程经典面试题,押题率99%(附视频笔记+思维导图)
Java---synchronized锁的优化
Synchronized优化手段:锁膨胀、锁消除、锁粗化和自适应自旋锁