深度解析Java synchronized关键字及其底层实现原理
第一部分:程序员的视角 - 保证与用法
本部分旨在为 synchronized
这一语言特性建立坚实的知识基础。我们将探索它是什么,如何使用,以及它为开发者提供了哪些保证,为后续深入探讨其实现原理做好铺垫。
第一节:Java并发的基石
1.1 引言:内置锁的诞生
synchronized
是Java语言中历史最悠久、最基础的并发控制机制,用于协调多个线程对共享资源的访问 1。自JDK 1.0起,Java中的每一个对象都被设计为拥有一个与之关联的“内置锁”(Intrinsic Lock),也被称为“监视器”(Monitor)3。这一设计决策使得任何对象都能成为一个潜在的并发原语,这种便利性对Java虚拟机(JVM)的后续设计产生了深远的影响 6。其核心目标是强制实现互斥(Mutual Exclusion),防止多个线程在同一时刻执行被保护的代码“临界区”,从而避免竞态条件(Race Conditions)和数据损坏 7。
这种“万物皆可为锁”的设计哲学,是理解 synchronized
的关键。它为程序员提供了极大的便利,无需像其他语言或框架那样为基本的同步需求显式创建锁对象 6。然而,这种便利性并非没有代价。它给每个Java对象都施加了一项“锁税”:无论该对象是否被用于同步,其内存布局中都必须包含用于支持锁机制的元数据。这直接导致了对象内存开销的增加 10。这一根本性的设计约束,迫使JVM工程师们必须开发出极为复杂的优化策略,才能在实践中让这个看似“重量级”的设计变得轻巧高效。可以说,
synchronized
的整个演进史,就是一部JVM为弥补语言设计便利性所带来的固有开销而进行不懈优化的历史。对象头中的“标记词(Mark Word)”之所以会成为一个被高度复用、状态繁多的复杂数据结构,正是因为它必须为每一个对象管理这种潜在的锁定状态 12。
1.2 语法与应用:同步的三种形态
synchronized
关键字提供了三种主要的应用形式,每种形式对应不同的锁对象和同步范围:
同步实例方法 (Synchronized Instance Methods): 将
synchronized
关键字应用于实例方法时,线程获取的是this
对象实例的内置锁。这意味着,在同一时刻,只有一个线程能够执行该特定对象上的 任何 一个同步实例方法。其他试图访问该对象任何同步实例方法的线程都将被阻塞 1。同步静态方法 (Synchronized Static Methods): 当
synchronized
应用于静态方法时,线程获取的锁是与该类关联的Class
对象(例如,MyClass.class
)的内置锁。这提供了类级别的同步,确保在同一时刻,只有一个线程能够执行该类中的任何同步静态方法,这种锁定效果横跨该类的所有实例 1。同步代码块 (Synchronized Blocks): 这是最灵活、控制粒度最细的形式。它允许开发者显式指定用作锁的对象:
synchronized (lockObject) {... }
。这种方式至关重要,因为它能够将同步的范围最小化到真正需要保护的“临界区”,并且允许使用this
或Class
对象以外的任意对象作为锁 1。
1.3 可重入性(Reentrancy)
synchronized
的锁是可重入的。这意味着一个已经持有某个锁的线程,可以再次成功获取同一个锁而不会被自己阻塞 4。这个特性至关重要,它有效避免了在一个同步方法内部调用同一个对象的另一个同步方法时可能发生的“自我死锁” 4。在JVM内部,这一机制是通过一个与锁关联的递归计数器来实现的。线程每次重入,计数器加一;每次退出,计数器减一。只有当计数器归零时,锁才会被真正释放 19。
第二节:synchronized
的三大支柱 - JMM保证
许多开发者将 synchronized
主要与互斥(原子性)联系在一起。然而,其在确保内存可见性和有序性方面的作用同等重要,甚至更为关键。这三大保证共同构成了 synchronized
在并发编程中的核心价值。
2.1 Java内存模型(JMM)简介
为了屏蔽底层不同硬件架构的复杂性,Java语言规范定义了Java内存模型(JMM)。JMM(在Java 5中通过JSR-133被正式确立和完善)是一套规则,它规定了线程如何以及何时能看到其他线程修改过的共享变量的值,以及在必要时,如何同步对共享变量的访问 21。JMM主要解决现代多核处理器架构下的两个核心问题:
缓存一致性 (Cache Coherency): 每个CPU核心都有自己的高速缓存。一个核心对缓存中数据的修改,不会立即对其他核心可见,可能导致线程读取到过时的数据 23。
指令重排序 (Instruction Reordering): 为了提升性能,编译器和CPU可能会对指令的执行顺序进行调整。在单线程环境下,这种重排序的结果与代码顺序执行的结果一致,但在多线程环境下,它可能破坏程序的并发正确性 8。
synchronized
便是JMM中用于强制实施这些规则,为程序员提供确定性保证的关键工具之一。
2.2 三大保证详解
原子性 (Atomicity):
synchronized
确保其保护的代码块(临界区)中的所有操作,对于其他线程来说,是作为一个不可分割的原子单元执行的。这意味着,任何线程都不可能观察到临界区内共享变量的中间状态,要么看到的是临界区执行前的状态,要么是执行完毕后的状态 8。可见性 (Visibility): 这是
synchronized
最重要也最容易被忽视的保证。JMM通过“先行发生”(Happens-Before)原则来阐述可见性。其中,“管程锁定规则”(Monitor Lock Rule)规定:对一个锁的解锁(unlock)操作,先行发生于后续对同一个锁的加锁(lock)操作 14。这意味着,当一个线程释放锁时,JMM会确保该线程在释放锁之前对所有共享变量的修改,都对下一个成功获取同一个锁的线程完全可见 23。JVM通过在锁释放(monitorexit
)时插入一个“释放屏障”(Release Barrier),强制将线程本地缓存中的修改刷新到主内存;在锁获取(monitorenter
)时插入一个“获取屏障”(Acquire Barrier),强制使本地缓存失效,从主内存重新加载共享变量,从而实现了这一保证 34。有序性 (Ordering):
synchronized
隐式地充当了内存屏障(Memory Barrier/Fence)的角色。它禁止编译器和处理器为了优化而将指令重排序,从而跨越同步块的边界。也就是说,同步块内部的代码不能被重排序到同步块外部,反之亦然,以确保其原子性和可见性不受影响 34。
synchronized
不仅仅是一个互斥锁,更是一个强大的内存可见性工具。一个简单的互斥锁只需保证临界区的独占访问,但如果没有可见性保证,后一个获取锁的线程看到的可能是前一个线程进入临界区之前的数据,这显然是错误的。synchronized
通过强制的缓存刷新与失效机制,解决了这个问题。这也解释了为什么仅在 synchronized
块内部访问的共享变量不需要被声明为 volatile
。synchronized
已经提供了 volatile
关键字所具备的可见性和有序性保证,并在此基础上增加了原子性。深刻理解这一点,是编写正确且高效并发程序的关键,可以避免在代码中进行冗余的同步声明。
第二部分:JVM的视角 - 底层运作机制
本部分将深入HotSpot虚拟机的内部,揭示 synchronized
关键字背后的实现细节。我们将探索“锁”这一抽象概念是如何在对象内存中具体表示的,以及JVM如何根据锁的竞争情况动态调整其锁定策略。
第三节:Java对象的解剖学 - 对象头与标记词
3.1 Java对象的内存布局
在HotSpot虚拟机中,一个Java对象的实例在内存中由三部分组成:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)12。对象头是JVM进行对象管理的核心,它主要包含两部分信息(以64位JVM为例):
标记词 (Mark Word): 占用8个字节。这是一个高度复用的字段,其内部结构会根据对象的状态动态变化,用于存储对象的哈希码(Identity Hash Code)、GC分代年龄、以及至关重要的锁信息 12。
类型指针 (Klass Pointer): 在开启压缩指针(CompressedOops)的情况下占用4个字节。它指向方法区(Metaspace)中该对象的类元数据信息 37。
3.2 深入标记词(Mark Word)
Mark Word的意义并非一成不变,它的位(bits)被“重载”以表示不同的状态。对象是无锁、被锁定还是等待GC,都由Mark Word的内容决定,特别是其末尾的几个“标志位”(tag bits)10。
下表详细展示了在64位HotSpot虚拟机中,Mark Word如何根据不同的锁状态来组织其内部的位结构。这张表是理解 synchronized
物理实现的核心,它将“无锁”、“偏向锁”、“轻量级锁”和“重量级锁”这些抽象状态,转换为了具体的二进制位模式,直观地揭示了锁升级过程中对象头内部发生的变化。
表1:64位HotSpot虚拟机Mark Word状态布局
锁状态 | 标志位 | Mark Word 内容 (高位 -> 低位) | 引用 |
无锁 (Unlocked) | 01 | 未使用 (25 bits) | identity_hashcode (31 bits) | GC分代年龄 (4 bits) | 偏向锁标志 (0) | 13 |
偏向锁 (Biased Lock) | 01 | 持有锁的线程ID (54 bits) | epoch (2 bits) | GC分代年龄 (4 bits) | 偏向锁标志 (1) | 42 |
轻量级锁 (Lightweight Lock) | 00 | 指向线程栈中锁记录(Lock Record)的指针 (62 bits) | 42 |
重量级锁 (Heavyweight Lock) | 10 | 指向堆中监视器(ObjectMonitor)的指针 (62 bits) | 42 |
GC标记 (Marked for GC) | 11 | (空, 或GC转发指针) | 41 |
第四节:锁的动态之舞 - 升级与降级
4.1 核心哲学:自适应优化
早期的JVM版本中,synchronized
的实现非常“笨重”,每次加锁都会直接动用操作系统层面的互斥量(Mutex),这在锁竞争不激烈的情况下性能开销巨大 47。从JDK 1.6开始,HotSpot虚拟机引入了一套精巧的锁优化机制,即“锁升级”阶梯。JVM总是从最乐观、开销最低的锁类型开始,只有当竞争程度加剧时,才会逐步“升级”到更重量级的锁类型 48。
整个锁升级路径是一系列基于竞争情况的概率性押注。JVM首先押注:“这个锁很可能只会被一个线程使用”,这就是偏向锁,其回报是后续加锁的极高效率,而押注失败的代价是昂贵的偏向撤销。如果这个赌注失败,JVM会进行第二个赌注:“即使有竞争,也很可能是短暂的”,这就是轻量级锁与自适应自旋,其回报是避免了操作系统层面的线程上下文切换,代价是自旋时消耗的CPU周期。如果这个赌注再次失败,JVM便放弃乐观,采用最保守、开销最大但永远正确的重量级锁机制。这个框架不仅解释了 synchronized
的工作原理,也揭示了其性能的演变。例如,JDK 15中默认禁用偏向锁的决定(JEP 374)50,就是对第一个赌注的重新评估。JVM工程师们认为,在现代硬件(CAS指令更快)和现代编程范式(更多使用
java.util.concurrent
包而非老旧的同步集合)下,偏向锁撤销的成本往往超过了其带来的收益,因此在通用场景下,这个赌注不再划算。
4.2 第一级:偏向锁 (Biased Locking) - “独奏者”优化
前提假设: 绝大多数对象在其生命周期内最多只被一个线程锁定 49。
工作机制: 当一个线程首次获取一个无锁对象的锁时,JVM会使用一次CAS(Compare-And-Swap)原子操作,将该线程的ID记录到对象的Mark Word中,并设置偏向锁标志位为1 43。此后,当
同一个 线程再次请求该锁时,只需简单地检查Mark Word中的线程ID是否为自己,无需任何原子操作,开销极低 11。解锁时,通常也只是一个简单的检查,偏向状态得以保持 53。
4.3 竞争的代价:偏向锁撤销 (Bias Revocation)
触发条件: 当有 另一个 线程尝试获取这个已经被偏向的锁时 43。
工作机制: 这是一个高成本操作。它需要触发一次虚拟机安全点(VM Safepoint),这会暂停持有偏向锁的线程(有时是所有线程)11。然后,JVM会遍历该线程的栈帧,找到与此对象相关的锁记录,并将对象的Mark Word恢复到无锁或轻量级锁的状态,之后才允许发起竞争的线程继续尝试获取锁 43。
批量撤销与重偏向: 为了应对某些场景下,一类对象的所有权被频繁地从一个线程转移到另一个线程的模式,JVM引入了批量操作。通过一个存储在类元数据中并被复制到对象Mark Word里的“纪元”(epoch)值,JVM可以一次性地将该类的所有实例的偏向锁批量撤销或“重偏向”给新的线程,从而摊销了逐个撤销的成本 43。
4.4 第二级:轻量级锁 (Lightweight Locking) - “君子之争”
前提假设: 当锁不再适合偏向时,JVM假设线程间的锁竞争是短暂的,持有锁的线程会很快释放它。
工作机制:
请求锁的线程会在自己的线程栈上创建一个“锁记录”(Lock Record)空间 45。
JVM将锁对象的Mark Word内容复制到这个锁记录中,这部分被复制的内容被称为“Displaced Mark Word” 52。
然后,线程尝试使用一次CAS原子操作,将锁对象的Mark Word更新为指向其栈上锁记录的指针 45。
如果CAS操作成功,则该线程获得锁。解锁时,再通过一次CAS操作将Displaced Mark Word恢复到对象头。整个过程完全在用户态完成,避免了昂贵的内核态切换和操作系统调用 53。
4.5 应对短暂竞争:自适应自旋 (Adaptive Spinning)
触发条件: 如果线程尝试获取轻量级锁的CAS操作失败,说明锁已被其他线程持有。
工作机制: 此时,线程并不会立即被挂起(这需要操作系统介入),而是会进入一个短暂的、忙等待的循环,即“自旋”,在循环中不断地重试CAS操作 49。
优化原理: 如果锁的持有时间非常短,自旋等待的CPU开销会远小于线程挂起和唤醒所涉及的两次上下文切换的开销 55。所谓“自适应”,是指JVM会根据该锁上一次自旋的成功率和锁持有者的状态,动态地调整自旋的次数和时间,以求达到最佳平衡 49。
4.6 第三级:重量级锁 (Heavyweight Locking) - “终极仲裁”
触发条件 (锁膨胀): 如果自旋了一定次数后仍然无法获得锁,或者有多个线程同时在等待同一个锁,轻量级锁就会“膨胀”(Inflate)为重量级锁 11。
工作机制:
JVM会为该锁对象在堆中分配(或从一个预分配池中获取)一个完整的
ObjectMonitor
对象。对象的Mark Word被修改为指向这个
ObjectMonitor
的指针 42。之后所有等待该锁的线程,都会被
ObjectMonitor
接管,并通过操作系统原语(如pthread_mutex
或futex
)进行挂起(park),进入阻塞状态,等待被唤醒 56。
第五节:重量级武器 - ObjectMonitor
5.1 通往操作系统之桥
当锁膨胀后,JVM会创建一个 ObjectMonitor
。这是一个在HotSpot虚拟机运行时内部实现的C++对象,是管理高竞争锁的核心数据结构 19。
ObjectMonitor
负责维护等待线程队列,并与操作系统调度器交互,以阻塞(park)和唤醒(unpark)线程 56。
Java语言规范定义了包含 wait()
和 notify()
方法的“监视器” 59,而操作系统层面则提供了如互斥量(mutex)和条件变量(condition variable)等底层同步原语 61。
ObjectMonitor
正是连接这两者的关键翻译层。它利用操作系统的互斥量来实现Java锁的互斥访问部分,同时在此基础上,利用操作系统的条件变量来实现 wait/notify
机制。因此,锁膨胀不仅仅是“使用了一个操作系统锁”,而是实例化了一个复杂的C++对象,该对象将Java监视器的完整语义映射到了可用的操作系统原语之上。
5.2 ObjectMonitor
的内部结构
根据OpenJDK的源码(objectMonitor.hpp
),ObjectMonitor
的核心字段包括 19:
_owner
: 指向当前持有该锁的线程。_recursions
: 记录锁的重入次数。_EntryList
: 一个队列,存放所有正在等待获取该锁的线程。_WaitSet
: 一个队列,存放所有在该对象上调用了Object.wait()
方法而进入等待状态的线程。
5.3 线程的阻塞与唤醒
当一个线程尝试获取一个已膨胀的锁失败后,它会被封装成一个节点(ObjectWaiter
)并加入到 _EntryList
队列中。随后,JVM会调用操作系统层面的机制来挂起该线程。在Linux系统上,这通常通过 futex
(Fast Userspace Mutex) 实现。futex
是一种高效的系统调用,它允许线程在内核态等待一个内存地址上的值的变化,而无需在内核中进行忙等待,从而避免了不必要的CPU消耗 62。
这个过程涉及到从用户态(Java应用程序)到内核态(操作系统)的切换,这是重量级锁开销的主要来源之一 65。当持有锁的线程释放锁时,
ObjectMonitor
会根据某种策略(如FIFO)从 _EntryList
中取出一个等待线程,并再次通过操作系统机制(如另一次 futex
调用)将其唤醒 58。被唤醒的线程并不会立即获得锁,而是会回到就绪状态,重新参与锁的竞争。
第三部分:高级主题与现代语境
本部分将探讨 synchronized
关键字如何与Java生态系统的其他部分互动,包括编译期优化、替代锁机制,以及在面对像Project Loom这样的重大平台变革时其角色的演变。
第六节:编译器的神来之笔 - JIT优化
synchronized
的实际性能不仅取决于运行时的锁升级机制,还深受即时编译器(Just-In-Time, JIT)的优化影响 70。
6.1 逃逸分析 (Escape Analysis): 锁优化的基石
JIT编译器的一项关键技术是逃逸分析。它会分析一个对象引用的作用域,判断该对象是否会“逃逸”出其被创建的方法或线程之外 72。如果一个对象被证明是线程本地的(
NoEscape
),即永远不会被其他线程访问到,那么对这个对象进行同步加锁就变得毫无意义 76。
6.2 锁消除 (Lock Elision): 移除无用之锁
机制: 基于逃逸分析的结果,如果JIT编译器确定一个锁对象是线程本地的,它就可以实施锁消除优化,即在生成的机器码中完全移除对应的
monitorenter
和monitorexit
指令 73。典型场景: 在一个方法内部使用
StringBuffer
或Vector
(这两个类的内部方法是同步的),但这个对象的引用从未离开该方法。JIT编译器可以消除其内部所有的同步开销,使其性能与非同步的StringBuilder
或ArrayList
相媲美 80。
6.3 锁粗化 (Lock Coarsening): 合并相邻之锁
机制: 如果JIT编译器检测到在一段代码中,多个连续的
synchronized
块锁定了 同一个 对象,它可能会将这些锁合并成一个范围更大的锁,这个过程称为锁粗化 49。收益: 减少了频繁申请和释放锁的开销。
与循环优化的互动: JIT通常会避免将锁粗化到整个循环的外部,以防锁被长时间占用。但它会采用一种更聪明的策略:先对循环进行“循环展开”(Loop Unrolling),即将循环体复制几次,然后再对展开后的多个相邻同步块进行锁粗化,这同样能带来显著的性能提升 83。
这些JIT优化与 synchronized
之间形成了强大的协同效应。开发者可以遵循安全的编程实践,编写防御性的同步代码,而JVM则能在运行时分析其实际使用场景,当同步并非必要时,智能地移除其性能开销。这颠覆了“同步总是慢”的陈旧观念,并使得 synchronized
在许多单线程使用场景下的成本几乎为零。
第七节:synchronized
与 java.util.concurrent.locks
的对决
7.1 内置锁与显式锁
synchronized
: 作为语言内置的特性,其加锁和解锁由JVM自动管理,语法简洁 85。ReentrantLock
: 作为java.util.concurrent.locks
包下的一个类,它提供了显式的API。程序员必须手动调用lock()
和unlock()
方法,并且通常需要配合try...finally
结构来确保锁的正确释放 85。
7.2 功能性差异
ReentrantLock
提供了 synchronized
所不具备的多种高级功能,使其在复杂并发场景下更具灵活性。
表2:synchronized
与 ReentrantLock
功能对比
功能特性 | synchronized | ReentrantLock | 引用 |
加/解锁作用域 | 块结构化 (方法内) | 非结构化 (可跨方法) | 85 |
锁释放 | 自动 (隐式) | 手动 (必须try...finally ) | 85 |
可中断等待 | 不支持 | 支持 (lockInterruptibly ) | 85 |
超时等待 | 不支持 | 支持 (tryLock 带超时) | 90 |
公平性 | 非公平 | 可选 (构造函数配置) | 90 |
条件变量 | 每个对象一个 (wait/notify) | 可绑定多个 (newCondition ) | 92 |
实现方式 | JVM内置 | Java类 (基于AQS/CAS) | 56 |
虚拟线程钉住 | JDK 24前会, 24+不会 | 不会 | 95 |
7.3 性能:破除迷思
关于 synchronized
和 ReentrantLock
的性能对比,存在一些历史包袱。在JDK 1.5时代,ReentrantLock
在高竞争下的性能确实优于 synchronized
94。然而,随着JDK 1.6及以后版本对
synchronized
的一系列优化(如锁升级机制),两者的性能差距已经大大缩小。在许多中低竞争场景下,synchronized
的性能甚至可能反超 ReentrantLock
94。因此,在现代Java开发中,选择哪种锁应主要基于功能需求,而非对性能的预先假设 94。
第八节:synchronized
的演进与未来
synchronized
并非一个一成不变的特性,它随着Java平台的发展而不断进化。
8.1 性能演进史
synchronized
的性能故事是一个持续优化的过程,反映了JVM技术的进步。
表3:synchronized
优化在JDK版本间的演进
JDK版本 | 关键变更/优化 | 对synchronized 的影响 | 引用 |
< 1.5 | 仅有重量级监视器 | 性能普遍较差,所有同步操作开销都很高。 | 47 |
1.5 / 1.6 | 引入锁升级机制 | 在无竞争或低竞争场景下性能大幅提升。 | 49 |
15 | 默认禁用偏向锁 (JEP 374) | 在某些特定无竞争场景下有轻微性能回归,但提升了整体性能的可预测性,并降低了JVM的复杂性。 | 50 |
21 | 虚拟线程 (JEP 444) | synchronized 导致线程“钉住”(pinning),限制了其在虚拟线程环境下的可伸缩性。 | 95 |
24 | 解决钉住问题 (JEP 491) | 钉住问题被消除。synchronized 完全兼容虚拟线程且高效。 | 104 |
8.2 synchronized
与虚拟线程 (Project Loom)
钉住问题 (Pinning Problem, JDK 21): 虚拟线程的核心优势在于,当它们执行阻塞I/O操作时,可以从其载体平台线程(Carrier Thread)上“卸载”,从而释放平台线程去执行其他任务。然而,在JDK 21的实现中,如果虚拟线程在
synchronized
块内部发生阻塞,它会被“钉住”在载体线程上,无法被卸载。这严重削弱了虚拟线程在高并发I/O密集型应用中的优势 95。解决方案 (Unpinning, JDK 24): 针对这一痛点,JEP 491对JVM的监视器实现进行了重构,使其能够感知虚拟线程。从JDK 24开始,当虚拟线程在
synchronized
块内阻塞时,它能够正常地从载体线程卸载。这彻底解决了钉住问题,使得synchronized
重新成为虚拟线程环境下的一个高效且可靠的同步工具 95。
第九节:综合与最佳实践
经过从应用到原理、从历史到未来的全面剖析,我们可以总结出一套适用于现代Java开发的 synchronized
最佳实践。
9.1 核心要点回顾
synchronized
已经从一个简单的语言关键字,演变为一个由JVM、JIT编译器和操作系统协同工作的复杂、自适应的同步机制。它不仅是互斥工具,更是保证内存可见性的基石。其性能是动态的,取决于应用的竞争模式,并且随着JVM的进化而不断提升。
9.2 现代Java开发 actionable 建议
何时使用
synchronized
: 对于简单、明确的互斥需求,当ReentrantLock
的高级功能并非必需时,synchronized
因其简洁性而成为首选。随着JDK 24+解决了虚拟线程的钉住问题,它在各种场景下(包括虚拟线程)都是一个完全有效的选择 107。选择正确的锁对象:
避免
synchronized(this)
: 在可能与不受信任的外部代码交互的类中,避免使用this
作为锁。外部代码可能会无意或恶意地锁定该对象,导致死锁或拒绝服务攻击 109。推荐模式: 使用一个私有的、final的
Object
实例作为锁对象:private final Object lock = new Object();
。这种方式将锁封装在类的内部,防止外部干扰,并为未来实现更细粒度的锁定策略(如使用多个锁保护不同状态)提供了可能 4。
控制锁的粒度:
最小化同步范围: 始终将
synchronized
块的作用范围限制在绝对必要的代码上。同步块越小,线程持有锁的时间就越短,系统的并发能力就越强 1。警惕长时方法: 对一个执行时间很长的方法整体进行同步,极易使其成为系统性能瓶颈 112。
预防死锁:
锁序一致: 使用
synchronized
时,最常见的死锁原因是多个线程以不同的顺序请求相同的多个锁。最有效的预防策略是,在整个应用程序中,强制所有线程始终以一个固定的、全局一致的顺序来获取这些锁 113。
适时选择替代方案:
当需要可中断等待、超时等待、公平性策略或多个条件变量等高级功能时,应选择
ReentrantLock
100。对于简单的原子操作,如计数器递增,应优先使用
java.util.concurrent.atomic
包下的类(如AtomicInteger
)。它们基于无锁的CAS操作实现,通常比任何锁机制都要快得多 28。对于读多写少的场景,
ReadWriteLock
或StampedLock
提供了更优的并发性能 15。