0%

synchronized锁内部实现

一、基本语义

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语言层面的类对象1
  • lockObj->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
25bit4bit1bit2bit
23bit2bit是否可偏向锁标志位
无锁HashCode对象分代年龄001
偏向锁ThreadIDEpoch对象分代年龄101
轻量级锁Lock Record指针00
重量级锁Monitor指针10
标记为GC11

3.1.2、Lock Record

锁最终被线程持有,线程使用线程栈中的Lock Record来记录本线程的锁持有情况。
Lock Record主要有两个字段:1)obj,指向对应的lockObj;2)displaced_header,释放轻量级锁时作为Mark Word的复制来源。

具体定义如下:

1
2
3
4
5
6
7
8
9
10
11
// A BasicObjectLock associates a specific Java object with a BasicLock.
// It is currently embedded in an interpreter frame.
class BasicObjectLock VALUE_OBJ_CLASS_SPEC {
private:
BasicLock _lock; // the lock, must be double word aligned
oop _obj; // object holds the lock;
};
class BasicLock VALUE_OBJ_CLASS_SPEC {
private:
volatile markOop _displaced_header;
};
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:+/-UseBiasedLockingVM参数,须知-XX:+/-UseHeavyMonitorsVM参数只是通过隐式设置-XX:+/-UseBiasedLockingVM参数来间接控制是否开启偏向锁。这个途径是静态固定的
  • kclassObj->prototype_markword上的锁标志位,为101表示开启偏向锁,否则表示关闭偏向锁。这个途径是动态可变的,具体是“开始开启偏向锁,经过偏向锁使用数据统计发现属于该kclassObj的lockObj不适合使用偏向锁,关闭偏向锁”

在创建lockObj对象分配内存时,当开启偏向锁时,Mark Word的初始状态为匿名偏向状态,否则为无锁状态。

备注:笔者不能确定-XX:+/-UseBiasedLockingVM参数和“kclassObj->prototype_markword上初始锁标志位”两者之间的关系,推测是“如果设置了-XX:-UseBiasedLockingVM参数,那么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释放锁,首先合并cxqEntryList队列,选取合并后队列首元素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
2
3
4
5
6
7
8
9
10
11
12
public class SynchronizedExample {

public synchronized void f() {
System.out.println("hello world");
}

public void g() {
synchronized (this) {
System.out.println("hello world");
}
}
}

依次执行javac SynchronizedExample.javajavap -v SynchronizedExample命令,得到以上类的JVM指令形式。

f()方法和g()方法的JVM指令片段分别如下:

f()方法的JVM指令片段:

1
2
3
4
5
6
7
8
9
10
11
12
public synchronized void f();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String hello world
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 4: 0
line 5: 8

g()方法的JVM指令片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public void g();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #3 // String hello world
9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: aload_1
13: monitorexit
14: goto 22
17: astore_2
18: aload_1
19: monitorexit
20: aload_2
21: athrow
22: return
Exception table:
from to target type
4 14 17 any
17 20 17 any
LineNumberTable:
line 8: 0
line 9: 4
line 10: 12
line 11: 22
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 17
locals = [ class SynchronizedExample, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4

在f()方法的JVM指令片段中,使用ACC_SYNCHRONIZED来标记synchronized方法(最终也会转换成锁申请和释放操作),在g()方法的JVM指令片段中,使用monitorentermonitorexit分别标记锁申请和释放。两者对应两条源代码实现路径(虽然这两条源代码实现路径相似度非常高),接下来基于后者对应的源代码实现路径来进行介绍。

接下来在JVM源码中寻找解析“monitorenter”和“monitorexit”指令的地方,有两处标的:hotspot/src/share/vm/interpreter/bytecodeInterpreter.cpp:1884hotspot/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:+UseHeavyMonitorsVM参数,意图不使用偏向锁和轻量级锁,但是由下面会执行到的方法可知,还是有可能会去申请轻量级锁的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS)
{
markOop mark = obj->mark();
assert(!mark->has_bias_pattern(), "should not see bias pattern here");

if (mark->is_neutral()) {
// Anticipate successful CAS -- the ST of the displaced mark must
// be visible <= the ST performed by the CAS.
// 申请轻量级锁
lock->set_displaced_header(mark);
if (mark == (markOop)Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
TEVENT(slow_enter
: release stacklock);
return;
}
// Fall through to inflate() ...
} else if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) {
assert(lock != mark->locker(), "must not re-lock the same lock");
assert(lock != (BasicLock*)obj->mark(), "don't relock with same BasicLock");
lock->set_displaced_header(NULL);
return;
}

...
}

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

您的支持将鼓励我继续分享!