锁升级流程图

Mark-Word
进入锁升级的学习前,必须明确一个概念,所谓的锁,所著的一定是一个对象,实例也好,类也好。先了解Java对象头中的mark-word

为什么无锁/偏向锁的标志位是01,而轻量级锁的标志位是00?
即按理说,无锁是锁状态的初始情况,为什么标志位不是从00开始?
个人查询到的一个解释,是因为 轻量级锁除了锁标志位外,另外62位都是一个指针地址。
如果将轻量级锁标志位设置为00, 那么在判断标志位为00后, m无需再额外做一次markWord>>2的操作,而是直接将markWord拿来当作地址使用即可!
哈希code去哪了
我们注意到无锁时的hashcode不见了。
对于偏向锁而言, 一旦在对象头中设置过hashcode, 那么进入同步块时就不会进入偏向锁状态,会直接跳到轻量级锁,毕竟偏向锁里没有存放hashcode的地方(下文的轻量级锁和重量级锁则有存储的地方)
因此凡是做过类似hashmap.put(k,v)操作且没覆写hashcode的k对象, 以后加锁时,都会直接略过偏向锁。
偏向锁
偏向锁的意义
假设一个场景,一个同步代码块只有一个线程使用,如果使用多次的话,需要重复获取锁和释放锁的操作,这对性能很有影响,而偏向锁在退出同步代码块时,不会释放锁,而是继续占用,下次进入同步代码块时,只需检查锁中的线程id是否是自己的线程id
无锁升级为偏向锁
首次进入同步代码块时,会通过cas的方式设置锁中的线程id(采用cas是因为存在同时写的可能的),进入偏向锁,即无锁转为偏向锁(个人认为用升级两个字其实不太妥当,因为无锁根本不涉及到同步代码块)

偏向锁的竞争
当一个线程已经持有偏向锁,而另一个线程也想进入该同步代码块时,就会出现竞争,
- 第二个线程先等程序进入jvm全局安全点(即当前无字节码执行)
- 判断偏向锁占有线程是否还在执行
- 如没有执行则退为无锁,第二个线程通过cas尝试获取偏向锁,若还在执行,则进入轻量级锁的升级过程
偏向锁启用延迟
即使你开启了偏向锁,但是这个偏向锁的启用是有延迟,大概 4s左右。即java进程启动的4s内,都会直接跳过偏向锁,有同步代码块时直接使用轻量级锁。
原因是 JVM 初始化的代码有很多地方用到了synchronized,如果直接开启偏向,产生竞争就要有锁升级,会带来额外的性能损耗,jvm团队经过测试和评估, 选择了启动速度最快的方案, 即强制4s内禁用偏向锁,所以就有了这个延迟策略 (当然这个延迟时间也可以通过参数自己调整)
偏向锁的演变历史
偏向锁在JDK6引入, 且默认开启偏向锁优化, 可通过JVM参数-XX:-UseBiasedLocking来禁用偏向锁。但随着时代发展,发现偏向锁带来的维护、撤销成本, 远大于轻量级锁的少许CAS动作。官方说明中有这么一段话: since the introduction of biased locking into HotSpot also change the amount of uncontended operations needed for that relation to remain true。
即随着硬件发展,原子指令成本变化,导致轻量级自旋锁需要的原子指令次数变少,所以自旋锁成本下降,故偏向锁的带来的优势就更小了。于是jdk团队在Jdk15之后, 再次默认关闭了偏向锁。
轻量级锁
轻量级锁的意义
我们需要先明白一下阻塞线程是一个很消耗性能的操作,我们假设现在多个线程不断的轮着执行同步代码块,会不断的发生获取和释放锁的操作,就会不断地阻塞和唤醒线程,非常消耗性能,而轻量级锁则不阻塞线程,而是通过自旋尝试获取锁来达到类似阻塞的效果(这里需要强调一点,自旋是需要消耗cpu的,如果自旋过久的话,性能消耗反而会超过阻塞线程的性能消耗)
阻塞线程为什么消耗性能
因为阻塞线程需要操作系统的介入,导致从用户态切换到内核态,这是一个很消耗性能的过程
偏向锁升级为轻量级锁
如果对象进行过获取hashcode的操作,则直接无锁进入轻量级锁
在上文提到了偏向锁升级为轻量级锁的时机,那升级过程究竟是怎样的呢,我们注意到,当进入轻量级锁时,mark-word的一些属性不存在了,其实这些属性被存储到对应线程栈帧中的lockRecord(即轻量级锁)中了
- 当其他线程将锁升级为轻量级锁后,当前持有锁的线程会升级为轻量级锁,复制对象头中的mark-word到lockRecord中,然后将mark-word中的内容替换为指向当前lockRecord的指针
- 其他线程通过轻量级锁的机制竞争锁,即自旋的方式,如果自旋的次数超过阈值,则升级为重量级锁

轻量级锁的重入机制
首先提一下为什么偏向锁没有重入机制,因为偏向锁的本质是一个线程持有锁,所以无论重入多少次,也只有一个线程访问,也没有重入机制的必要了
线程的lockRecord不是一个单一的lockRecord,而是lockRecord集合,每次重入会添加一个存储null的lockRecord,所以除了第一个(其实应该叫最后一个,因为是最后访问到的)lockRecord存储的是对象的mark-word,其他的lockRecord存储的都是null,每一次退出重入时,删除一个lockRecord(这里是后进先出),计数减一

轻量级锁的加锁过程
- 每次线程进入同步代码块前,判断mark-word的指针是否指向自己,如果是,则直接走重入机制,添加一个lockRecord
- 如果指针不是指向自己,则通过cas替换对象mark-word中的指针使指针指向自己,如果cas的次数超过阈值(默认10次),则升级为重量级锁
- 如果mark-word还没有指向线程,说明还没被占有,复制mark-word到lockRecord,尝试通过cas将mark-record替换为指向lockRecord的指针
- 如果cas失败,则继续自旋,超出阈值,升级为重量级锁
轻量级锁的解锁过程
- 退出同步代码块时,获取当前lockRecord的值,如果是null,表示是退出重入,则走退出重入机制
- 如果不为null,证明是退出锁,通过cas将mark-word复制到对象的mark-word中
- 如果cas成功,解锁成功
- 如果cas失败,证明已经被其他线程膨胀到重量级锁,走重量级锁解锁机制
重量级锁
objectMonitor
每个对象的重量级锁指向一个独有的objectMonitor,这个对象是C++实现的,其内部维护了以下对象
- _owner:初始值为null。当有线程获取到该对象锁(即获取到该对象的Monitor对象)时,owner变量便将值置为该线程的ID;当线程释放对象锁时,owner恢复为null。owner是一个临界资源,JVM是通过CAS操作来保证其线程安全;
- cxq:竞争队列,所有请求对象锁的线程都会首先被放置于这个队列当中(单向)。__cxq 是一个临界资源,JVM通过CAS原子指令来修改cxq队列。新入节点采用头插法,置于队列头部,__cxq指针会指向该新入队的节点。
- _EntryList:__cxq队列中有资格成为候选资源的线程会被移动到该队列;该队列主要存放获取锁或者重新获取锁时被阻塞的线程(也存放竞争锁失败的线程);
- _WaitSet:调用wait()阻塞的线程会被移动至该等待队列中,等待线程组成双向循环链表,WaitSet所指向的是第一个节点;
- _recursions:synchronized是一个可重入锁,该变量用于记录锁重入(获取)的次数;
轻量级锁升级为重量级锁
升级为重量级锁的条件
- 轻量级锁状态下,自旋超出阈值(默认10次)
- 从无锁/偏向锁直接升级为重量级锁的条件:调用了object.wait()方法,则会直接升级为重量级锁!
升级过程
当触发轻量级锁升级为重量级锁时,将对象头中的mark-word复制到objectMonitor的对象头中,并将owner属性设置为当前持有锁的线程的id,其他线程便进入阻塞队列中进行阻塞等待,并进入BLOCKED状态;
重量级锁加解锁过程
1、当线程第一次调用monitorEntry执行且是重量级锁的情况下,会先进入cxq队列

2、当涉及锁的频繁竞争且需要阻塞时,需要进入entryList队列中。

3、如果线程能CAS竞争到onwer指针,就说明占有同步代码块成功, 如果CAS竞争不到,则block阻塞。

4、monitorExit退出时,会让entryList中block阻塞的线程唤醒重新竞争

5、如果调用了object.wait()方法, onwer线程会进入等待队列(注意,因为竞争失败的线程,不会进入waitSet,waitSet只服务于那些wait()方法引发的线程)

6、当调用的object.notify()或者notifyAll, 等待队列中的线程会根据qmod模式的不同,进入cxq或者进入entryList。

简要版流程如下:
