锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级也可以降级,不过降级的条件特别苛刻,当JVM进入安全点(SafePoint) 的时候, 会检查是否有闲置的Monitor, 然后试图进行降级。
一、锁的分类
1、⽆锁状态
⽆锁就是没有对资源进⾏锁定,任何线程都可以尝试去修改它
2、偏向锁状态
偏向锁会偏向于第⼀个访问锁的线程,如果在接下来的运⾏过程中,该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要触发同步。
偏向锁在资源⽆竞争情况下消除了同步语句,连CAS操作都不做,提⾼了程序的运⾏性能
偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)
偏向锁的获得与撤销的过程
(1)⼀个线程在第⼀次进⼊同步块时,会在对象头和栈帧中的锁记录⾥存储锁的偏向的
线程ID。
(2)当下次该线程进⼊这个同步块时,会去检查锁的Mark Word⾥⾯是不是放
的⾃⼰的线程ID。
(3)如果是,表明该线程已经获得了锁,以后该线程在进⼊和退出同步块时不需要花费
CAS操作来加锁和解锁
(4)如果不是,就代表有另⼀个线程来竞争这个偏向锁。这个时候会尝试使⽤CAS来替换Mark Word⾥⾯的线程ID为新线程的ID,这个时候要分两种情况:
成功,表示之前的线程不存在了, Mark Word⾥⾯的线程ID为新线程的ID,锁不会升级,仍然为偏向锁;
失败,表示之前的线程仍然存在,那么暂停之前的线程,设置偏向锁标识为0,并设置锁标志位为00,升级为轻量级锁,会按照轻量级锁的⽅式进⾏竞争锁。
偏向锁的获得与撤销流程图
3、轻量级锁状态
多个线程在不同时段获取同⼀把锁,即不存在锁竞争的情况,也就没有线程阻塞。
针对这种情况,JVM采⽤轻量级锁来避免线程的阻塞与唤醒。
轻量级锁的升级过程
(1)如果⼀个线程获得锁的时候发现是轻量级锁,会把锁的Mark Word复制到⾃⼰的Displaced Mark Word⾥⾯。
(2)然后线程尝试⽤CAS将锁的Mark Word替换为指向锁记录的指针。
如果成功,当前线程获得锁,
如果失败,表示Mark Word已经被替换成了其他线程的锁记录,说明在与其它线程竞争锁,当前线程就尝试使⽤⾃旋来获取锁。⾃旋到⼀定程度(和JVM、操作系统相关),依然没有获取到锁,称为⾃旋失败,那么这个线程会阻塞。同时这个锁就会升级成重量级锁
⾃旋是需要消耗CPU的,JDK采⽤了更聪明的⽅式——适应性⾃旋,简单来说就是线程如果⾃旋成功
了,则下次⾃旋的次数会更多,如果⾃旋失败了,则⾃旋的次数就会减少
4、重量级锁状态
重量级锁依赖于操作系统的互斥量(mutex) 实现的,⽽操作系统中线程间状态的转换需要相对⽐较⻓的时间,所以重量级锁效率很低,但被阻塞的线程不会消耗CPU
当调⽤⼀个锁对象的 wait 或 notify ⽅法时,如当前锁的状态是偏向锁或轻量级锁则会先膨胀成重量级锁
轻量级锁及膨胀流程图
二、锁的升级流程
每⼀个线程在准备获取共享资源时
(1)检查MarkWord⾥⾯是不是放的⾃⼰的ThreadId ,如果是,表示当前线程是处于 “偏向锁” 。
(2)如果MarkWord不是⾃⼰的ThreadId,锁升级,这时候,⽤CAS来执⾏切换,新的线程根据MarkWord⾥⾯现有的ThreadId,通知之前线程暂停,之前线程将Markword的内容置为空。
(3)两个线程都把锁对象的HashCode复制到⾃⼰新建的⽤于存储锁的记录空间,接着开始通过CAS操作, 把锁对象的MarKword的内容修改为⾃⼰新建的记录空间的地址的⽅式竞争MarkWord。
(4)第三步中成功执⾏CAS的获得资源,失败的则进⼊⾃旋 。
(5)⾃旋的线程在⾃旋过程中,成功获得资源(即之前获的资源的线程执⾏完成并释放了共享资源),则整个状态依然处于 轻量级锁的状态,如果⾃旋失败 。
(6)进⼊重量级锁的状态,这个时候,⾃旋的线程进⾏阻塞,等待之前线程执⾏完成并唤醒⾃⼰。
三、锁的优缺点对比
锁 | 优点 | 缺点 | 使用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问同步块场景 |
轻量级锁 | 竞争的线程不会阻塞,提供了程序的响应速度 | 如果始终得不到锁竞争的线程,使用自旋会消耗CPU | 追求响应时间,同步块执行速度非常快 |
重量级锁 | 线程竞争不适用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢 | 追求吞吐量,同步块执行速度较长 |