文章目录
前言:本章知识点比较繁杂,面向的是有一定JUC基础的小伙伴,纯小白可能看起来不费劲。因为知识点较多所以我自己个人画出了思维导图帮助理解,全文也基本上是按照是思维导图来的,我也是最近刚复习JUC,有任何我说错的地方,欢迎大家指出,互相学习
1、锁指向总结
先说好结论 ,全文都要都是围绕这几点进行分析的,看不得懂得可以看后面的分析讲解
锁对象 | 锁指向 |
---|---|
偏向锁 | MarkWord存储的是偏向线程的ID |
轻量锁 | MarkWord存储的是线程栈中的LockRecord指针 |
重量锁 | MarkWord存储是指向堆中的monitor对象的指针 |
2、锁升级过程
下图是锁升级过程中的标记位,展示图,全文都是围绕这些进行讲解
2.1、无锁
没啥好讲的, 没啥竞争, 记好他们的标记位即可。
- 偏向锁位 0 锁标志位: 01 ;
2.2、偏向锁
这里是整个流程的总结;
2.2.1、什么时候升级成偏向锁?
- 当高并发的条件下,可能获取锁的都是同一个线程;
- 当一段同步代码一直被同一个线程多次访问后,由于只有一个线程那么该线程在后续的访问会直接获取锁,懒得连cas都不操作了,直接提高了程序的性能
2.2.1、偏向锁的原理?
当同一个线程反复抢到锁,这个线程我们称之为偏向线程, 我们只需要在他第一次获取锁的时候,记录下偏向线程的ID,这样偏向线程就一直持有锁(后序这个线程进入和退出这段加了同步锁的代码块,不需要再次的加锁和释放锁.而是直接去检查锁的MarkWord里面放的是不是自己的线程ID)
如果相等
- 如果相等,表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了,直到竞争发生才释放锁.以后每次同步,检查锁的偏向线程ID与当前线程ID是否一致,如果一致直接进入同步.无需每次加锁解锁都去更新对象头.如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高.
如果不等
- 如果不等,表示发生了竞争,锁已经不是总是偏向于同一个线程了,这个时候会尝试使用CAS来替换MarkWord里面的线程ID为新线程的ID,
– 竞争成功: 表示之前的线程不存在了,MarkWord里面的线程ID为新线程的ID,锁不会升级,仍然为偏向锁;
– 这时候可能需要升级变为轻量级锁,才能保证线程间公平竞争锁。
2.2.3、偏向锁开启条件?
- jdk1.8 默认开启,但是有4s的延迟, 可以通过参数, 把延迟设置为0
- -XX:BiasedLockingStartupDelay=0
- 特殊说明 : java15废弃偏向锁,默认关闭[开销和成本大]
2.3、撤销偏向锁
2.3.1、发生的条件
如果一个线程持有 synchronized 块的偏向锁,而另一个线程也想要获取该锁,那么偏向锁就会失效,发生锁的撤销和竞争
2.3.2、全局安全点的概念
偏向锁使用一种等到竞争出现才释放锁的机制,只有当其他线程竞争锁时,持有偏向锁的原来线程才会被撤销。撤销需要等待全局安全点(该时间点上没有字节码正在执行),同时检查持有偏向锁的线程是否还在执行:
- 当第二个线程尝试获取偏向锁时,偏向锁会失效,JVM 会检查当前持有偏向锁的线程是否仍在执行同步代码块。
- 如果持有偏向锁的线程仍在执行同步代码块,那么JVM 会暂停第二个线程,等待第一个线程执行完同步代码块后释放锁。
- 如果持有偏向锁的线程已经执行完同步代码块并释放了锁,那么JVM 会将锁升级为轻量级锁,然后继续执行第二个线程。
- 如果第二个线程竞争锁失败,那么锁就会继续保持在轻量级锁状态,等待下一次竞争。
2.4、轻量锁(CAS)
我知道这里分析的逻辑比较乱,给你们准备好了思维导图
2.4.1、发生的时机
- 多线程竞争,但是任意时刻最多只有一个线程竞争,即不存在锁竞争太过激烈的情况,也就没有线程阻塞。
2.4.2、加锁
如果CAS交换成功
- 每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word
- 让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录
- 如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁,这时图示如下
如果交换失败 - 如果是重入锁
– 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数[重入也会进入cas交换,但是此时他会失败,失败也没关系,他知道是他自己加,此时他存入的不是markWord啦, 是null ,表示我重入了几次, 就有几个null] - 不是重入锁
– 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
特点
- 在 Java 6 之后自旋锁是自适应的;
- 线程如果自旋成功了,那么下次自旋的最大次数会增加,因为JVM认为既然上次成功了,那么这一次也会很大概率成功
- 如果很少自旋成功,那么下次自旋的次数会减少,甚至不自旋,避免cpu空转
- Java 7 之后不能控制是否开启自旋功能
2.4.3、解锁
如果是重入锁
- 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
不是重入锁 - 当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头
- 成功,则解锁成功
- 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
2.4.4、释放CAS锁
在释放锁时,当前线程会使用CAS操作将Displaced Mark Word的内容复制回锁的Mark Word里面。如果没有发生竞争,那么这个复制的操作会成功。如果有其他线程因为自旋多次导致轻量级锁升级成了重量级锁,那么CAS操作会失败,此时会释放锁并唤醒被阻塞的线程。
2.5、重量级锁/锁膨胀
2.5.1、发生的时机
如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁
2.5.2、步骤原理
- 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
这时 Thread-1 加轻量级锁失败,进入锁膨胀流程,即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址然后自己进入 Monitor 的 EntryList BLOCKED
当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程 [因为object已经指向了Monitor锁]
3、锁升级后,hashCode去哪里了
4、各种锁的优缺点对比
- 偏向锁:适用于单线程适用的情况,在不存在锁竞争的时候进入同步方法/代码块则使用偏向锁。
- 轻量级锁:适用于竞争较不激烈的情况(这和乐观锁的使用范围类似),存在竞争时升级为轻量级锁,轻量级锁采用的是自旋锁,如果同步方-法/代码块执行时间很短的话,采用轻量级锁虽然会占用cpu资源但是相对比使用重量级锁还是更高效。
- 重量级锁:适用于竞争激烈的情况,如果同步方法/代码块执行时间很长,那么使用轻量级锁自旋带来的性能消耗就比使用重量级锁更严重,这时候就需要升级为重量级锁。
5、JIT编译器对锁的优化
JIT Just in time compiler 及时编译器, Java中会对我们写的代码,进行一些合理的优化;
5.1、锁消除
从JIT的角度相当于无视了synchronized(o) 不存在了;这个锁并没有共用扩散到其他线程使用
5.2、锁粗化
假如方法中首尾相接,前后相邻的都是同一个锁对象,那了IT编译器就会把这几个synchronized块合并成一个大块. 加粗加大范围,一次申请锁使用即可,避免次次的申请和隆放锁,提升了性能