简介:
- Synchronized如何工作的
- Synchronized锁升级的过程
- 重量级锁队列之间的协作过程和策略
一、对象头
在JVM中对象布局分为单块区域:
- 对象头
- 实例数据
- 对象填充
当线程访问同步块时首先需要获取锁并把相关信息存储在对象头中,所以wait、notify、notifyAll这些方法设计在Object中。
HotSpot有两种对象头:数据对象头、其他
对象头的组成部分:
- Mark Word:存储自身的运行时数据,例如HashCode、GC年龄、锁相关信息等
- Class Pointer:类型指针,指向它的类元数据的指针
锁优化:
- 偏向锁
- 轻量级锁
- 适应性自旋锁
- 锁消除
- 锁粗话
锁升级流程:无锁-->偏向锁-->轻量级锁-->重量级锁
二、偏向锁
流程:当线程访问同步块并获取锁时,流程如下
- 检查mark word的线程id
- 如果为空则设置CAS替换当前线程id。如果替换成功则获取锁成功,如果失败则撤销偏向锁
- 如果不为空则检查线程id是否为本线程。如果时则获取锁成功,如果失败则撤销偏向锁
持有偏向锁的线程以后每次进入这个锁相关的同步快时,只需要对比下mark word的线程id是否为本线程,如果时则获取锁成功。如果发生线程竞争既发生2、3步失败的情况则需要撤销偏向锁
偏向锁的撤销:
- 偏向锁的撤销动作必须等待全局安全点
- 暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态
- 撤销偏向锁恢复到无锁状态(标志位01)或轻量级锁(标志位00)的状态
优点:
只有一个线程执行同步块时进一步提高性能,适用于一个线程反复获取同一锁的情况。偏向锁可以提高带有同步但无竞争的程序性能。
缺点:如果存在竞争会带来额外的锁撤销操作。
三、轻量级锁
多个线程竞争偏向锁导致偏向锁升级为轻量级锁
- JVM在当前线程的栈帧中创建Lock Record,并将对象头中的Mark Word复制到Lock Record中。(Displaced Mark Word)
- 线程尝试使用CAS将对象头中的Mark Word替换为指向Lock Record的指针。如果成功则获取锁,如果失败则先检查对象的Mark Word是否指向当前线程的栈帧,如果是则说明已经获取锁,否则说明其他线程竞争锁则膨胀为重量级锁。
解锁:
- 使用CAS操作将Mark Word还原
- 如果第一步执行成功则释放完成
- 如果第一步执行失败则膨胀为重量级锁
优点:其性能提升的依据是对于绝大部分的锁在整个生命周期内都是不会存在竞争。在多线程交替执行同步块的情况下,可以避免重量级锁引起的性能消耗。
缺点:在有多线程竞争的请况下轻量级锁增加了额外开销
四、自旋锁
自旋是一种获取锁的机制并不是一个锁状态。在膨胀为重量级锁的过程中或重入时会多次尝试自旋获取锁以避免线程唤醒的开销,但是它会占用 CPU 的时间因此如果同步代码块执行时间很短自旋等待的效果就很好,反之则浪费了 CPU 资源。默认情况下自旋次数是 10 次用户可以使用参数 -XX : PreBlockSpin
来更改。那么如何优化来避免此情况发生呢?我们来看适应性自旋。
五、适应性自旋锁
JDK 6 引入了自适应自旋锁,意味着自旋的次数不在固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果对于某个锁很少自旋成功那么以后有可能省略掉自旋过程以避免资源浪费。有了自适应自旋随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测就会越来越准确。
优点:竞争的线程不会阻塞挂起,提高了程序响应速度。避免重量级锁引起的性能消耗(阻塞、唤醒)
缺点:如果线程始终无法获取锁,自旋消耗CPU且最终膨胀为重量级锁。
六、重量级锁
在重量级锁中没有竞争到锁的对象会 park 被挂起,退出同步块时 unpark 唤醒后续线程。唤醒操作涉及到操作系统调度会有额外的开销。
在 HotSpot 中 monitor 是由 ObjectMonitor 实现的。其源码是用 c++来实现的源文件是 ObjectMonitor.hpp 主要数据结构如下所示:
ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0, // 等待中的线程数
_recursions = 0; // 线程重入次数
_object = NULL; // 存储该 monitor 的对象
_owner = NULL; // 指向拥有该 monitor 的线程
_WaitSet = NULL; // 等待线程 双向循环链表_WaitSet 指向第一个节点
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ; // 多线程竞争锁时的单向链表
FreeNext = NULL ;
_EntryList = NULL ; // _owner 从该双向循环链表中唤醒线程,
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0; // 前一个拥有此监视器的线程 ID
}
- _owner:初始时为 NULL。当有线程占有该 monitor 时 owner 标记为该线程的 ID。当线程释放 monitor 时 owner 恢复为 NULL。owner 是一个临界资源 JVM 是通过 CAS 操作来保证其线程安全的。
- _cxq:竞争队列所有请求锁的线程首先会被放在这个队列中(单向)。_cxq 是一个临界资源 JVM 通过 CAS 原子指令来修改_cxq 队列。
- 每当有新来的节点入队,它的 next 指针总是指向之前队列的头节点,而_cxq 指针会指向该新入队的节点,所以是后来居上。
- _EntryList: _cxq 队列中有资格成为候选资源的线程会被移动到该队列中。
- _WaitSet: 等待队列因为调用 wait 方法而被阻塞的线程会被放在该队列中。
monitor 竞争过程
- 通过 CAS 尝试把 monitor 的 owner 字段设置为当前线程。
- 如果设置之前的 owner 指向当前线程,说明当前线程再次进入 monitor,即重入锁执行 recursions ++ , 记录重入的次数。
- 如果当前线程是第一次进入该 monitor, 设置 recursions 为 1,_owner 为当前线程,该线程成功获得锁并返回。
- 如果获取锁失败,则等待锁的释放。
monitor 等待
- 当前线程被封装成 ObjectWaiter 对象 node,状态设置成 ObjectWaiter::TS_CXQ。
- for 循环通过 CAS 把 node 节点 push 到
_cxq
列表中,同一时刻可能有多个线程把自己的 node 节点 push 到_cxq
列表中。- node 节点 push 到_cxq 列表之后,通过自旋尝试获取锁,如果还是没有获取到锁则通过 park 将当前线程挂起等待被唤醒。
- 当该线程被唤醒时会从挂起的点继续执行,通过 ObjectMonitor::TryLock 尝试获取锁。
monitor 释放
当某个持有锁的线程执行完同步代码块时,会释放锁并
unpark
后续线程(由于篇幅只保留重要代码)。
notify 唤醒
notify 或者 notifyAll 方法可以唤醒同一个锁监视器下调用 wait 挂起的线程,具体实现如下
七、总结
- 偏向锁通过对比 Mark Word thread id 解决加锁问题;
- 轻量级锁是通过用 CAS 操作 Mark Word来解决加锁问题;
- 自旋锁(获取锁的方式)避免线程阻塞和唤醒而影响性能;
- 重量级锁是将除了拥有锁的线程以外的线程都阻塞。