
在 Java 并发编程领域,synchronized堪称基础同步工具的 “元老”。自 Java 诞生以来,它伴随开发者走过二十余载。即便如今有ReentrantLock、StampedLock等更灵活的锁机制,synchronized凭借简洁语法与 JVM 级优化支持,依然在并发编程中占据不可替代的地位。
本文将全方位剖析synchronized的实现原理,从对象头结构到 Monitor 运作机制,从锁升级过程到 JVM 优化手段,结合实战案例助你彻底掌握这个核心同步关键字。无论你是初涉并发编程的新手,还是深耕技术的资深开发者,都能从中获得清晰的知识脉络与实用的实践指导。
深入底层原理前,我们先梳理synchronized的基本用法。只有掌握其应用场景,才能更好理解设计初衷与优化方向。
synchronized有三种常见使用形式,对应不同同步粒度:
当synchronized修饰实例方法时,锁对象为当前实例。这意味着:多个线程访问同一对象的同步方法会互斥;访问不同对象的同步方法则无干扰。
@Slf4j
public class SynchronizedDemo {
// 同步实例方法,锁对象为当前实例
public synchronized void syncInstanceMethod() {
log.info("进入同步实例方法");
try {
// 模拟业务操作耗时
Thread.sleep(1000);
} catch (InterruptedException e) {
log.error("线程中断异常", e);
Thread.currentThread().interrupt();
}
log.info("离开同步实例方法");
}
public static void main(String[] args) {
SynchronizedDemo demo = new SynchronizedDemo();
// 线程1与线程2竞争同一实例锁
new Thread(demo::syncInstanceMethod, "Thread-1").start();
new Thread(demo::syncInstanceMethod, "Thread-2").start();
// 线程3使用新实例,与前两个线程无锁竞争
new Thread(new SynchronizedDemo()::syncInstanceMethod, "Thread-3").start();
}
}运行结果可见:Thread-1与Thread-2依次执行(间隔约 1 秒),而Thread-3与Thread-1几乎同时执行,因二者持有不同对象锁。
synchronized修饰静态方法时,锁对象为当前类的Class对象。由于一个类在 JVM 中仅存在一个Class实例,所有访问该静态同步方法的线程都会共享这把锁,与具体实例无关。
@Slf4j
public class SynchronizedStaticDemo {
// 同步静态方法,锁对象为SynchronizedStaticDemo.class
public static synchronized void syncStaticMethod() {
log.info("进入同步静态方法");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
log.error("线程中断异常", e);
Thread.currentThread().interrupt();
}
log.info("离开同步静态方法");
}
public static void main(String[] args) {
SynchronizedStaticDemo demo1 = new SynchronizedStaticDemo();
SynchronizedStaticDemo demo2 = new SynchronizedStaticDemo();
// 线程1与线程2共享类锁,存在竞争
new Thread(demo1::syncStaticMethod, "Thread-1").start();
new Thread(demo2::syncStaticMethod, "Thread-2").start();
}
}运行结果显示:Thread-1与Thread-2依次执行,因二者争夺的是SynchronizedStaticDemo.class对象的锁。
synchronized修饰代码块时,需显式指定锁对象。这种方式灵活性最高,可根据业务需求选择锁粒度,减少锁竞争损耗。
@Slf4j
public class SynchronizedBlockDemo {
// 显式定义锁对象,遵循阿里巴巴规约使用final修饰
private final Object lock = new Object();
private int count = 0;
public void increment() {
// 同步代码块,锁对象为lock
synchronized (lock) {
count++;
log.info("当前计数:{},线程:{}", count, Thread.currentThread().getName());
}
}
public static void main(String[] args) {
SynchronizedBlockDemo demo = new SynchronizedBlockDemo();
// 10个线程竞争同一把锁,保证count自增原子性
for (int i = 0; i < 10; i++) {
new Thread(demo::increment, "Thread-" + i).start();
}
}
}上述代码中,10 个线程竞争lock对象的锁,确保count++操作原子性,最终计数正确递增到 10。
多线程环境下,多个线程同时访问共享资源可能导致数据不一致。通过以下案例直观感受该问题:
@Slf4j
public class UnsynchronizedProblemDemo {
private int count = 0;
// 未加锁的自增方法,存在线程安全问题
public void increment() {
count++;
}
public static void main(String[] args) throws InterruptedException {
UnsynchronizedProblemDemo demo = new UnsynchronizedProblemDemo();
int threadCount = 10;
Thread[] threads = new Thread[threadCount];
// 创建10个线程,每个线程执行1000次自增
for (int i = 0; i < threadCount; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
demo.increment();
}
});
threads[i].start();
}
// 等待所有线程执行完毕
for (Thread thread : threads) {
thread.join();
}
log.info("最终计数:{}", demo.count);
}
}期望结果为 10000(10 线程 ×1000 次),但实际运行常小于 10000。因count++非原子操作,可分解为:读取count值→值加 1→结果写回count。多线程并发执行时,易出现数据覆盖。
synchronized的核心作用是保证同一时间仅一个线程执行特定代码块或方法,从而避免多线程竞争导致的数据不一致。
理解基本用法后,我们探索底层实现机制。搞懂synchronized,需从 Java 对象内存结构说起。
JVM 中,每个 Java 对象都有对象头(Object Header),这是实现synchronized的关键。对象头主要由Mark Word和类型指针(Klass Pointer) 组成;数组对象还包含数组长度信息。
Mark Word 是对象头核心部分,存储对象哈希码、GC 分代年龄、锁状态标志等信息。为节省空间,其结构随对象状态动态变化。32 位 JVM 中 Mark Word 结构如下:
锁状态 | 25 位 | 4 位 | 1 位 | 2 位 | 描述 |
|---|---|---|---|---|---|
无锁 | 对象的哈希码 | 分代年龄 | 是否是偏向锁(0) | 锁标志位(01) | 未被锁定的状态 |
偏向锁 | 线程 ID(23 位)+ epoch(2 位) | 分代年龄 | 是否是偏向锁(1) | 锁标志位(01) | 偏向某个线程的状态 |
轻量级锁 | 指向栈中锁记录的指针 | 锁标志位(00) | 通过 CAS 实现的轻量级锁 | ||
重量级锁 | 指向 Monitor 的指针 | 锁标志位(10) | 依赖操作系统互斥量的重量级锁 | ||
GC 标记 | 空 | 锁标志位(11) | 垃圾回收标记状态 |
64 位 JVM 中 Mark Word 结构类似,但有更多位存储哈希码和线程 ID 等信息。
类型指针指向对象对应的类元数据,JVM 通过该指针确定对象所属类。开启指针压缩时(默认开启)占 4 字节,否则占 8 字节。
当synchronized升级为重量级锁时,依赖 Monitor(管程 / 监视器)实现同步。Monitor 是一种同步机制,保证同一时间仅一个线程访问被保护代码块。
HotSpot JVM 中,Monitor 由 C++ 的ObjectMonitor实现,简化结构如下:
typedef struct ObjectMonitor {
// 指向持有锁的线程
Thread* owner;
// 等待锁的线程队列
Queue<Thread*> EntryList;
// 等待条件的线程队列
Queue<Thread*> WaitSet;
// 重入次数
int recursion;
// 竞争计数
int count;
// 条件变量,用于wait/notify
Object* cond;
} ObjectMonitor;owner:指向当前持有 Monitor 的线程。Monitor 被持有后,owner指向该线程。
EntryList:存储等待获取锁的线程队列。线程尝试获取 Monitor 失败时,进入此处等待。
WaitSet:存储调用wait()后等待唤醒的线程队列。被唤醒后需重新进入EntryList竞争锁。
recursion:记录当前线程持有锁的重入次数。synchronized是可重入锁,同一线程多次获取不会死锁。
count:记录 Monitor 被线程竞争的次数。
线程尝试获取锁时,Monitor 运作流程如下:
线程调用wait()时,释放 Monitor 所有权,进入WaitSet等待,需其他线程通过notify()或notifyAll()唤醒。被唤醒线程从WaitSet转移到EntryList,重新参与锁竞争。
使用synchronized时,JVM 在字节码层面生成相应指令实现同步。同步方法与同步代码块的字节码实现不同。
同步代码块由monitorenter和monitorexit指令包围。示例如下:
public class SynchronizedBytecodeDemo {
private final Object lock = new Object();
public void syncBlock() {
synchronized (lock) {
// 同步代码块
System.out.println("同步代码块");
}
}
}通过javap -v查看字节码关键部分:
public void syncBlock();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: getfield #2 // Field lock:Ljava/lang/Object;
4: dup
5: astore_1
6: monitorenter // 进入Monitor,获取锁
7: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
10: ldc #4 // String 同步代码块
12: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
15: aload_1
16: monitorexit // 正常退出,释放锁
17: goto 25
20: astore_2
21: aload_1
22: monitorexit // 异常退出,确保锁释放
23: aload_2
24: athrow
25: return
Exception table:
from to target type
7 17 20 any
20 23 20 any同步代码块被monitorenter(指令 6)和monitorexit(指令 16、22)包围。指令 22 的monitorexit用于异常时释放锁,保证锁安全性。
同步方法在访问标志中添加ACC_SYNCHRONIZED标志,而非生成monitorenter和monitorexit指令。线程调用时,JVM 检查该标志并尝试获取锁。
public class SynchronizedMethodBytecodeDemo {
public synchronized void syncMethod() {
// 同步方法
System.out.println("同步方法");
}
}字节码关键部分:
public synchronized void syncMethod();
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 同步方法
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return无论同步代码块还是同步方法,底层最终都通过 Monitor 实现同步,仅字节码层面实现方式不同。
Java 6 前,synchronized直接依赖重量级锁,性能较差。Java 6 及之后引入锁升级机制,根据竞争情况从偏向锁逐步升级到轻量级锁,最终到重量级锁。这种自适应机制大幅提升了synchronized性能。
对象刚创建且未被任何线程锁定时处于无锁状态。此时 Mark Word 存储哈希码、分代年龄等信息,锁标志位为 01,偏向锁标志为 0。
实际应用中,许多同步代码块多数情况下仅被一个线程访问。偏向锁设计旨在减少这种无竞争场景的性能开销。
线程首次访问同步代码块时,尝试获取偏向锁流程:
其他线程尝试获取偏向锁时,持有偏向锁的线程被唤醒,进行撤销:
Java 中偏向锁默认延迟启动(约 4 秒)。因 JVM 启动时存在大量线程竞争同步资源,过早启用会导致频繁撤销,影响性能。可通过-XX:BiasedLockingStartupDelay=0禁用延迟。
偏向锁撤销后,升级为轻量级锁。适用于多线程交替访问同步资源场景,通过 CAS 操作避免使用重量级锁。
轻量级锁竞争时,未获取锁的线程自旋等待(循环尝试获取)。基于多数线程持有锁时间短,自旋可避免线程切换开销。
JVM 根据自旋成功率动态调整次数,即适应性自旋。成功率高则增加次数,成功率低则减少或取消自旋。
多线程同时竞争轻量级锁,且自旋失败时,升级为重量级锁。依赖操作系统互斥量(Mutex)实现同步,会导致线程阻塞和唤醒,性能开销较大。
主要来自两方面:
锁升级不可逆,升级为重量级锁后不会降级。流程图如下:
无锁状态(标志01,偏向0)
↓ 首次获取锁,CAS设置线程ID
偏向锁状态(标志01,偏向1)
↓ 其他线程竞争,撤销偏向锁
轻量级锁状态(标志00)
↓ 竞争激烈,自旋失败
重量级锁状态(标志10)此设计因锁升级至重量级锁时,已存在激烈竞争,降级无法提升性能。
除锁升级机制,JVM 还提供多种锁优化手段,进一步提升synchronized性能。这些优化在 JVM 层面自动进行,了解原理有助于编写更高效并发代码。
锁消除指 JVM 编译阶段通过逃逸分析,发现某些锁对象不会被多线程访问,从而自动移除这些锁,避免不必要同步开销。
JVM 的逃逸分析技术用于分析对象生命周期是否局限于当前方法或线程。若对象不会 “逃逸” 到方法外部,则不可能被其他线程访问。
最常见场景是StringBuffer的append()方法。StringBuffer的append()是同步方法,但对象仅在方法内部使用时,JVM 会消除其同步锁。
@Slf4j
public class LockEliminationDemo {
public String buildString() {
StringBuffer sb = new StringBuffer();
// StringBuffer的append方法是同步的,但sb不会逃逸
sb.append("a");
sb.append("b");
sb.append("c");
return sb.toString();
}
public static void main(String[] args) {
LockEliminationDemo demo = new LockEliminationDemo();
long start = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
demo.buildString();
}
log.info("耗时:{}ms", System.currentTimeMillis() - start);
}
}上述代码中,sb仅在buildString()内部使用,不会逃逸。JVM 编译时消除append()的同步锁,提升性能。可通过-XX:-EliminateLocks禁用锁消除,对比性能差异验证效果。
通常认为同步代码块粒度越小越好,可减少锁竞争时间。但多个连续同步代码块使用相同锁对象时,频繁加解锁会增加开销。锁粗化将多个连续加解锁操作合并为一个更大范围的锁,减少操作次数。
@Slf4j
public class LockCoarseningDemo {
private final Object lock = new Object();
public void process() {
// 连续同步代码块使用相同锁
synchronized (lock) {
log.info("操作1");
}
synchronized (lock) {
log.info("操作2");
}
synchronized (lock) {
log.info("操作3");
}
}
}JVM 会合并为一个大同步代码块:
public void process() {
synchronized (lock) {
log.info("操作1");
log.info("操作2");
log.info("操作3");
}
}减少两次加解锁操作,降低开销。
循环中频繁加解锁是常见性能问题,JVM 会优化:
public void loopWithLock() {
for (int i = 0; i < 100; i++) {
synchronized (lock) {
// 循环内同步操作
}
}
}优化后:
public void loopWithLock() {
synchronized (lock) {
for (int i = 0; i < 100; i++) {
// 循环内同步操作
}
}
}锁移至循环外部,避免 100 次加解锁操作。
轻量级锁竞争时,未获取锁的线程自旋等待。JVM 根据历史自旋成功率动态调整次数,即适应性自旋锁。
此机制使 JVM 动态调整自旋策略,减少线程阻塞同时避免无效自旋消耗 CPU。
大量对象偏向锁指向同一线程,另一线程竞争时,JVM 进行批量重偏向;竞争持续加剧则批量撤销偏向锁。
线程创建大量对象并加偏向锁后,另一线程需访问这些对象时,JVM 统计撤销次数。达阈值后,批量将这些对象偏向锁重偏向新线程,避免频繁撤销。
批量重偏向仍竞争激烈时,JVM 批量撤销偏向锁,直接升级为轻量级或重量级锁,彻底避免偏向锁开销。
这些批量优化机制提升了偏向锁在复杂场景的性能。
理论学习后,通过实战案例对比不同锁状态性能差异,总结synchronized最佳实践。
测试程序对比无锁、偏向锁、轻量级锁和重量级锁性能:
@Slf4j
public class LockPerformanceDemo {
private static final int LOOP_COUNT = 10000000;
private int count = 0;
// 同步方法
public synchronized void increment() {
count++;
}
public static void main(String[] args) throws InterruptedException {
// 测试偏向锁性能(单线程)
testSingleThreadPerformance();
// 测试轻量级锁性能(两线程交替执行)
testLightweightLockPerformance();
// 测试重量级锁性能(两线程激烈竞争)
testHeavyweightLockPerformance();
}
private static void testSingleThreadPerformance() {
LockPerformanceDemo demo = new LockPerformanceDemo();
long start = System.currentTimeMillis();
for (int i = 0; i < LOOP_COUNT; i++) {
demo.increment();
}
long end = System.currentTimeMillis();
log.info("单线程(偏向锁)耗时:{}ms,计数:{}", end - start, demo.count);
}
private static void testLightweightLockPerformance() throws InterruptedException {
LockPerformanceDemo demo = new LockPerformanceDemo();
Thread t1 = new Thread(() -> {
for (int i = 0; i < LOOP_COUNT / 2; i++) {
demo.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < LOOP_COUNT / 2; i++) {
demo.increment();
}
});
long start = System.currentTimeMillis();
t1.start();
t2.start();
t1.join();
t2.join();
long end = System.currentTimeMillis();
log.info("双线程交替(轻量级锁)耗时:{}ms,计数:{}", end - start, demo.count);
}
private static void testHeavyweightLockPerformance() throws InterruptedException {
LockPerformanceDemo demo = new LockPerformanceDemo();
Thread t1 = new Thread(() -> {
for (int i = 0; i < LOOP_COUNT; i++) {
demo.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < LOOP_COUNT; i++) {
demo.increment();
}
});
long start = System.currentTimeMillis();
t1.start();
t2.start();
t1.join();
t2.join();
long end = System.currentTimeMillis();
log.info("双线程竞争(重量级锁)耗时:{}ms,计数:{}", end - start, demo.count);
}
}测试需通过 JVM 参数控制锁状态:
运行结果通常为:偏向锁性能最佳(接近无锁),轻量级锁次之,重量级锁最差。验证了锁升级机制在不同竞争场景下的最优性能设计。
除synchronized,ReentrantLock也是常用同步工具。对比优缺点助你选择:
特性 | synchronized | ReentrantLock |
|---|---|---|
可重入性 | 支持 | 支持 |
公平锁 | 非公平 | 支持公平和非公平 |
响应中断 | 不支持 | 支持 |
超时获取 | 不支持 | 支持 |
条件变量 | 通过 wait/notify 实现,单一条件 | 通过 Condition 实现,多条件 |
锁状态查询 | 不支持 | 支持 |
性能 | 竞争小时接近无锁,竞争大时略差 | 竞争大时性能略好 |
Java 6 后synchronized经优化,性能与ReentrantLock接近。多数情况建议优先使用synchronized,因其更简洁,JVM 自动管理,减少手动释放锁风险。
掌握原理与优化机制后,总结最佳实践助你写出高效同步代码:
尽量缩小同步代码块范围,仅同步必要代码,减少锁持有时间:
// 不推荐:同步整个方法
public synchronized void process() {
// 非同步操作1
// 同步操作
// 非同步操作2
}
// 推荐:仅同步必要代码块
public void process() {
// 非同步操作1
synchronized (lock) {
// 同步操作
}
// 非同步操作2
}多线程访问不同资源时,使用不同锁对象,避免竞争同一锁。如 ConcurrentHashMap 的分段锁,不同段用不同锁。
频繁被单线程访问的同步资源,确保偏向锁启用(默认开启),通过-XX:+UseBiasedLocking控制,获最佳性能。
长时间持有锁导致其他线程阻塞,增加响应时间。避免在同步块中执行 IO、复杂计算等耗时任务:
// 不推荐:同步块中执行耗时操作
synchronized (lock) {
// 数据库查询(耗时IO)
// 复杂计算
}
// 推荐:耗时操作移至同步块外
// 先获取必要信息
synchronized (lock) {
// 获取需同步的数据
}
// 执行耗时操作嵌套锁易导致死锁,增加竞争复杂度。尽量避免,必须使用时确保所有线程获取锁顺序一致。
集合并发访问优先用java.util.concurrent包下的并发集合(如 ConcurrentHashMap、CopyOnWriteArrayList),性能通常优于synchronized包装的同步集合(如 Collections.synchronizedMap)。
简单变量同步,volatile提供更轻量同步机制。但volatile不保证原子性,适用于读远多于写的场景。
synchronized作为 Java 并发编程基石,经多年优化成为高效可靠的同步工具。从早期依赖重量级锁的低效实现,到引入偏向锁、轻量级锁、锁消除、锁粗化等优化,性能大幅提升。
随着 Java 版本更新,synchronized优化持续进行。如 Java 15 的 ZGC 进一步优化锁与 GC 交互;未来版本可能引入更多优化技术,使其在并发编程中发挥更大作用。