首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >从底层到优化:深入理解 synchronized 的 “锁” 事

从底层到优化:深入理解 synchronized 的 “锁” 事

作者头像
果酱带你啃java
发布2026-04-14 11:24:32
发布2026-04-14 11:24:32
900
举报

从底层到优化:深入理解 synchronized 的 “锁” 事(面试必看)

在 Java 并发编程领域,synchronized堪称基础同步工具的 “元老”。自 Java 诞生以来,它伴随开发者走过二十余载。即便如今有ReentrantLock、StampedLock等更灵活的锁机制,synchronized凭借简洁语法与 JVM 级优化支持,依然在并发编程中占据不可替代的地位。

本文将全方位剖析synchronized的实现原理,从对象头结构到 Monitor 运作机制,从锁升级过程到 JVM 优化手段,结合实战案例助你彻底掌握这个核心同步关键字。无论你是初涉并发编程的新手,还是深耕技术的资深开发者,都能从中获得清晰的知识脉络与实用的实践指导。

一、synchronized 的基础:你真的会用吗?

深入底层原理前,我们先梳理synchronized的基本用法。只有掌握其应用场景,才能更好理解设计初衷与优化方向。

1.1 synchronized 的三种使用方式

synchronized有三种常见使用形式,对应不同同步粒度:

(1)修饰实例方法

当synchronized修饰实例方法时,锁对象为当前实例。这意味着:多个线程访问同一对象的同步方法会互斥;访问不同对象的同步方法则无干扰。

代码语言:javascript
复制
代码语言:javascript
复制
@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几乎同时执行,因二者持有不同对象锁。

(2)修饰静态方法

synchronized修饰静态方法时,锁对象为当前类的Class对象。由于一个类在 JVM 中仅存在一个Class实例,所有访问该静态同步方法的线程都会共享这把锁,与具体实例无关。

代码语言:javascript
复制
代码语言:javascript
复制
@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对象的锁。

(3)修饰代码块

synchronized修饰代码块时,需显式指定锁对象。这种方式灵活性最高,可根据业务需求选择锁粒度,减少锁竞争损耗。

代码语言:javascript
复制
代码语言:javascript
复制
@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。

1.2 为什么需要 synchronized?

多线程环境下,多个线程同时访问共享资源可能导致数据不一致。通过以下案例直观感受该问题:

代码语言:javascript
复制
代码语言:javascript
复制
@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 的实现原理

理解基本用法后,我们探索底层实现机制。搞懂synchronized,需从 Java 对象内存结构说起。

2.1 Java 对象头:锁的 “身份证”

JVM 中,每个 Java 对象都有对象头(Object Header),这是实现synchronized的关键。对象头主要由Mark Word类型指针(Klass Pointer) 组成;数组对象还包含数组长度信息。

(1)Mark Word 的结构

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 等信息。

(2)类型指针(Klass Pointer)

类型指针指向对象对应的类元数据,JVM 通过该指针确定对象所属类。开启指针压缩时(默认开启)占 4 字节,否则占 8 字节。

2.2 Monitor:重量级锁的核心

当synchronized升级为重量级锁时,依赖 Monitor(管程 / 监视器)实现同步。Monitor 是一种同步机制,保证同一时间仅一个线程访问被保护代码块。

(1)Monitor 的结构

HotSpot JVM 中,Monitor 由 C++ 的ObjectMonitor实现,简化结构如下:

代码语言:javascript
复制
typedef struct ObjectMonitor {
    // 指向持有锁的线程
    Thread* owner;
    // 等待锁的线程队列
    Queue<Thread*> EntryList;
    // 等待条件的线程队列
    Queue<Thread*> WaitSet;
    // 重入次数
    int recursion;
    // 竞争计数
    int count;
    // 条件变量,用于wait/notify
    Object* cond;
} ObjectMonitor;
代码语言:javascript
复制

owner:指向当前持有 Monitor 的线程。Monitor 被持有后,owner指向该线程。

EntryList:存储等待获取锁的线程队列。线程尝试获取 Monitor 失败时,进入此处等待。

WaitSet:存储调用wait()后等待唤醒的线程队列。被唤醒后需重新进入EntryList竞争锁。

recursion:记录当前线程持有锁的重入次数。synchronized是可重入锁,同一线程多次获取不会死锁。

count:记录 Monitor 被线程竞争的次数。

(2)Monitor 的运作流程

线程尝试获取锁时,Monitor 运作流程如下:

  1. 线程进入同步代码块时,尝试获取 Monitor 所有权。若owner为null,当前线程成为owner,recursion设为 1,流程结束。
  2. 若当前线程已是owner(重入),recursion加 1,流程结束。
  3. 若owner是其他线程,当前线程进入EntryList阻塞等待。
  4. owner线程退出同步代码块时,recursion减 1。若recursion变为 0,释放 Monitor 所有权(owner设为null),从EntryList唤醒一个线程竞争锁。

线程调用wait()时,释放 Monitor 所有权,进入WaitSet等待,需其他线程通过notify()或notifyAll()唤醒。被唤醒线程从WaitSet转移到EntryList,重新参与锁竞争。

2.3 synchronized 的字节码实现

使用synchronized时,JVM 在字节码层面生成相应指令实现同步。同步方法与同步代码块的字节码实现不同。

(1)同步代码块的字节码

同步代码块由monitorenter和monitorexit指令包围。示例如下:

代码语言:javascript
复制
代码语言:javascript
复制
public class SynchronizedBytecodeDemo {
    private final Object lock = new Object();

    public void syncBlock() {
        synchronized (lock) {
            // 同步代码块
            System.out.println("同步代码块");
        }
    }
}

通过javap -v查看字节码关键部分:

代码语言:javascript
复制
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用于异常时释放锁,保证锁安全性。

(2)同步方法的字节码

同步方法在访问标志中添加ACC_SYNCHRONIZED标志,而非生成monitorenter和monitorexit指令。线程调用时,JVM 检查该标志并尝试获取锁。

代码语言:javascript
复制
代码语言:javascript
复制
public class SynchronizedMethodBytecodeDemo {
    public synchronized void syncMethod() {
        // 同步方法
        System.out.println("同步方法");
    }
}

字节码关键部分:

代码语言:javascript
复制
代码语言:javascript
复制
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性能。

3.1 无锁状态

对象刚创建且未被任何线程锁定时处于无锁状态。此时 Mark Word 存储哈希码、分代年龄等信息,锁标志位为 01,偏向锁标志为 0。

3.2 偏向锁:减少无竞争开销

实际应用中,许多同步代码块多数情况下仅被一个线程访问。偏向锁设计旨在减少这种无竞争场景的性能开销。

(1)偏向锁的获取

线程首次访问同步代码块时,尝试获取偏向锁流程:

  1. 检查对象 Mark Word 是否处于可偏向状态(锁标志位 01,偏向锁标志 0)。
  2. 若是可偏向状态,通过 CAS 操作将当前线程 ID 写入 Mark Word,偏向锁标志设为 1。
  3. CAS 成功,线程获得偏向锁,执行同步代码块。
  4. CAS 失败(其他线程尝试获取),撤销偏向锁,升级为轻量级锁。
(2)偏向锁的撤销

其他线程尝试获取偏向锁时,持有偏向锁的线程被唤醒,进行撤销:

  1. 暂停持有偏向锁的线程。
  2. 检查线程是否仍在执行同步代码块:
    • 已退出:对象头恢复无锁状态,允许其他线程竞争。
    • 仍在执行:偏向锁升级为轻量级锁,线程继续执行。
  3. 唤醒被暂停的线程。
(3)偏向锁的延迟启动

Java 中偏向锁默认延迟启动(约 4 秒)。因 JVM 启动时存在大量线程竞争同步资源,过早启用会导致频繁撤销,影响性能。可通过-XX:BiasedLockingStartupDelay=0禁用延迟。

3.3 轻量级锁:CAS 避免重量级锁

偏向锁撤销后,升级为轻量级锁。适用于多线程交替访问同步资源场景,通过 CAS 操作避免使用重量级锁。

(1)轻量级锁的获取
  1. 线程进入同步代码块前,在栈帧创建锁记录(Lock Record),存储对象头 Mark Word 拷贝(Displaced Mark Word)。
  2. 通过 CAS 操作将对象头 Mark Word 替换为指向锁记录的指针。
  3. CAS 成功,线程获得轻量级锁,执行同步代码块。
  4. CAS 失败(其他线程竞争),当前线程自旋尝试获取。自旋达阈值仍失败,升级为重量级锁。
(2)轻量级锁的释放
  1. 线程退出同步代码块时,通过 CAS 将对象头 Mark Word 恢复为Displaced Mark Word。
  2. CAS 成功,无其他线程竞争,释放轻量级锁。
  3. CAS 失败(存在竞争或已升级),释放锁并唤醒阻塞线程。
(3)自旋优化

轻量级锁竞争时,未获取锁的线程自旋等待(循环尝试获取)。基于多数线程持有锁时间短,自旋可避免线程切换开销。

JVM 根据自旋成功率动态调整次数,即适应性自旋。成功率高则增加次数,成功率低则减少或取消自旋。

3.4 重量级锁:依赖操作系统的互斥量

多线程同时竞争轻量级锁,且自旋失败时,升级为重量级锁。依赖操作系统互斥量(Mutex)实现同步,会导致线程阻塞和唤醒,性能开销较大。

(1)重量级锁的获取
  1. 轻量级锁升级为重量级锁时,JVM 为对象关联 Monitor。
  2. 线程尝试获取锁时调用 Monitor 的enter()方法:
    • owner为null:线程成为owner,获得锁。
    • 线程已是owner:重入次数加 1。
    • 其他情况:线程进入EntryList阻塞等待。
(2)重量级锁的释放
  1. 线程退出同步代码块时,调用exit()方法,重入次数减 1。
  2. 重入次数为 0 时,释放 Monitor 所有权(owner设为null)。
  3. 从EntryList唤醒一个线程竞争锁。
(3)重量级锁的性能开销

主要来自两方面:

  • 内核态与用户态切换:线程阻塞 / 唤醒需切换状态,开销大。
  • 线程调度:操作系统调度阻塞 / 唤醒线程,涉及上下文切换,开销增加。

3.5 锁升级的完整流程

锁升级不可逆,升级为重量级锁后不会降级。流程图如下:

代码语言:javascript
复制
无锁状态(标志01,偏向0)
↓ 首次获取锁,CAS设置线程ID
偏向锁状态(标志01,偏向1)
↓ 其他线程竞争,撤销偏向锁
轻量级锁状态(标志00)
↓ 竞争激烈,自旋失败
重量级锁状态(标志10)

此设计因锁升级至重量级锁时,已存在激烈竞争,降级无法提升性能。

四、JVM 的锁优化:让 synchronized 更快

除锁升级机制,JVM 还提供多种锁优化手段,进一步提升synchronized性能。这些优化在 JVM 层面自动进行,了解原理有助于编写更高效并发代码。

4.1 锁消除

锁消除指 JVM 编译阶段通过逃逸分析,发现某些锁对象不会被多线程访问,从而自动移除这些锁,避免不必要同步开销。

(1)逃逸分析

JVM 的逃逸分析技术用于分析对象生命周期是否局限于当前方法或线程。若对象不会 “逃逸” 到方法外部,则不可能被其他线程访问。

(2)锁消除的场景

最常见场景是StringBuffer的append()方法。StringBuffer的append()是同步方法,但对象仅在方法内部使用时,JVM 会消除其同步锁。

代码语言:javascript
复制
代码语言:javascript
复制
@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禁用锁消除,对比性能差异验证效果。

4.2 锁粗化

通常认为同步代码块粒度越小越好,可减少锁竞争时间。但多个连续同步代码块使用相同锁对象时,频繁加解锁会增加开销。锁粗化将多个连续加解锁操作合并为一个更大范围的锁,减少操作次数。

(1)锁粗化的场景
代码语言:javascript
复制
代码语言:javascript
复制
@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 会合并为一个大同步代码块:

代码语言:javascript
复制
代码语言:javascript
复制
public void process() {
  synchronized (lock) {
  log.info("操作1");
  log.info("操作2");
  log.info("操作3");
  }
}

减少两次加解锁操作,降低开销。

(2)循环中的锁粗化

循环中频繁加解锁是常见性能问题,JVM 会优化:

代码语言:javascript
复制
代码语言:javascript
复制
public void loopWithLock() {
    for (int i = 0; i < 100; i++) {
        synchronized (lock) {
            // 循环内同步操作
        }
    }
}

优化后:

代码语言:javascript
复制
代码语言:javascript
复制
public void loopWithLock() {
    synchronized (lock) {
        for (int i = 0; i < 100; i++) {
            // 循环内同步操作
        }
    }
}

锁移至循环外部,避免 100 次加解锁操作。

4.3 适应性自旋锁

轻量级锁竞争时,未获取锁的线程自旋等待。JVM 根据历史自旋成功率动态调整次数,即适应性自旋锁。

  • 某锁自旋成功率高:增加自旋次数。
  • 某锁自旋成功率低:减少次数或取消自旋,直接阻塞。

此机制使 JVM 动态调整自旋策略,减少线程阻塞同时避免无效自旋消耗 CPU。

4.4 偏向锁批量重偏向与批量撤销

大量对象偏向锁指向同一线程,另一线程竞争时,JVM 进行批量重偏向;竞争持续加剧则批量撤销偏向锁。

(1)批量重偏向

线程创建大量对象并加偏向锁后,另一线程需访问这些对象时,JVM 统计撤销次数。达阈值后,批量将这些对象偏向锁重偏向新线程,避免频繁撤销。

(2)批量撤销

批量重偏向仍竞争激烈时,JVM 批量撤销偏向锁,直接升级为轻量级或重量级锁,彻底避免偏向锁开销。

这些批量优化机制提升了偏向锁在复杂场景的性能。

五、实战案例:synchronized 的性能对比与最佳实践

理论学习后,通过实战案例对比不同锁状态性能差异,总结synchronized最佳实践。

5.1 不同锁状态的性能对比

测试程序对比无锁、偏向锁、轻量级锁和重量级锁性能:

代码语言:javascript
复制
代码语言:javascript
复制
@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 参数控制锁状态:

  • 测试偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
  • 禁用偏向锁:-XX:-UseBiasedLocking

运行结果通常为:偏向锁性能最佳(接近无锁),轻量级锁次之,重量级锁最差。验证了锁升级机制在不同竞争场景下的最优性能设计。

5.2 synchronized vs ReentrantLock:如何选择?

除synchronized,ReentrantLock也是常用同步工具。对比优缺点助你选择:

(1)功能对比

特性

synchronized

ReentrantLock

可重入性

支持

支持

公平锁

非公平

支持公平和非公平

响应中断

不支持

支持

超时获取

不支持

支持

条件变量

通过 wait/notify 实现,单一条件

通过 Condition 实现,多条件

锁状态查询

不支持

支持

性能

竞争小时接近无锁,竞争大时略差

竞争大时性能略好

(2)适用场景
  • 使用synchronized的场景:
    • 简单同步场景,如基本方法或代码块同步。
    • 追求代码简洁,减少模板代码。
    • 无需高级功能(中断、超时、多条件)。
  • 使用ReentrantLock的场景:
    • 需要公平锁。
    • 需要响应中断或超时获取锁。
    • 需要多个条件变量。
    • 需要查询锁状态。

Java 6 后synchronized经优化,性能与ReentrantLock接近。多数情况建议优先使用synchronized,因其更简洁,JVM 自动管理,减少手动释放锁风险。

5.3 最佳实践:写出高效的同步代码

掌握原理与优化机制后,总结最佳实践助你写出高效同步代码:

(1)减小锁粒度

尽量缩小同步代码块范围,仅同步必要代码,减少锁持有时间:

代码语言:javascript
复制
代码语言:javascript
复制
// 不推荐:同步整个方法
public synchronized void process() {
    // 非同步操作1
    // 同步操作
    // 非同步操作2
}

// 推荐:仅同步必要代码块
public void process() {
    // 非同步操作1
    synchronized (lock) {
        // 同步操作
    }
    // 非同步操作2
}
(2)避免锁竞争

多线程访问不同资源时,使用不同锁对象,避免竞争同一锁。如 ConcurrentHashMap 的分段锁,不同段用不同锁。

(3)利用偏向锁优化单线程场景

频繁被单线程访问的同步资源,确保偏向锁启用(默认开启),通过-XX:+UseBiasedLocking控制,获最佳性能。

(4)避免在同步代码块中执行耗时操作

长时间持有锁导致其他线程阻塞,增加响应时间。避免在同步块中执行 IO、复杂计算等耗时任务:

代码语言:javascript
复制
代码语言:javascript
复制
// 不推荐:同步块中执行耗时操作
synchronized (lock) {
    // 数据库查询(耗时IO)
    // 复杂计算
}

// 推荐:耗时操作移至同步块外
// 先获取必要信息
synchronized (lock) {
    // 获取需同步的数据
}
// 执行耗时操作
(5)避免嵌套锁

嵌套锁易导致死锁,增加竞争复杂度。尽量避免,必须使用时确保所有线程获取锁顺序一致。

(6)使用 Concurrent 集合替代同步集合

集合并发访问优先用java.util.concurrent包下的并发集合(如 ConcurrentHashMap、CopyOnWriteArrayList),性能通常优于synchronized包装的同步集合(如 Collections.synchronizedMap)。

(7)合理使用 volatile

简单变量同步,volatile提供更轻量同步机制。但volatile不保证原子性,适用于读远多于写的场景。

六、总结与展望

synchronized作为 Java 并发编程基石,经多年优化成为高效可靠的同步工具。从早期依赖重量级锁的低效实现,到引入偏向锁、轻量级锁、锁消除、锁粗化等优化,性能大幅提升。

随着 Java 版本更新,synchronized优化持续进行。如 Java 15 的 ZGC 进一步优化锁与 GC 交互;未来版本可能引入更多优化技术,使其在并发编程中发挥更大作用。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-08-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 果酱带你啃java 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 从底层到优化:深入理解 synchronized 的 “锁” 事(面试必看)
    • 一、synchronized 的基础:你真的会用吗?
      • 1.1 synchronized 的三种使用方式
      • 1.2 为什么需要 synchronized?
    • 二、深入底层:synchronized 的实现原理
      • 2.1 Java 对象头:锁的 “身份证”
      • 2.2 Monitor:重量级锁的核心
      • 2.3 synchronized 的字节码实现
    • 三、锁的升级:从偏向锁到重量级锁
      • 3.1 无锁状态
      • 3.2 偏向锁:减少无竞争开销
      • 3.3 轻量级锁:CAS 避免重量级锁
      • 3.4 重量级锁:依赖操作系统的互斥量
      • 3.5 锁升级的完整流程
    • 四、JVM 的锁优化:让 synchronized 更快
      • 4.1 锁消除
      • 4.2 锁粗化
      • 4.3 适应性自旋锁
      • 4.4 偏向锁批量重偏向与批量撤销
    • 五、实战案例:synchronized 的性能对比与最佳实践
      • 5.1 不同锁状态的性能对比
      • 5.2 synchronized vs ReentrantLock:如何选择?
      • 5.3 最佳实践:写出高效的同步代码
    • 六、总结与展望
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档