偏向锁
当线程访问同步块并获取锁时处理流程如下:
- 检查
mark word
的线程 id
。 - 如果为空则设置 CAS 替换当前线程 id。如果替换成功则获取锁成功,如果失败则撤销偏向锁。
- 如果不为空则检查
线程 id
为是否为本线程。如果是则获取锁成功,如果失败则撤销偏向锁。
持有偏向锁的线程以后每次进入这个锁相关的同步块时,只需比对一下 mark word 的线程 id 是否为本线程,如果是则获取锁成功。
如果发生线程竞争发生 2、3 步失败的情况则需要撤销偏向锁。
偏向锁撤销
- 偏向锁的撤销动作必须等待全局安全点
- 暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态
- 撤销偏向锁恢复到无锁(标志位为 01)或轻量级锁(标志位为 00)的状态
总结:只有一个线程执行同步块时进一步提高性能,适用于一个线程反复获得同一锁的情况。偏向锁可以提高带有同步但无竞争的程序性能。但是,如果存在竞争会带来额外的锁撤销操作。
轻量级锁
线程竞争或者自旋次数较多(默认是10次),偏向锁升级为轻量级锁
- JVM 在当前线程的栈帧中创建 Lock Reocrd,并将对象头中的 Mark Word 复制到 Lock Reocrd 中。(Displaced Mark Word)
- 线程尝试使用 CAS 将对象头中的 Mark Word 替换为指向 Lock Reocrd 的指针。如果成功则获得锁,如果失败则先检查对象的 Mark Word 是否指向当前线程的栈帧如果是则说明已经获取锁,否则说明其它线程竞争锁则膨胀为重量级锁。
解锁(轻量级锁 ——> 偏向锁\系统锁)
- 使用 CAS 操作将 Mark Word 还原
- 如果第 1 步执行成功则释放完成 (轻量级锁 ——> 偏向锁)
- 如果第 1 步执行失败则膨胀为重量级锁。(轻量级锁 ——> 系统锁)
总结
其性能提升的依据是对于绝大部分的锁在整个生命周期内都是不会存在竞争。在多线程交替执行同步块的情况下,可以避免重量级锁引起的性能消耗。
但是,如果线程始终无法获取锁,自旋消耗 CPU 最终会膨胀为重量级锁。
重量级锁
在重量级锁中没有竞争到锁的对象会 park 被挂起,退出同步块时 unpark 唤醒后续线程。唤醒操作涉及到操作系统调度会有额外的开销。
ObjectMonitor
中包含一个同步队列(由 _cxq
和 _EntryList
组成)一个等待队列( _WaitSet
)。
- 被
notify
或notifyAll
唤醒时根据policy
策略选择加入的队列(policy 默认为 0) - 退出同步块时根据
QMode
策略来唤醒下一个线程(QMode 默认为 0)
这里稍微提及一下管程这个概念。synchronized 关键字及 wait
、notify
、notifyAll
这三个方法都是管程的组成部分。可以说管程就是一把解决并发问题的万能钥匙。有两大核心问题管程都是能够解决的:
- 互斥:即同一时刻只允许一个线程访问共享资源;
- 同步:即线程之间如何通信、协作。
synchronized
的 monitor
锁机制和 JDK 并发包中的 AQS
是很相似的,只不过 AQS
中是一个同步队列多个等待队列。