当声明synchronized当声明 synchronized 代码块时,编译而成的字节码将包含monitorenter指令和monitorexit指令,二者是一对多的关系(JVM需要保证所获得的锁在正常执行路径以及异常执行路径上都能够被解锁)
monitorenter + monitorexit
抽象理解:每个锁对象拥有一个锁计数器和指向持有该锁的线程的指针。
-
当执行monitorenter时,如果锁对象的计数器不为0
- 如果锁对象的持有线程是当前线程,那么JVM会将其计数器+1
否则需要等待,直到持有该锁对象的线程释放该锁
- 如果锁对象的持有线程是当前线程,那么JVM会将其计数器+1
-
当执行monitorexit时,JVM则需要将该锁对象的计数器-1
- 当计数器减为0时,那么代表该锁已经被释放掉了
采用计数器的方式,是为了允许同一个线程重复获取同一把锁,可重入。
针对一个对象的整个生命周期,锁升级是单向不可逆:偏向锁 -> 轻量级锁 -> 重量级锁
重量级锁
红绿灯Java 线程的阻塞相当于熄火停车,而自旋状态相当于怠速停车。
-
重量级锁是JVM中最为基础的锁实现
- JVM会阻塞加锁失败的线程,并在目标锁被释放掉的时候,唤醒这些线程
- Java线程的阻塞和唤醒,都依赖于操作系统完成
- 涉及系统调用,需要从操作系统的用户态切换至内核态,开销很大
-
为了尽量避免昂贵的线程阻塞和唤醒操作,采用自旋
- 自旋:在处理器上空跑并且轮询锁是否被释放
- 线程会进入自旋的两种情况(睡前醒后):
(1)线程即将进入阻塞状态之前
(2)线程被唤醒后竞争不到锁 - 与线程阻塞相比,自旋可能会浪费大量的处理器资源
- JVM采取的是自适应自旋根据以往自旋等待时间是否能够获得锁来动态调整自旋的时间(循环次数)
- 自旋还会带来不公平锁
(1)处于阻塞状态的线程,没有办法立即竞争被释放的锁
(2)处于自旋状态的线程,则很有可能优先获得这把锁
轻量级锁
深夜交通闪黄灯,代表车辆可自由通过
- 场景:多个线程在不同的时间段请求同一把锁,没有锁竞争
- JVM采用轻量级锁,可以避免避免重量级锁的阻塞和唤醒
偏向锁
-
在无多线程竞争的情况下,仍然需要尽量减少不必要的轻量级锁的执行路径
- 轻量级锁的获取和释放依赖多次CAS原子指令,由此引入了偏向锁
- 偏向锁乐观地认为:从始至终只有一个线程请求某一把锁
只需要在置换ThreadID时需要依赖一次CAS操作
-
一旦出现多线程竞争,必须撤销偏向锁
- 轻量级锁是为了在线程交替执行同步块时提高性能
- 偏向锁是为了在只有一个线程执行同步块时进一步提高性能