引言
synchronized 在 JVM 底层是一项非常复杂的技术,涉及了非常多的内容,包括但不局限于:对象头、偏向锁、轻量级锁、重量级锁、锁偏向、锁膨胀、锁撤销、CAS、安全点等技术,并且这些技术在 JDK 不同版本中的实现也大相径庭。
synchronized 是一种用于修饰同步代码块或方法的关键字。当一个线程进入被 synchronized 修饰的代码块或方法时,会先尝试获取括号中对象(对于同步方法则是当前类的实例)的对象锁。在 JDK 1.6 及之后的版本,synchronized 锁具有锁升级的机制,分为偏向锁、轻量级锁和重量级锁。
synchronized 的存在为绝大多数并发编程提供了便捷的解决方案。它将我们从繁琐复杂的线程管理工作中解脱出来,使我们能够专注于程序的核心功能。然而,尽管 synchronized 在日常使用中简单方便,但它的内部实现及其升级过程却蕴藏着极大的复杂性和精妙之处。在这本期文章中,以 JDK17u 为例,我将详细记录对 synchronized 底层机制及其锁升级过程的探究和学习过程。
基础应用
synchronized 可以使用在方法和代码块中,使用的方式不同锁代表的含义也不同,包括:
- synchronized 方法:
synchronized void methodB()
- 静态 synchronized 方法:
static synchronized void methodA()
- 对象 synchronized 块:
synchronized(this) {}
- 类 synchronized 块:
synchronized(Test.class) {}
对上述四点的总结就是:
- 在使用 synchronized 关键字中锁主要分为两类:一种是对象锁,另一种类锁
- 对象锁:
synchronized void methodB()
,synchronized(this) {}
- 类锁:
static synchronized void methodA()
,synchronized(Test.class) {}
- 对象锁:
- 对象锁:同一对象持有锁,相同对象等待,其他对象不受影响;不同对象持有锁,互不影响
- 类锁:类锁时,只要该类的对象持有锁,无论是否为同一对象访问静态同步方法时都等待,访问非静态同步方法不受影响
- 对象锁和类锁互相不影响,一个线程拿到了对象锁,并不会影响其他线程去获取类锁,反之亦然
方法锁
方法锁包括普通方法锁 synchronized void methodB()
和静态方法锁 static synchronized void methodA()
,以下面这段代码为例:
public class SyncTest {
public static void main(String[] args) {
SyncTest syncTest = new SyncTest();
new Thread(SyncTest::methodA, "Thread 01 ").start();
new Thread(syncTest::methodB, "Thread 02 ").start();
}
// 静态方法锁
static synchronized void methodA() {
System.out.println(Thread.currentThread().getName() + "start");
try {
System.out.println(Thread.currentThread().getName() + "sleep");
Thread.sleep(500);
} catch (InterruptedException ignored) {
}
System.out.println(Thread.currentThread().getName() + "end");
}
// 普通方法锁
synchronized void methodB() {
System.out.println(Thread.currentThread().getName() + "start");
try {
System.out.println(Thread.currentThread().getName() + "sleep");
Thread.sleep(300);
} catch (InterruptedException ignored) {
}
System.out.println(Thread.currentThread().getName() + "end");
}
}
在前面已经总结了“对象锁和类锁互相不影响”的结论,所以理论上 methodA()
的执行与 methodB()
的执行是互不干扰的,这也就可以推断出这两个线程的输出一定是穿插执行的。经过多次运行观察结果可以验证这个猜想。
类锁与对象锁
无论使用哪个语言哪个框架来设计高并发程序,锁的概念是逃不掉的,Java 中的锁的概念多而复杂,在 synchronized 中就涉及了将近超过 6 种锁。在前面的粗略的总结中已经提到了“对象锁和类锁互相不影响”,也已经通过方法锁进行了初步的认知,但是对于更细致的 synchronized 块来说还远远不够。通过以下一个简单的对 synchronized 块的使用案例来进一步解释这个结论:
public class SyncTest {
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
SyncTest syncTest = new SyncTest();
ThreadPoolExecutor pool = new ThreadPoolExecutor(
5, 5, 5000, TimeUnit.MILLISECONDS,
new SynchronousQueue<>(),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
for (int i = 0; i < 5; i++) {
pool.submit(syncTest::methodA);
}
pool.shutdown();
while (!pool.isTerminated()) {}
System.out.println("execution consumes " + (System.currentTimeMillis() - startTime) + " ms");
}
void methodA() {
synchronized (this) {
// 对象锁块
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("Thread-methodA");
}
}
}
这里重点突出了对象锁块的方法,按之前的解释改程序的执行耗时一定是在 左右(这里就不放截图了因为结果是很明显的)。这相当于 methodA()
方法被串行化了,接下来我们加入类锁方法:
public class SyncTest {
public static void main(String[] args) {
// ...
ThreadPoolExecutor pool = new ThreadPoolExecutor(
10, 10, 5000, TimeUnit.MILLISECONDS,
new SynchronousQueue<>(),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
for (int i = 0; i < 5; i++) {
pool.submit(syncTest::methodA);
pool.submit(syncTest::methodB);
}
// ...
}
void methodA() {
// ...
}
void methodB() {
synchronized (SyncTest.class) {
// 类锁块
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("Thread-methodB");
}
}
}
在这个案例中,methodA
采用了对象锁,而 methodB
采用了类锁,虽然在线程池中提交的任务数量多了一倍但是程序耗费的时间仍旧是造 左右。也就是类锁和对象锁被分开串行化了(具体的可以看上面的时序图)。
四种锁状态
到目前为止已经区分了 synchronized 的类锁和对象锁,也已经知道 synchronized 的本质其实就是将多线程任务进行了串行化。synchronized 一共拥有四种锁状态:无锁、偏向锁、轻量级锁、重量级锁:
- 无锁:这是 synchronized 的默认状态,表示没有线程获取到锁
- 偏向锁:如果一个线程已经获取了锁,然后又再次请求相同的锁,此时 JVM 就不会再进行锁获取的操作,而是将锁标记为偏向锁,这样可以减少不必要的锁获取操作。需要注意的是,JVM 启动时,偏向锁默认是延迟启动的,在启动后一段时间内才会启用偏向锁
- 轻量级锁:如果有多个线程竞争锁,但是锁的竞争并不激烈,也就是说锁大部分时间都是被某个线程持有的,这种情况下,JVM 会将锁标记为轻量级锁。轻量级锁能够在没有大量竞争的情况下提高性能
- 重量级锁:如果有多个线程竞争锁,锁的竞争非常激烈,这种情况下,JVM 会将锁升级为重量级锁。重量级锁会导致竞争锁的线程进入 BLOCKED 状态,只有等待获取锁的线程释放锁后,其他线程才能获取到锁
synchronized的这四种锁状态是在JVM层面自动进行的,深入理解synchronized锁升级过程可以帮助开发者更好地对程序进行性能调优或设计更好的并发控制策略。
锁的升降
synchronized 锁升降的过程是非常复杂繁琐的,涉及到了 Java 对象、线程以及 JVM,抛开中间一些关于安全点轮询、CAS 操作,并模拟一个“当多个线程交替尝试获取同一个对象的锁”的场景,根据 synchronized 的性质可以得到一个简单的时序图:
sequenceDiagram participant Object as Java Object participant Thread1 as Thread 1 participant Thread2 as Thread 2 participant JVM as JVM Note over Object, JVM: 无锁 Thread1->>Object: 尝试获取锁 Object->>JVM: Thread 1向JVM请求这个对象的偏向锁 JVM-->>Object: 请求JVM为Thread 1提供偏向锁 Object-->>Thread1: Object偏向锁指向Thread 1 Note over Object, JVM: Thread 1持有偏向锁 Thread2->>Object: 尝试获取锁 Object->>JVM: 请求JVM撤销偏向锁 JVM-->>Object: 确认偏向锁的撤销请求 Object-->>Thread2: Object阻塞请求 Note over Object, JVM: Thread 1释放偏向锁降为无锁 Thread1->>Object: 再次尝试获取锁 Object->>JVM: 请求JVM将锁膨胀为轻量级锁 JVM-->>Object: 确认锁膨胀事件 Object-->>Thread1: Object允许请求 Note over Object, JVM: Thread 2持有轻量级锁 Thread2->>Object: 再次尝试获取锁 Object->>JVM: 请求JVM将锁膨胀为重量级锁 JVM-->>Object: 确认锁膨胀事件 Object-->>Thread2: Object阻塞请求 Note over Object, JVM: Thread 3持有重量级锁
在这个图中,Java 对象从无锁状态开始,首次由线程 1 获取,升级为偏向锁。然后当线程 2 试图获取锁时,偏向锁被撤销,变回无锁状态。再次由线程 1 获取时,锁被升级为轻量级锁。最后,当线程 2 再次试图获取锁时,锁被升级为重量级锁。这个过程非常抽象,具体到代码层面是:
public class SynchronizedTest {
private static final Object LOCK = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (LOCK) {
System.out.println("Thread 1 acquired the lock.");
try {
// 线程睡眠一段时间来模拟一些复杂的处理过程
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("Thread 1 released the lock.");
}
});
Thread thread2 = new Thread(() -> {
synchronized (LOCK) {
System.out.println("Thread 2 acquired the lock.");
try {
// 线程睡眠一段时间来模拟一些复杂的处理过程
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("Thread 2 released the lock.");
}
});
thread1.start();
// 睡眠一段时间以确保线程1先获取锁
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
thread2.start();
// 睡眠一段时间以确保线程2尝试获取锁,但被阻塞
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// 在此之后,线程1和线程2会按照未知的顺序交替获取和释放锁
}
}
这一块时序图所基于的理论是“当尝试获取锁失败时,synchronized 才会发生升级”,synchronized 的锁升级过程是一种优化策略,其目的是在不同的竞争条件下尽可能减少系统的开销。锁的升级一般发生在下述条件(在锁升级前会先进行锁的膨胀):
- 无锁到偏向锁: 如果一个线程第一次访问一个 synchronized 块,JVM 将会在对象头上记录这个线程 ID,然后线程将持有偏向锁。这种锁偏向于第一个访问锁的线程,如果下次还是这个线程进入锁相关的同步块,就不需要执行任何同步操作。这个升级过程在程序启动几秒后自动开启。
- 偏向锁到轻量级锁: 如果一个新的线程尝试获取已经被偏向锁保护的对象,JVM 首先会检查对象头中的线程 ID 是否是当前线程。如果不是,JVM 将尝试撤销偏向锁,并将其升级为轻量级锁。如果撤销偏向锁的过程中原线程仍在活动,或者有其他线程尝试获取这个锁,那么可能直接升级为重量级锁。
- 轻量级锁到重量级锁: 如果一个线程尝试获取轻量级锁,但发现该锁已经被其他线程持有,那么它就会进入自旋状态,尝试不断获取锁。如果自旋超过一定次数(具体次数取决于JVM),锁将会被升级为重量级锁。此时,如果还有其他线程尝试获取这个锁,它们将会被阻塞,直到锁被释放。
底层实现
synchronized 锁的实现似乎是非常复杂的,一般开发者并不需要过于深入的了解,如果要想深入研究 synchronized 就必须要深入 Hotspot 源码,以 JDK17u 为例,先对这些有锁状态、上锁过程、锁安全检查的实现位置进行定位:
- 偏向锁:
src/hotspot/share/runtime/biasedLocking.cpp
- 轻量级锁和重量级锁:
src/hotspot/share/runtime/synchronizer.cpp
- 锁的膨胀和撤销:
src/hotspot/share/runtime/synchronizer.cpp
- 安全点:
src/hotspot/share/runtime/safepoint.cpp
- 安全点轮询:
src/hotspot/share/runtime/safepointMechanism.cpp
对象头
在开始逐个分析前需要先知道一些关于对象头的理论基础。synchronized 是使用对象的对象头中的一部分标志位来实现的,标志位有三位,可以表示四种状态:偏向锁、轻量级锁、重量级锁和无锁状态。对象头的概念非常简单这里不再赘述。
操作对象头并尝试加锁的逻辑位于 synchronizer.cpp
文件中,下面是去掉官方注释并加入了我本人理解注释后的源码:
void ObjectSynchronizer::enter(Handle obj, BasicLock* lock, JavaThread* current) {
if (obj->klass()->is_value_based()) {
handle_sync_on_value_based_class(obj, current);
}
if (UseBiasedLocking) { // 是否已经是偏向锁
BiasedLocking::revoke(current, obj); // 尝试撤销偏向锁
}
markWord mark = obj->mark(); // 获取对象头的mark word,存储锁信息的地方
assert(!mark.has_bias_pattern(), "should not see bias pattern here");
if (mark.is_neutral()) { // 锁是否处于无锁状态
lock->set_displaced_header(mark);
// 尝试进行CAS操作将对象头设置为轻量级锁
if (mark == obj()->cas_set_mark(markWord::from_pointer(lock), mark)) {
return;
}
} else if (mark.has_locker() &&
current->is_lock_owned((address)mark.locker())) {
// 对象头的状态表示已经是被锁定的,并且锁的拥有者是当前线程
assert(lock != mark.locker(), "must not re-lock the same lock");
assert(lock != (BasicLock*)obj->mark().value(), "don't relock with same BasicLock");
lock->set_displaced_header(markWord::from_pointer(NULL));
return;
}
lock->set_displaced_header(markWord::unused_mark());
// 如果以上都不满足,则会进入重量级锁的处理流程
while (true) {
// 将锁升级为重量级锁
ObjectMonitor* monitor = inflate(current, obj(), inflate_cause_monitor_enter);
// 调用ObjectMonitor的enter函数尝试获取重量级锁
if (monitor->enter(current)) {
return;
}
}
}
enter
函数是在 JVM 中用于线程尝试获取对象锁的处理逻辑,并同时处理了偏向锁,轻量级锁以及重量级锁的逻辑。下面对这段代码进行拆解分析。
偏向锁
在 JDK 15 以后,Java 官方就开始废弃了偏向锁的功能,直到 JDK17u 中被完全移除。这使得 Java 不会再默认开启启用偏向锁选项了。下面将先针对 JDK 1.8 进行偏向锁的研究,然后再来分析为什么要移除偏向锁。
偏向锁在对象头中还使用了两位额外的标志位,一位表示是否可偏向,一位表示是否已经偏向。在 Hotspot 中,偏向锁是默认开启的,可通过启动参数 -XX:+UseBiasedLocking
和 -XX:-UseBiasedLocking
来控制。对象实例化后,默认处于可偏向状态,当一个线程首次获取这个对象的偏向锁时,会将偏向锁的线程 ID 设置为该线程的 ID,表示锁已经偏向这个线程。
在 JDK 1.8 中 BiasedLocking::revoke_and_rebias
函数主要负责这一过程,它会先尝试撤销偏向锁再进行锁偏向:
bool BiasedLocking::revoke_and_rebias(Handle obj, bool is_bulk, JavaThread* requesting_thread) {
markWord mark = obj->mark();
// 检查对象的markWord来确定当前的锁状态
assert(!mark.has_bias_pattern() || (mark.age() == markWord::max_age), "should have been aged to prevent inflation");
markWord biased_prototype = markWord::biased_locking_prototype()->set_age(mark.age());
// 基于这个状态来决定如何撤销现有的锁并设置新的偏向锁
if (mark.is_neutral()) {
obj->set_mark(biased_prototype);
return true;
}
if (mark.has_bias_pattern()) {
if (mark.biased_locker() == requesting_thread) {
return false;
}
}
// ...
obj->set_mark(biased_prototype);
return true;
}
在 JDK17u 中处理偏向锁的主要函数分别是 BiasedLocking::revoke_at_safepoint
和 BiasedLocking::revoke
,从函数名就会发现这两个函数不存在加持偏向锁的功能,它们也只是负责对具有偏向锁模式的对象进行偏向锁的撤销操作:
BiasedLocking::revoke
函数会遍历传入的所有对象,并检查每一个对象的markWord
是否具有偏向锁模式。如果有,那么它将调用walk_stack_and_revoke
函数撤销这个偏向锁。最后,如果有任何偏向锁被撤销,它还会调用clean_up_cached_monitor_info
函数来清理缓存中的monitor信息。BiasedLocking::revoke_at_safepoint
函数也是进行偏向锁的撤销,但是这个函数在 JVM 的安全点(safepoint)被调用,也就是当所有 Java 线程都被暂停时。函数首先检查偏向锁撤销的启发式规则,然后根据启发式规则来决定是进行单个撤销(single_revoke_at_safepoint
),还是批量撤销或者重偏向(bulk_revoke_at_safepoint
)。在偏向锁被撤销之后,也会清理缓存中的 monitor 信息。
既然已经在 JDK 17 中完全废弃了偏向锁那么就没有再研究JDK17u底层锁偏向实现原理的意义了,再来看看为什么 Java 决定在 JDK 15 开始废除偏向锁:
- 在很早以前,Java 应用通常使用的都是 HashTable、Vector 等比较老的集合库,这类集合库大量使用了 synchronized 来保证线程安全,但是现在这些库现如今都已经不被官方推荐使用了,因为性能更好功能更丰富的集合类在 JDK 1.2 就被推出了
- 偏向锁是 Java HotSpot 虚拟机为优化多线程锁竞争引入的一个机制,但在实际应用中,偏向锁并不总是能够提供预期的性能优化
- 具体来说,偏向锁的设计主要是为了优化那些基本上没有真实竞争的锁,通过省去一些原本会在无竞争情况下产生的无谓消耗,来提高系统的整体性能。然而,在某些具有锁竞争的场景下,偏向锁可能会引发额外的开销。例如,尝试获取偏向锁的线程不是原始偏向线程时,系统需要进行锁撤销,这会导致额外的系统消耗
- 伴随现代多核处理器的并行能力不断增强,无锁编程模型以及其它并发工具和框架的使用也越来越普遍。这些因素使得偏向锁的性能优势不再那么明显。同时,偏向锁也增加了JVM的复杂性,并可能导致一些难以预见和排查的问题
- 在权衡利弊后,Java 决定在 JDK 15 中废除了偏向锁。具体的废除决定和理由可以在 JEP 374 中找到,在这个提案中还提到了与 HotSpot 相关的一点:
- 偏向锁为整个「同步子系统」引入了大量的复杂度,并且这些复杂度也入侵到了 HotSpot 的其它组件
- 这导致了系统代码难以理解,难以进行大的设计变更,降低了子系统的演进能力
轻量级锁
重新回到对象头中的源码,obj()->cas_set_mark(markWord::from_pointer(lock), mark)
函数实现了轻量级锁的设置。从函数名中不难发现这里使用了 CAS 操作来实现锁升级,进入这里的源码:
markWord oopDesc::cas_set_mark(markWord new_mark, markWord old_mark) {
// 熟悉的CAS
uintptr_t v = HeapAccess<>::atomic_cmpxchg_at(as_oop(), mark_offset_in_bytes(), old_mark.value(), new_mark.value());
// 返回被设置为CAS结果的markWord
return markWord(v);
}
当 synchronized 尝试获取轻量级锁时,如果对象的 mark word 处于 neutral 状态(无锁状态),它会使用 CAS 操作试图将 mark word 改变为轻量级锁状态(将 mark word 设为指向 BasicLock 的指针)。这个过程是尝试性的,如果 CAS 操作失败,表示有其他线程同时尝试获取锁,那么这个 synchronized 就可能会选择性地升级到重量级锁(在 HotSpot JVM 中,这种选择是由 JVM 决定的)。
markWord
src\hotspot\share\oops\markWord.hpp
是 Java 对象在底层的对象头的一部分,包含了关于对象自身的运行时数据,如哈希码(HashCode)、GC 年龄、锁状态标志等信息。对于不同的对象,markword
的内容和结构都可能会有所不同。以 64 位系统为例。一个 markWord 大致包含以下结构:
- 无锁状态(默认状态):
- 无偏向锁:GC 年龄(4位) + 类型指针指向对象的类元数据的地址(54位) + 是否是偏向锁(1位) + 锁标志位(2位)
- 偏向锁:线程 ID(54位) + Epoch(2位) + 是否是偏向锁(1位) + 锁标志位(2位)
- 轻量级锁状态:
- 指向栈中锁记录的指针(62位) + 锁标志位(2位)
- 重量级锁状态:
- 指向重量级锁(即管程或 Monitor)的指针(62位) + 锁标志位(2位)
- GC标记状态:
- 一些用于垃圾回收算法的标记信息
// ......
static const int age_bits = 4;
static const int lock_bits = 2;
static const int biased_lock_bits = 1;
static const int max_hash_bits = BitsPerWord - age_bits - lock_bits - biased_lock_bits;
static const int hash_bits = max_hash_bits > 31 ? 31 : max_hash_bits;
static const int unused_gap_bits = LP64_ONLY(1) NOT_LP64(0);
static const int epoch_bits = 2;
// ......
以轻量级锁升级过程的代码为例,cas_set_mark
方法使用CAS操作来修改对象的markword,这是为了保证在多线程环境下操作的原子性,防止数据竞争。比如,在实现轻量级锁(偏向锁的升级)时,会将 markword 中的线程 ID 字段替换为指向锁记录的指针。如果 CAS 操作成功,表示获取锁成功;如果失败,则表示有其它线程正在尝试获取锁,这时就需要进入锁的自旋或阻塞状态。
在偏向锁的实现中,也是通过类似的方式修改 markword 的线程 ID 字段,将其设置为当前线程的 ID,表示这个对象被当前线程偏向。
重量级锁
重新回到对象头中的源码,其实在轻量级锁中也提及了重量级锁在什么时候会进行升级(如果轻量级锁的 CAS 操作失败,则由 JVM 选择是否升级为重量级锁)开发者需要重点关注的是 inflate(current, obj(), inflate_cause_monitor_enter);
函数(同样在这里省略掉所有的官方注释并注释掉一些不重要的部分),重量级锁使用了操作系统的互斥量(mutex),可以将等待的线程挂起,让出 CPU,因此更适合锁竞争的情况。下面的源码就是完成这个膨胀操作:
ObjectMonitor* ObjectSynchronizer::inflate(Thread* current, oop object,
const InflateCause cause) {
EventJavaMonitorInflate event;
for (;;) {
const markWord mark = object->mark();
assert(!mark.has_bias_pattern(), "invariant");
// 如果Mark Word已经指向了一个ObjectMonitor
// * 说明这个对象的锁已经被膨胀为重量级锁
// * 那么直接返回这个ObjectMonitor
if (mark.has_monitor()) {
ObjectMonitor* inf = mark.monitor();
markWord dmw = inf->header();
assert(dmw.is_neutral(), "invariant: header=" INTPTR_FORMAT, dmw.value());
return inf;
}
// 如果Mark Word是"INFLATING"
// * 说明有其他线程正在尝试膨胀这个对象的锁
// * 那么就等待这个操作完成
if (mark == markWord::INFLATING()) {
read_stable_mark(object);
continue;
}
LogStreamHandle(Trace, monitorinflation) lsh;
// 如果Mark Word表示这个对象被当前线程或其他线程轻量级锁定
if (mark.has_locker()) {
// 创建一个新的ObjectMonitor
ObjectMonitor* m = new ObjectMonitor(object);
// 将Mark Word设置为INFLATING
markWord cmp = object->cas_set_mark(markWord::INFLATING(), mark);
if (cmp != mark) {
delete m;
continue;
}
// 将这个ObjectMonitor的地址存入Mark Word完成膨胀操作
markWord dmw = mark.displaced_mark_helper();
// ......
return m;
}
// 如果Mark Word是中立状态,说明这个对象未被锁定
assert(mark.is_neutral(), "invariant: header=" INTPTR_FORMAT, mark.value());
// 那么也创建一个新的ObjectMonitor
ObjectMonitor* m = new ObjectMonitor(object);
m->set_header(mark);
// 尝试将其地址存入Mark Word
if (object->cas_set_mark(markWord::encode(m), mark) != mark) {
delete m;
m = NULL;
// 如果失败继续尝试
continue;
}
// 存入成功,就完成了膨胀操作
// ......
return m;
}
}
在这个过程中有一个重要的并发控制机制:只有成功将 Mark Word 设置为 “INFLATING” 的线程才能进行膨胀操作。这样可以保证在膨胀过程中不会有其他线程干扰。另外,膨胀操作是一个不可逆的过程,一旦一个对象的锁被膨胀为重量级锁,就无法回退为轻量级锁。
锁撤销
虽然这个对象头源码中不包括锁撤销的过程,但是锁撤销也属于 synchronized 生命周期中重要的一部分。要分析锁撤销就需要重点分析 exit
函数:
void ObjectSynchronizer::exit(oop object, BasicLock* lock, JavaThread* current) {
// 先获取对象的markWord
// * 这是一个描述对象锁状态的数据结构
markWord mark = object->mark();
assert(mark == markWord::INFLATING() ||
!mark.has_bias_pattern(), "should not see bias pattern here");
// 获取BasicLock的displaced header
// * 这是一个在轻量级锁状态下用来保存对象原始的markWord的结构
markWord dhw = lock->displaced_header();
// 如果displaced header的值为0
// * 则这个锁的获取是递归的
// * 也就是当前线程已经拥有这个锁,并且又重复获取了这个锁
// * 这种情况下exit()函数并不需要做任何实际的工作
// * 因为在递归锁的情况下,锁的撤销并不需要改变对象的状态,只需要简单地返回就可以了
if (dhw.value() == 0) {
#ifndef PRODUCT
if (mark != markWord::INFLATING()) {
assert(!mark.is_neutral(), "invariant");
assert(!mark.has_locker() ||
current->is_lock_owned((address)mark.locker()), "invariant");
if (mark.has_monitor()) {
ObjectMonitor* m = mark.monitor();
assert(m->object()->mark() == mark, "invariant");
assert(m->is_entered(current), "invariant");
}
}
#endif
return;
}
// 如果markWord是BasicLock的地址
// * 那么这意味着当前线程持有的是轻量级锁
// * 在这种情况下exit()函数尝试通过CAS将displaced header恢复到markWord从而释放
// * 如果这个操作成功,函数就直接返回
if (mark == markWord::from_pointer(lock)) {
assert(dhw.is_neutral(), "invariant");
if (object->cas_set_mark(dhw, mark) == mark) {
return;
}
}
// 如果以上情况都不满足
// * 那么就意味着这个锁已经被膨胀为重量级锁,或者由其他线程持有
// * 在这种情况下,需要调用inflate()函数将轻量级锁膨胀为重量级锁
// * 并通过monitor->exit(current)来释放锁
ObjectMonitor* monitor = inflate(current, object, inflate_cause_vm_internal);
monitor->exit(current);
}
既然重量级锁的加锁过程已经非常复杂了,那么它的撤销过程也一定是非常复杂的。由于 inflate
方法已经在前面(重量级锁)分析过了,所以这里重点分析 monitor->exit()
重量级锁的撤销函数,这个函数在 src\hotspot\share\runtime\objectMonitor.cpp
中被实现:
// 重量级锁撤销是一个非常复杂的函数,主要的目标是保证资源被释放,并确保对等待获取锁的线程的正确唤醒
void ObjectMonitor::exit(JavaThread* current, bool not_suspended) {
// 先获取当前对象监视器(ObjectMonitor)的所有者
void* cur = owner_raw();
// 如果所有者不是当前线程
// * 那么要么是基本锁(BasicLock)所有者
// * 要么是不匹配的锁定状态
// * 也就是说,当前线程试图释放它并未拥有的锁
// * 如果是真的在释放一个它并未拥有的锁那么就会打印错误日志并返回
if (current != cur) {
if (current->is_lock_owned((address)cur)) {
assert(_recursions == 0, "invariant");
set_owner_from_BasicLock(cur, current);
_recursions = 0;
} else {
#ifdef ASSERT
// 打印错误日志并返回
#endif
return;
}
}
// 如果锁是递归获取的,那么就递减`_recursions`并返回
// * 因为递归锁的释放并不会真正释放资源
// * 只有当所有递归的锁都被释放时,资源才会被释放
if (_recursions != 0) {
_recursions--;
return;
}
// ★ 进入锁释放阶段
// 先将_Responsible字段设置为NULL
_Responsible = NULL;
#if INCLUDE_JFR
if (not_suspended && EventJavaMonitorEnter::is_enabled()) {
_previous_owner_tid = JFR_THREAD_ID(current);
}
#endif
// 进入无限循环,该循环将一直运行,直到锁被成功释放并有一个线程接管为止
for (;;) {
assert(current == owner_raw(), "invariant");
release_clear_owner(current);
// 使用一个storeload内存屏障来确保此操作在内存中的顺序
OrderAccess::storeload();
// 如果_EntryList(等待获取锁的线程链表)
// * 和_cxq(另一个等待获取锁的线程链表)都为空
// * 或_succ(即将获取锁的线程)非空
// * 则该函数可以直接返回
if ((intptr_t(_EntryList)|intptr_t(_cxq)) == 0 || _succ != NULL) {
return;
}
// 再次尝试获取锁
if (try_set_owner_from(NULL, current) != NULL) {
return;
}
guarantee(owner_raw() == current, "invariant");
ObjectWaiter* w = NULL;
w = _EntryList;
// ★ 如果再次成功获取了锁,那么就从_EntryList或_cxq中获取一个等待的线程
if (w != NULL) {
assert(w->TState == ObjectWaiter::TS_ENTER, "invariant");
// 然后调用ExitEpilog()来释放锁并唤醒这个线程
ExitEpilog(current, w);
return;
}
w = _cxq;
// 如果获取锁失败,那么唤醒等待的线程的责任就转交给新的锁所有者
if (w == NULL) continue;
// ......
}
}
这个函数的主要目标是释放锁,并确保在锁被释放后,将锁传递给等待线程队列中的一个线程。在整个过程中,为了保证内存操作的顺序性,使用了多个内存屏障。同时,也需要处理一些特殊情况,如递归锁、基本锁的所有者等。
需要注意的是,尽管重量级锁使用了操作系统中的互斥量但它和轻量级锁一样,都是在 JVM 层面实现的,而不是操作系统层面。
整个过程是从当前线程试图退出监视器开始,然后检查锁的所有权,执行递归的锁释放或实际的锁释放,并在必要的时候唤醒等待的线程。如果尝试重新获取锁失败,那么确保唤醒等待的线程的责任就会转交给新的锁所有者。直至重量级锁被成功释放。
性能优化
synchronized 的设计与实现是如此之复杂而精妙,但是在高并发场景中,synchronized 可能会成为性能瓶颈,因为它在同一时间只允许一个线程访问标记为 synchronized 的方法或代码块。这可能导致线程阻塞,降低了程序的吞吐量。
在大促、秒杀等业务场景中,性能需求通常非常高,因此 synchronized 可能不是最好的选择。值得开发者进行性能优化的方面有如下几种:
- 减少锁的粒度:这可以通过锁定更小的代码块而不是整个方法来实现。如果一个方法中只有一部分代码需要同步,那么可以只锁定那部分代码,而不是整个方法。
- 锁分离:如果有两个不相关的操作都需要同步,那么可以使用两个不同的锁,而不是一个。这可以减少锁竞争,从而提高性能。
- 避免在持有锁的情况下执行耗时操作:在持有锁的情况下执行耗时的操作会增加其他线程等待锁的时间,这可能导致性能问题。
- 使用更高效的并发工具:JUC 提供了许多比 synchronized 更高效的并发工具,如 ReentrantLock、Semaphore、CountDownLatch 等。这些工具提供了更灵活、更高效的并发控制。
- 使用无锁数据结构:Java 并发库提供了一些无锁的线程安全的数据结构,如 ConcurrentHashMap、CopyOnWriteArrayList 等。这些数据结构使用了优化的并发控制策略,比使用 synchronized 的数据结构更高效。
- 使用Java8的并发API:Java 8 引入了一些新的并发 API,如 CompletableFuture,它可以帮助你更好地处理并发任务,而无需显式地使用锁。
高并发业务的性能优化对于开发者来说不是一日之谈,需要大量的实际开发经验与业务开发的折磨,这里可以参考美团的技术文章 CompletableFuture 原理与实践-外卖商家端API的异步化 - 美团技术团队 (meituan.com)。
参考文献
[1] Oracle. (2021). The Java® Virtual Machine Specification Java SE 17 Edition. Oracle Corporation. Retrieved May 16, 2023, from https://docs.oracle.com/javase/specs/jvms/se17/html/index.html
[2] vran. (2021). 你知道 Java 的偏向锁要被废弃掉了吗?. Retrieved from https://zhuanlan.zhihu.com/p/365454004.
[3] Java Magazine Staff. (2020, September 27). A convenient list of essential Java 15 resources. Retrived from https://blogs.oracle.com/javamagazine/post/a-convenient-list-of-essential-java-15-resources
[4] Mateo, P. C. (2021, August 28). JEP 374: Deprecate and Disable Biased Locking. OpenJDK. https://openjdk.java.net/jeps/374
[5] Christian Wimmer. (2008). Synchronization and Object Locking. Retrieved from https://wiki.openjdk.org/display/HotSpot/Synchronization
[6] Michael D. Shah and Samuel Z. Guyer. (2018). Iceberg: dynamic analysis of Java synchronized methods for investigating runtime performance variability. In Companion Proceedings for the ISSTA/ECOOP 2018 Workshops (ISSTA ‘18). Association for Computing Machinery, New York, NY, USA, 119–124. https://doi.org/10.1145/3236454.3236505
[7] Radu Iosif. (2000). Formal verification applied to Java concurrent software. In Proceedings of the 22nd international conference on Software engineering (ICSE ‘00). Association for Computing Machinery, New York, NY, USA, 707–709. https://doi.org/10.1145/337180.337594
[8] 陈益 & 王佩.(2018).基于同步机制解决Java多线程安全问题的应用. 软件导刊(12),165-168+172.
[9] Lindholm, T., Yellin, F., Bracha, G., & Buckley, A. (2015). The Java Virtual Machine Specification, Java SE 8 Edition (爱飞翔 & 周志明, Trans.). 机械工业出版社. (Original work published 2015)
[10] Oracle. (2021). The Java® Virtual Machine Specification Java SE 17 Edition. Oracle Corporation. Retrieved May 16, 2023, from https://docs.oracle.com/javase/specs/jvms/se17/html/index.html