偏向锁_轻量级锁_重量级锁
高并发场景(EntrySet、偏向锁、轻量级锁、重量级锁)
考虑大量线程竞争一个用户实现的synchronized
块,如图:
在高并发场景,竞争非常激烈。而作为语言的设计者,不能假设synchronized
的块可以迅速完成。图中很多线程竞争一个同步块,按照之前的设计,这个时候这些线程会优先考虑自旋锁。因为同步块需要10ms
,因此多数线程拿不到自旋锁。考虑到CPU执行速度非常快,每10ms
才有一个线程拿到锁。而1ms
可以执行数以10万-百万计的CAS操作。因此这样是非常不划算的:
- 自旋锁消耗大量计算能力(CPU资源)
- 大量线程进入WaitSet
这时JVM的解决方案是增加一个EntrySet。如图:
图中线程先用自旋锁竞争进入EntrySet,竞争进入EntrySet只需要少量的cas操作。
如果像图中那样用链表实现EntrySet,新线程进入EntrySet只需要两条指令,第一步创建一个节点,指向EntrySet的第一个元素。第二步,用cas操作将EntrySet的尾部指向新的节点。 这样需要的时间是非常短的,因此多数线程都可以进入EntrySet。
进入EntrySet之后,如果当前没有持有者(没有线程再执行)。就让这个线程去竞争:其实就是把线程的状态改为READY,让它竞争。如果既没有持有者,WaitSet也是空的,那么就不存在竞争,可以考虑直接让线程执行。但是判断有没有持有者,再判断WaitSet种有没有线程,这个过程不是一个原子操作。因此Java在每个对象的头部,增加了几个标识位,记录这个资源的所有者,如果这个位没有被占领,那么就可以考虑直接执行这个线程。这个方法称为**偏向锁(Bias Lock)**。
如果有线程占领了偏向锁,那么说明有多个线程在竞争,就升级为轻量级锁(Light Weighted Lock)。轻量级锁,会先观察目前Monitor 中有没有持有者(Owner)——正在临界区的线程。如果没有,就直接去竞争Owner。为什么不直接给Owner? 因为这个时候有可能WaitSet的线程也在竞争Owner,比如用户在这之前刚好调用了notifyAll,通知了所有WaitSet中的线程去竞争Owner。轻量级锁的特点是会先进行自旋,如果若干次自旋后,比如10次,还是无法获取锁,就加入EntrySet,其实就是采用重量级锁。之所以称为重量级锁,是因为接下来每个从EntrySet获取竞争机会的线程,会直接用操作系统的mutex API进行锁的竞争。
一个持有锁的线程离开临界区后,会重新进入WaitSet。如果这个线程休眠,也会进入WaitSet。WaitSet和EntrySet中的线程,当持有线程休眠或者离开时,会一起再次竞争Owner,这个时候不再使用偏向锁和轻量级锁,这个特性我们称为锁的不可降级。 一旦升级为重量级锁,不会再使用轻量级或者偏向锁。 重量级锁,就是利用操作系统API提供的互斥能力进行竞争。
重量级锁
Monitor 实现锁分配
2.1 Monitor 的结构
在 Java 虚拟机 (HotSpot)中,Monitor 基于C++实现,其数据结构为 objectMonitor.hpp ,比较关键的属性如下:
1 | ObjectMonitor() { |
锁相关主要的流程如下:
当多个线程同时访问一段同步代码时,首先进入_EntryList队列中,当某个线程获取到对象的Monitor后进入_Owner区域并把Monitor中的_owner变量设置为当前线程,同时Monitor中的计数器_count自增1,表示获得对象锁
若持有Monitor的线程调用wait()方法,将释放当前持有的 Monitor ,_owner变量恢复为 null,_count自减1,同时该线程进入_WaitSet集合中等待被唤醒。若当前线程执行完毕也会释放Monitor(锁)并复位变量的值,以便其他线程能够获取Monitor(锁)
在 Entry Set中等待的线程状态是 Waiting for monitor entry,而在 Wait Set中等待的线程状态是 in Object.wait()
2.2 获取锁流程