1.什么是轻量级锁
线程之间存在锁的伪竞争行为
,即同一时刻绝对不会存在两个线程申请获取锁,各个线程尽管都有使用锁的需求,但是是交替使用锁
(轻量级锁情况下,线程还是不会发生堵塞)
2.为什么引入轻量级锁
轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的场景。因为阻塞线程需要CPU从用户态转到内核态,代价比较大,如果刚刚堵塞不久这个锁被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不堵塞这个线程,让他自旋等待锁的释放
3.轻量级锁的升级时机
- 关闭偏向锁功能
- 使用 -XX:-UseBiasedLocking参数关闭偏向锁,此时默认进入轻量级锁
- 多个线程竞争偏向锁
- 偏向锁状态下,由于别的线程尝试竞争偏向锁,并且CAS更新MarkWord中线程ID失败,此时发生【偏向锁 >> 轻量级锁】升级
4.轻量级锁演示案例
- case1
public class Main {
public static void main(String[] args) {
// -XX:UseBiasedLocking 关闭偏向锁功能,此时在有锁竞争的场景下 objLock起步就是轻量级锁
Object objLock = new Object();
synchronized (objLock){
System.out.println(ClassLayout.parseInstance(objLock).toPrintable());
}
}
}
- case2
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread.sleep(5000);
Object objLock = new Object();
System.out.println(Thread.currentThread().getName() + " - " + ClassLayout.parseInstance(objLock).toPrintable());
new Thread(() -> {
synchronized (objLock){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println(Thread.currentThread().getName() + " - " + ClassLayout.parseInstance(objLock).toPrintable());
},"T1").start();
Thread.sleep(1000);
new Thread(() ->{
synchronized (objLock){
System.out.println(Thread.currentThread().getName() + " - " + ClassLayout.parseInstance(objLock).toPrintable());
}
},"T2").start();
}
}
# WARNING: Unable to attach Serviceability Agent. You can try again with escalated privileges. Two options: a) use -Djol.tryWithSudo=true to try with sudo; b) echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
main - java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
T1 - java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 38 41 cc (00000101 00111000 01000001 11001100) (-868141051)
4 4 (object header) 96 7f 00 00 (10010110 01111111 00000000 00000000) (32662)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
T2 - java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 78 e8 5f 90 (01111000 11101000 01011111 10010000) (-1872762760)
4 4 (object header) 96 7f 00 00 (10010110 01111111 00000000 00000000) (32662)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
结合代码与输出结构进行分析
- 首先是在执行代码前对主线程(mian)进行了5s的阻塞,这样的目的是为了让偏向锁加载成功,到后面线程T1去获取锁对象的时候,此时的锁就是一个偏向锁
- 在T1线程的同步代码块中对T1线程进行了1s的阻塞,用来模拟线程持有锁,但是持有时间并不长
- 在创建T2线程前对主线程进行了1s的阻塞,这里是为了避免在同一时刻有超过一个线程对同一把锁的竞争,确保这里一定是T1先获取到锁(避免锁的膨胀导致锁最终变成重量级锁)
- 当创建T2线程获取锁的时候T1线程恰好使用完了锁,并释放了锁,此时T2线程会尝试通过CAS去修改Mark Word中的偏向线程ID,此时修改偏向线程ID失败,导致锁从偏向锁升级到轻量级锁
5.轻量级锁的原理
5.1.轻量级锁的加锁
- JVM会在当前线程的栈帧中创建一个名为锁记录(LockRecord)的空间,用于存储锁对象目前Mark Word的拷贝(官方称为 Displaced Mark Word)若一个线程获取锁发现是轻量级锁,它会将对象的Mark Word复制到栈帧中的LockRecord中(Displaced Mark Word里面)
- 线程尝试利用CAS操作将对象的Mark Work更新为指向LockRecord的指针,如果成功表示当前线程竞争到了锁,如则将锁的标志位改成00,执行同步操作
- 如果失败,表示Mark Work已经被替换成其他的线程锁记录,说明在于其他线程竞争锁,当前贤臣会尝试使用自旋来获取锁
5.2.轻量级锁的释放
轻量级所得释放也是通过CAS操作来进行的,当前线程使用CAS操作将Displaced Mark Word的内存复制回锁对象的Mark Word,如果CAS操作成功,则说明锁释放成功;如果CAS自旋多次还是替换失败的话,说明有其他线程尝试获取锁(同时有多个线程对锁产生了竞争),则需要将轻量级锁膨胀升级为重量级锁
6.轻量级锁的升级流程
暂时无法在飞书文档外展示此内容
7.轻量级锁的优缺点
- 优点:在多线程交替执行同步块的情况下,可以避免重量级锁引起的性能消耗
- 缺点:如果长时间自旋还没有竞争到锁,将会过度消耗CPU,即CPU空转