一、基本语义
1.1、两种形式、一种锁
synchronized关键词对应synchronized锁,synchronized关键词有两种形式的用法:1)synchronized方法
;2)synchronized(Java对象obj)语句块
。
但是本质只有一种synchronized锁,该锁围绕对应的Java对象:
- 在前者情形中,如果是非静态方法,对应的Java对象为实例对象;如果是静态方法,对应的Java对象为类对象
- 在后者情形中,对应的Java对象就是obj
1.2、申请锁和释放锁
申请锁:“synchronized方法
进入”或者“synchronized(Java对象obj)语句块
进入”。
释放锁:“synchronized方法
退出”或者“synchronized(Java对象obj)语句块
退出”。
1.3、锁分类
从不同维度进行锁分类,synchronized锁属于:
- 悲观锁
- 排他锁
- 阻塞锁
- 可重入锁
- 非公平锁
二、内部实现之4种锁状态
接下来所作内部实现讨论所基于操作系统为Linux。
最初的synchronized锁内部实现中,只有“无锁”和“有锁”两种锁状态,“有锁”的实现基于Monitor(参见“3.1.3、Monitor”小节)
,但是存在“Monitor操作使用代价高昂,有些锁使用场景不必须使用Monitor”的问题,比如“一些工具类方法为提供线程安全性会加上synchronized关键词,但在实际应用中却只是在单线程语境下使用”。
因此,为提升synchronized锁性能,引入“偏向锁”和“轻量级锁”两种锁状态,将原来的“有锁”锁状态重命名为“重量级锁”,故共有4个锁状态:无锁、偏向锁、轻量级锁、重量级锁。4个锁状态适用的锁使用场景如表1。
表1
状态 | 适用锁使用场景 |
---|---|
无锁 | / |
偏向锁 | 一个线程竞争锁 |
轻量级锁 | 多个线程交替竞争锁(线程T1申请锁,释放锁后,线程T2申请锁,释放锁,…) |
重量级锁 | 多个线程无序竞争锁(即锁竞争非常激烈) |
偏向锁、轻量级锁、重量级锁的锁分类如表2。
表2
锁类别\锁分类 | 悲观锁 | 排他锁 | 阻塞锁 | 可重入锁 | 非公平锁 |
---|---|---|---|---|---|
偏向锁 | 是 | 是 | 否 | 是 | 无意义 |
轻量级锁 | 是 | 是 | 否 | 是 | 无意义 |
重量级锁 | 是 | 是 | 是 | 是 | 是 |
备注:
- 根据“公平锁”和“非公平锁”的定义,由于“偏向锁”和“轻量级锁”不涉及到等待队列,故关于“偏向锁/轻量级锁是否为公平锁/非公平锁”的问题没有意义
三、内部实现之OpenJDK源代码阅读
这里的OpenJDK源代码指的是OpenJDK 8源代码。
在继续讨论之前,首先定义一些名词:
lockObj
,表示synchronized锁所围绕的Java对象,这个Java对象可以是实例对象,也可以是类对象1。比如“synchronized(obj){}
中的obj”,“synchronized(A.class){}
中的类A所对应的对象”kclassObj
,表示lockObj所对应的类对象2,这个类对象2属于VM进程内部实现层面,对Java语言层面不可见,并不是属于Java语言层面的类对象1lockObj->markword
,表示lockObj这个Java对象的Mark Word字段kclassObj->prototype_markword
,表示kclassObj这个类对象2的原型Mark Word字段
另外需要注意的是,本文注重描述主体脉络,一些具体细节并未涉及到。
3.1、数据结构
3.1.1、Mark Word
根据synchronized关键词的用法可知,实例对象和类对象都可作为锁对象,锁对象的Mark Word字段被设计用于保存相应的锁状态信息。
Mark Word可分为32位和64位,接下来以32位Mark Word为例。
32位Mark Word字段保存锁状态信息的方案设计如表3所示。
表3
对象状态 | |||||
---|---|---|---|---|---|
Mark Word | |||||
25bit | 4bit | 1bit | 2bit | ||
23bit | 2bit | 是否可偏向 | 锁标志位 | ||
无锁 | HashCode | 对象分代年龄 | 0 | 01 | |
偏向锁 | ThreadID | Epoch | 对象分代年龄 | 1 | 01 |
轻量级锁 | Lock Record指针 | 00 | |||
重量级锁 | Monitor指针 | 10 | |||
标记为GC | 无 | 11 |
3.1.2、Lock Record
锁最终被线程持有,线程使用线程栈中的Lock Record来记录本线程的锁持有情况。
Lock Record主要有两个字段:1)obj,指向对应的lockObj;2)displaced_header,释放轻量级锁时作为Mark Word的复制来源。
具体定义如下:
1 | // A BasicObjectLock associates a specific Java object with a BasicLock. |
3.1.2.1、创建
每次线程T0申请lockObj的锁时,都会在T0的线程栈中申请一个用于lockObj的Lock Record,创建过程描述如下:
0. 从T0线程栈的空闲Lock Record列表中取“上限下内存地址最高的Lock Record”,“上限下内存地址最高的Lock Record”的含义是“如果已经存在obj指向lockObj的Lock Record,其中最低内存地址为A0,那么下次再申请用于lockObj的Lock Record时,内存地址上限为A0;如果未存在obj指向lockObj的Lock Record,那么下次再申请用于lockObj的Lock Record时,内存地址上限为空闲Lock Record列表的本身内存地址上限”
0. 假设拿到的空Lock Record为LR0,那么LR0的obj字段指向lockObj
0. LR0的displaced_header字段填充分为3种情形:
- 申请的是偏向锁,那么displaced_header的值不管是首次申请成功,还是锁重入,始终为NULL
- 申请的是轻量级锁,如果是首次申请成功,设为该轻量级锁释放后Mark Word需要还原到的值;如果是锁重入,那么displaced_header的值为NULL
- 申请的是重量级锁,那么displaced_header的值不管是首次申请成功,还是锁重入,都为某个特定的值,这个特定的值没有任何语义,只是为了标识持有锁状态为重量级锁
3.1.2.2、销毁
每次线程T0释放lockObj的锁时,在T0线程栈的Lock Record列表中找到属于lockObj的最低内存地址的Lock Record进行销毁,即将其obj字段置为NULL。
3.1.2.3、Lock Record的意义
线程T0线程栈中属于lockObj的Lock Record列表是能表达T0对lockObj持有状态
和持有次数
最准确的数据。
\ | 持有状态 | 持有次数 |
---|---|---|
偏向锁 | 当T0释放偏向锁且为最后一次释放(即不为偏向锁锁重入的释放)后,相应Mark Word上的ThreadID并未更新,此时Mark Word的ThreadID表达持有状态是不准确的,只有此时最后一个Lock Record也被销毁才能准确表达这个锁持有状态的变化 | 只能通过Lock Record来表达 |
轻量级锁 | 通过Mark Word上Lock Record指针准确表达:当为NULL,表示未持有锁;当不为NULL,Lock Record所在线程持有锁 | 只能通过Lock Record来表达 |
重量级锁 | 通过Mark Word上Monitor指针准确表达:当为NULL,表示未持有锁;当不为NULL,所指向Monitor对象的owner字段直接或者间接表征(此时,owner的值为指向Lock Record的指针)持有线程,当然如果owner字段为NULL,同样表示未持有锁 | 当升级到重量级锁时,如果原来并没有“偏向锁和轻量级锁”的锁重入,此时recursions字段能表达锁重入次数,否则recursions字段不能表达锁重入次数,只有Lock Record表达锁重入次数恒久保持准确 |
3.1.3、Monitor
即重量级锁,对应的数据结构为ObjectMonitor
,其文件路径是hotspot/src/share/vm/runtime/objectMonitor.hpp:77
。
3.2、VM参数
在synchronized锁的源代码实现中有两个VM参数值得说明:
-XX:+/-UseBiasedLocking
:是否开启/关闭偏向锁-XX:+/-UseHeavyMonitors
:是否只使用重量级锁,而不使用偏向锁和轻量级锁,-XX:+UseHeavyMonitors
隐式包含了-XX:-UseBiasedLocking
3.3、锁申请和锁释放
关于“锁申请”和“锁释放”这里作简单说明,详见源代码。
1、锁申请
线程T1持有锁,线程T2竞争申请锁,且过程中T1不释放锁,那么:
- 当锁状态为重量级锁时,T2阻塞
- 当锁状态为偏向锁或者轻量级锁,锁最终升级为重量级锁(可能有中间状态的轻量级锁),锁升级后仍由T1持有(包括中间状态的轻量级锁),T2阻塞。锁升级后仍由T1持有是显而易见的,否则就是“在线程T1持有锁的过程中,允许锁的持有权被抢断式转移”,这违背happens-before规则——“对一个锁的解锁,happens-before于随后对这个锁的加锁”
2、锁释放
锁释放分为两种情形“重入锁释放”和“非重入锁释放”:
- 重入锁释放。偏向锁、轻量级锁、重量级锁的释放操作正常无特别之处
- 非重入锁释放。1)偏向锁释放时,不会被置为匿名偏向状态,即“Mark Word的ThreadID域值不会被置为全0值,而是保持原值”,考虑到“一个线程竞争锁”的偏向锁使用场景(T0持有偏向锁,释放,稍后再度申请持有时,无需CAS操作修改Mark Word,直接持有偏向锁成功),这个设计是合理的;2)轻量级锁释放时,会使用
displaced_header
重置Mark Word,且displaced_header
值必为无锁状态;3)重量级锁释放时,不会回退到无锁,考虑到“升级到重量级锁意味着锁竞争十分激烈,现在回退后后续仍然有很大概率继续升级”,这个设计是合理的
3.4、偏向锁
3.4.1、匿名偏向和非匿名偏向
匿名偏向:表示开启偏向锁,但是还未偏向具体线程,此时Mark Word中的ThreadID域值为全0。
非匿名偏向:即正常偏向,表示开启偏向锁,也已经偏向到具体线程,此时Mark Word中的ThreadID域值为该具体线程的ID。
3.4.2、控制是否开启偏向锁
有两个途径控制是否开启偏向锁:
-XX:+/-UseBiasedLocking
VM参数,须知-XX:+/-UseHeavyMonitors
VM参数只是通过隐式设置-XX:+/-UseBiasedLocking
VM参数来间接控制是否开启偏向锁。这个途径是静态固定的kclassObj->prototype_markword
上的锁标志位,为101
表示开启偏向锁,否则表示关闭偏向锁。这个途径是动态可变的,具体是“开始开启偏向锁,经过偏向锁使用数据统计发现属于该kclassObj的lockObj不适合使用偏向锁,关闭偏向锁”
在创建lockObj对象分配内存时,当开启偏向锁时,Mark Word的初始状态为匿名偏向状态,否则为无锁状态。
备注:笔者不能确定-XX:+/-UseBiasedLocking
VM参数和“kclassObj->prototype_markword上初始锁标志位”两者之间的关系,推测是“如果设置了-XX:-UseBiasedLocking
VM参数,那么kclassObj->prototype_markword上的初始锁标志位不为101”
3.4.3、重偏向和撤销
首先介绍两个概念:
- VM线程,即VM Thread,该线程会在进入安全点时,从VMOperationQueue队列中取出需要在安全点执行的操作去执行,比如GC操作
- 安全点,即Safe Point,其代表一个状态,在该状态下所有线程都是暂停的,除了VM线程
1、重偏向
重偏向:lockObj->markword
处于正常偏向状态,且ThreadID域的值表征线程T1,但实际上T1已释放掉该偏向锁,那么如果此时T2申请该偏向锁,则可成功,ThreadID域的值置为线程T2,这就是偏向锁的重偏向。
重偏向过程的重点在于“如何知晓T1已释放掉偏向锁?”。因为根据“3.3、锁申请和锁释放”小节内容可知,偏向锁释放时Mark Word的ThreadID域值不会被置为全0值,而是保持原值
,有两种策略:1)主动侦测线程T1是否仍然持有偏向锁;2)查看lockObj->markword
中Epoch域值与对应的kclassObj->prototype_markword
中Epoch域值是否不一致,即Epoch阈值是否过期,如果过期,则表征T1已释放掉该偏向锁。这个不一致状态是由“批量重偏向”过程设置的
上述提及的“批量重偏向”过程命名有一定歧义性,因为本质上并没有完成重偏向,而只是为后续完成重偏向作了预处理。另外,关于这个“批量重偏向”过程有4点说明:
- 触发条件是基于统计数据,满足设定的批量重偏向阈值时进行
- 方法名是
bulk_revoke_or_rebias_at_safepoint()
- 执行时机是:在安全点,VM线程执行
- 具体过程是:所传入lockObj对应的
kclassObj->prototype_markword
中的Epoch域值+1,遍历所有存活线程,所持有锁对应lockObj0的lockObj0->markword
中的Epoch域值同步+1,相对的,未被存活线程持有锁对应lockObj0的lockObj0->markword
中的Epoch域值未被同步+1。需要注意的是,上述lockObj0与所传入lockObj属于同一个kclassObj
2、撤销
撤销:从“已偏向锁”状态置为“无锁、匿名偏向锁、轻量级锁”。
关于“批量撤销”有4点说明:
- 触发条件是基于统计数据,满足设定的批量撤销阈值时进行
- 方法名是
bulk_revoke_or_rebias_at_safepoint()
- 执行时机是:在安全点,VM线程执行
- 具体过程是:所传入lockObj对应的
kclassObj->prototype_markword
中的锁标志位设为“关闭偏向锁”,遍历所有存活线程,如果所持有锁对应lockObj与所传入lockObj属于同一个kclassObj
时,撤销该偏向锁
3.5、轻量级锁
相对最简单的锁形态。
所指向Lock Record的displaced_header
字段存放释放该轻量级锁后需要还原到的Mark Word值,是一个无锁状态的值。
3.6、重量级锁
接下来对重量级锁的申请和释放过程作进一步细化描述,首先须知的是对应于重量级锁的数据结构ObjectMonitor内有两个相关等待队列ContentionList(简称为cxq)
和EntryList
,其内部元素对象数据结构为ObjectWaiter。
现假定线程T0已获得重量级锁M0:
- 后续一系列线程T1,T2,T3,…申请锁,首先尝试直接竞争锁,如果成功直接返回,否则基于线程本身构造一个ObjectWaiter对象加入cxq队列,调用阻塞方法阻塞线程本身,这里的阻塞分为两种情形:
- 无超时阻塞,只会被唤醒方法唤醒,唤醒后尝试竞争锁,如果成功直接返回,否则继续下一轮无超时阻塞
- 超时阻塞,除了会被唤醒方法唤醒外,还会在设定超时时间到期后自动唤醒,唤醒后尝试竞争锁,如果成功直接返回,否则继续下一轮无超时阻塞。被选中的超时阻塞线程称为“_Responsible”线程
- T0释放锁,首先合并
cxq
和EntryList
队列,选取合并后队列首元素ObjectWaiter,唤醒对应的线程,该选中线程又被称为“假定继承人(称为假定是因为不一定能竞争锁成功)”,结合上面描述可知,这个假定继承人可能是“_Responsible”线程,也可能不是“_Responsible”线程 - 根据上面描述可知,极端情况下同时竞争锁的线程最多有3个来源:申请锁的新线程、无超时阻塞线程被唤醒、超时阻塞线程设定超时时间到期后自动唤醒
根据上面叙述,重量级锁为非公平锁。
3.7、HashCode计算
根据表3可知,在无锁状态时,Mark Word中有个HashCode字段,该字段用来保存第一次计算原生HashCode值(调用未被覆盖的hashCode()方法,即lockObj.hashCode()
,覆盖后的hashCode()方法计算值无需保存;或者调用System.identityHashCode()方法,即System.identityHashCode(lockObj)
)的结果值,避免后续重复计算。
在“偏向锁”或者“轻量级锁”状态时,没有字段存放原生HashCode值,故此种情形下,为了能够存放原生HashCode值,也会进行锁升级,即最终升级到重量级锁,在重量级锁对应的ObjectMonitor数据结构中有个字段可用来存放该原生HashCode值。
3.8、锁状态转移图
锁状态示意图如图1。
图1
关于锁状态转移有以下几点说明:
- 关于“锁申请、锁释放、偏向锁的重偏向和撤销”已经在上文中涉及。这里主要说明下“重量级锁->无锁(即图示中
重量级锁降级[1]
)”的状态转移过程,根据“3.3、锁申请和锁释放”可知,释放重量级锁时,该过程并不会发生,但实际上该过程是客观存在的,对应的具体方法路径是“hotspot/src/share/vm/runtime/synchronizer.cpp”源文件内的deflate_idle_monitors()
方法,而对于该方法的具体调用条件、时机等,笔者并不是很清楚。关于这点,《Java并发编程的艺术》一书中作者说到“重量级锁没有锁降级机制”应该是个错误 - “合法的锁升级”包含:|无锁、偏向锁| -> |轻量级锁|,|无锁、轻量级锁| -> |重量级锁|;“合法的锁降级”包含:|轻量级锁、重量级锁| -> |无锁|
3.9、源代码阅读
synchronized锁有两种使用形式,对应着两条源代码实现路径,synchronized锁的两种使用示例代码如下:
1 | public class SynchronizedExample { |
依次执行javac SynchronizedExample.java
和javap -v SynchronizedExample
命令,得到以上类的JVM指令形式。
f()方法和g()方法的JVM指令片段分别如下:
f()方法的JVM指令片段:
1 | public synchronized void f(); |
g()方法的JVM指令片段:
1 | public void g(); |
在f()方法的JVM指令片段中,使用ACC_SYNCHRONIZED
来标记synchronized方法(最终也会转换成锁申请和释放操作),在g()方法的JVM指令片段中,使用monitorenter
和monitorexit
分别标记锁申请和释放。两者对应两条源代码实现路径(虽然这两条源代码实现路径相似度非常高),接下来基于后者对应的源代码实现路径来进行介绍。
接下来在JVM源码中寻找解析“monitorenter”和“monitorexit”指令的地方,有两处标的:hotspot/src/share/vm/interpreter/bytecodeInterpreter.cpp:1884
和hotspot/src/cpu/x86/vm/templateTable_x86_64.cpp:3668
。第一处标的是“字节码解释器”实现形式,用C++语言实现,其优点是“实现相对简单且容易理解”,缺点是“执行慢”;第二处标的是“模板解释器”实现形式,用汇编语言实现,其优点是“执行快”,缺点是“不好理解”。两者的实现是等价的,为了便于理解,跟踪阅读第一处标的。
另外,在源代码中有一个出现频率非常高的CAS方法cmpxchg_ptr(newV, addr, oldV)
,这个CAS方法是属于C++语言层面的,底层会映射到处理器的CAS指令,其含义是“如果内存地址addr当下值等于oldV,则其值原子更新成newV,否则更新失败,方法恒返回内存地址当下值”。
3.9.1、申请锁
方法/语句块 | 方法/语句块对应的流程图 |
---|---|
“src/share/vm/interpreter/bytecodeInterpreter.cpp”源文件: CASE(_monitorenter) |
|
“src/share/vm/interpreter/interpreterRuntime.cpp”源文件: InterpreterRuntime::monitorenter() |
|
“src/share/vm/runtime/synchronizer.cpp”源文件: ObjectSynchronizer::slow_enter() |
|
“src/share/vm/runtime/biasedLocking.cpp”源文件: BiasedLocking::Condition bulk_revoke_or_rebias_at_safepoint() |
|
“src/share/vm/runtime/biasedLocking.cpp”源文件: BiasedLocking::Condition revoke_bias() |
|
“src/share/vm/runtime/synchronizer.cpp”源文件: ObjectSynchronizer::inflate() |
|
“src/share/vm/runtime/objectMonitor.cpp”源文件: ObjectMonitor::enter() |
3.9.2、释放锁
方法/语句块 | 方法/语句块对应的流程图 |
---|---|
“src/share/vm/interpreter/bytecodeInterpreter.cpp”源文件: CASE(_monitorexit) |
|
“src/share/vm/runtime/objectMonitor.cpp”源文件: ObjectMonitor::exit() |
四、其他
4.1、一个Bug
Bug描述:配置了-XX:+UseHeavyMonitors
VM参数,意图不使用偏向锁和轻量级锁,但是由下面会执行到的方法可知,还是有可能会去申请轻量级锁的。
1 | void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) |
4.2、阻塞
对于synchronized锁来说,竞争不到锁阻塞,本质是对于重量级锁状态而言,如果是无锁、偏向锁和轻量级锁状态,则最终会升级成重量级锁再阻塞。
参考文献:
[1]https://github.com/farmerjohngit/myblog/issues/12
[2]https://github.com/farmerjohngit/myblog/issues/13
[3]https://github.com/farmerjohngit/myblog/issues/14
[4]https://github.com/farmerjohngit/myblog/issues/15