(4.6)轻量级锁(锁膨胀、锁自选、偏向锁、锁消除)
4.6 轻量级锁、偏向锁——Monitor升级
JDK6之前的加锁方式是:关联锁对象到Monitor进行加锁,Monitor是由操作系统提供的,加锁代价高。
JDK6之后,对加锁方式进行了优化,引入了轻量级锁、偏向锁等。
1. 轻量级锁
如果一个对象虽然有多个线程要加锁,但是加锁的时间是错开的(没有竞争),可以使用轻量级锁来优化。
如果这时有其他线程来获取轻量级锁,则被阻塞,加锁线程将轻量级锁升级为Monitor锁。
假设有两个方法同步块,利用同一个对象加锁
private static final Object lock = new Object();
public static void method1() {
synchronized (lock) {
log.info("method1....");
method2();
}
}
private static void method2() {
synchronized (lock) {
log.info("method2...");
}
}
-
synchronized加锁时,在栈帧中创建锁记录(Lock Record)。 锁记录的组成:
- 锁对象指针(Lock Record address):锁对象指针记录锁对象的地址
- 锁记录地址(Object reference):锁记录地址记录锁对象的Markword。
-
让Object reference指向锁对象;然后尝试用锁记录地址CAS替换锁对象的MarkWord。
-
如果成功,则MarkWord的值存入锁记录中,对象头的MarkWord存储了
锁记录地址和状态00
-
如果失败,则说明
- 对象锁已经持有了其他线程的轻量级锁(对象头的状态为00),这时表示有竞争,进入锁膨胀状态
- 对象锁持有了自己的锁(锁对象状态为00, 并且存储的锁记录地址是自己的锁记录的地址),表示执行了synchronized锁重入,则在栈帧中在创建一条锁地址为null的LockRecord,用于重入计数。
-
解锁时,是null的锁记录,表示有重入,这时清除锁记录,同时重入计数-1
-
解锁时,是不为null的锁记录,则尝试cas恢复MarkWord给锁对象
- 成功,解锁成功
- 失败,则说明有竞争,轻量级锁已经升为重量级锁,进入重量级锁解锁流程
2. 锁膨胀
线程尝试CAS加轻量级锁时,失败,如果情况是有其他线程已经对锁对象加上了轻量级锁,则进行锁膨胀,将轻量级锁变为重量级锁。
- 线程1尝试CAS加锁时,线程0已经对该对象加了轻量级锁
- 进入锁膨胀。为锁对象申请Monitor锁,让锁对象的MarkWord指向Monitor,状态变为10; Monitor的Own指向线程0;自己进入EntryList进入阻塞状态
- 线程0解锁时,尝试CAS将MarkWord恢复给锁对象,失败。说明已经锁膨胀。通过锁对象的MarkWord找到Monitor地址;将Owner设为null;唤醒EntryL中的阻塞线程,让阻塞线程获取重量级锁;Monitor记录线程的Hashcode。
- 线程1解锁时,没有阻塞线程,将锁
Hashcode age bias 01
恢复给锁对象,解锁。
3. 锁自旋
重量级锁竞争时,竞争线程进入EntryList阻塞前,还可以使用自旋来优化,如果自旋成功,就避免了阻塞带来的上下文切换。
JDK6的自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会 高,就多自旋几次;反之,就少自旋甚至不自旋。
JDK7之后无法控制是否开启自旋锁。
优点:
- 避免阻塞带来的上下文切换
缺点:
- 自旋会占用CPU时间,多核自旋才能发挥优势
4. 偏向锁
当轻量级锁(没有竞争,就自己这个线程)+ 锁重入时,每次都需要生成锁记录,并尝试CAS替换对象头的MarkWord操作。
JDK6引入偏向锁做进一步优化: 只有第一次加锁时,不需要生成锁记录,将线程ID设置到锁对象的MarkWord中;之后锁重入时,检查MarkWord是否是自己的线程ID,只要不发生竞争,则可以一直使用偏向锁。
优点:避免了每次加锁都需要生成锁记录,并CAS的过程。
5. 撤销偏向锁
(1)调用hashcode
调用hashcode会撤销偏向锁,因为偏向锁的锁对象MarkWord存储的是线程ID,调用偏向锁会导致偏向锁被撤销。
- 轻量级锁在锁记录中存储hashcode
- 重量级锁在Monitor中记录hashcode
(2) 其他线程交错使用锁对象
会将偏向锁升级为轻量级锁
(3) 其他线程竞争使用锁对象
将偏向锁升级为重量级锁
(4) 调用wait/notify
wait、notify是重量级锁才有的机制,调用它们会将偏向锁升级为重量级锁
6. 批量重偏向
锁对象偏向线程1,这时有线程2交替获取锁,会撤销包含线程1ID的偏向锁(线程ID变为线程2ID,释放锁后,偏向锁ID恢复为线程1ID);
当阈值超过20此后,会在加锁时重新偏向线程2(即将锁对象的线程ID替换为线程2的ID)
7. 批量撤销
当撤销偏向锁次数超过40,JVM意识到重偏向也不对,于是整个类的所有对象都变为不可偏向锁,新创建的对象也是不可偏向锁。
8. 锁消除
如果锁对象是局部的、不同享的、没有竞争的,则JIT即时编译器会对代码进行优化,执行时不加锁。提高运行效率。
锁消除默认开启,关闭锁消除:
java -XX:-EliminateLocks -java HelloConcurrent.jar
手动开启锁消除
java -XX:EliminateLock -java HelloConcurrent.jar