文章中如果有图看不到,可以点这里去 csdn 看看。从那边导过来的,文章太多,没法一篇篇修改好。

Java 锁相关详解【二、synchronized:Java 内置锁的全方位解析】

synchronized:Java 内置锁的全方位解析

在 Java 并发编程领域,synchronized 是最基础、最经典的同步手段。它伴随 Java 从诞生到如今,经历了从“性能低下”到“高度优化”的演变。本文将系统地拆解 synchronized 的语义、实现机制、性能优化与实践问题,帮助读者对这一核心锁机制形成全面、深入的理解。


一、synchronized 的语义与用法

1.1 基本语义

synchronized 关键字实现了 互斥(Mutual Exclusion)可见性(Visibility),是 Java 提供的 内置锁(Intrinsic Lock / Monitor Lock)

其作用可以归纳为:

  1. 互斥访问:保证同一时刻只有一个线程进入临界区。
  2. 内存语义:保证进入和退出临界区的线程在内存可见性上符合 JMM(Java 内存模型)的要求。

1.2 使用方式

  • 修饰实例方法:锁住当前实例对象。

    public synchronized void increment() {
        count++;
    }
    
  • 修饰静态方法:锁住当前类的 Class 对象。

    public static synchronized void log() {
        System.out.println("Log...");
    }
    
  • 修饰代码块:锁住指定对象引用。

    synchronized (lock) {
        list.add("item");
    }
    

注意:锁的真正粒度是 对象引用,而不是代码块本身。每个对象都可以作为一把锁。


二、JVM 层实现原理

2.1 字节码指令

synchronized 通过字节码中的 monitorentermonitorexit 指令实现:

  • monitorenter:线程请求获取锁。
  • monitorexit:线程释放锁。

例如:

synchronized (this) {
    count++;
}

编译后的字节码(简化):

monitorenter
  iload_1
  iinc
  istore_1
monitorexit

JVM 会保证 monitorexit 一定执行,即使方法抛出异常,否则会导致锁泄漏。


2.2 对象头(Mark Word)

在 HotSpot 虚拟机中,每个对象都有一个对象头,其中 Mark Word 用来存储锁相关状态。

锁状态Mark Word 内容(32位为例)
无锁哈希码、GC 分代年龄等
偏向锁线程 ID、epoch 时间戳
轻量级锁指向栈中 Lock Record 的指针
重量级锁指向 Monitor 对象的指针

2.3 Monitor 机制

当锁升级到重量级锁时,JVM 会使用 ObjectMonitor 来管理:

  • EntryList:等待获取锁的线程队列。
  • WaitSet:执行 wait() 后等待唤醒的线程队列。

Monitor 底层依赖操作系统的 互斥量(mutex)和条件变量(condvar),涉及用户态 ↔ 内核态切换,开销较大。


三、锁的升级过程

JDK 1.6 引入了 锁升级(Lock Escalation) 机制,通过多层锁状态减少无谓的开销。

出现竞争
自旋失败
无锁
偏向锁
轻量级锁
重量级锁

3.1 无锁(Unlocked)

  • 初始状态,没有任何竞争。

3.2 偏向锁(Biased Locking)

  • 对象第一次被某线程获取时,将 Mark Word 绑定该线程 ID。
  • 同一线程再次进入临界区时,无需 CAS 竞争,性能极高。
  • 当有其他线程竞争时,偏向锁会撤销,升级为轻量级锁。

3.3 轻量级锁(Lightweight Locking)

  • JVM 在栈上创建 Lock Record,尝试用 CAS 把对象头指向它。
  • 如果 CAS 成功,获得锁;若失败则自旋尝试。
  • 若自旋失败(竞争激烈),升级为重量级锁。

3.4 重量级锁(Heavyweight Locking)

  • 通过 ObjectMonitor 管理,线程进入阻塞/唤醒。
  • 保证安全,但性能最差。

四、内存语义

synchronized 在 JMM 中具备明确的内存语义:

  1. 加锁(monitorenter)

    • 清空工作内存,强制从主内存中读取共享变量的最新值。
  2. 解锁(monitorexit)

    • 将工作内存中的变量刷新到主内存,保证其他线程可见。

这意味着 synchronized 同时提供:

  • 原子性(互斥访问)
  • 可见性(写入刷新、读取加载)
  • 有序性(禁止指令重排序)

例如,在 DCL(双重检查锁定单例模式)中,若没有 synchronized 的内存语义,可能会出现“指令重排导致未初始化对象被访问”的问题。


五、JVM 优化策略

现代 JDK 已经对 synchronized 做了深度优化,使其性能不再是瓶颈。

  1. 锁消除(Lock Elimination)
    JIT 通过逃逸分析,消除只在当前线程可见的锁。

    public void foo() {
        Object o = new Object();
        synchronized (o) {
            // 没有线程间共享,锁会被优化掉
        }
    }
    
  2. 锁粗化(Lock Coarsening)
    多次短小的加锁操作会被合并成一次更大范围的加锁,减少频繁进入/退出锁的开销。

  3. 自旋锁与自适应自旋

    • 在轻量级锁下,CAS 失败时会先自旋几次而不是立刻阻塞。
    • JVM 根据历史情况自适应调整自旋次数,避免浪费 CPU。
  4. 偏向锁延迟

    • JVM 在应用启动几秒后再开启偏向锁,以避免冷启动阶段频繁撤销。

六、常见问题与实践

6.1 死锁

synchronized (lockA) {
    synchronized (lockB) {
        // ...
    }
}

若另一个线程反向加锁,会导致死锁。

解决方案

  • 保证获取锁顺序一致。
  • 使用可定时锁(ReentrantLock.tryLock())。

6.2 锁竞争

  • 临界区过大或写操作过多,可能严重影响性能。

优化思路:

  1. 减少锁粒度(例如分段锁)。
  2. 使用并发容器替代传统集合。
  3. 使用 ReadWriteLockStampedLock 提升读性能。

6.3 工程建议

  1. 优先使用 synchronized(JDK 1.6+ 性能足够,语义简洁)。
  2. 避免锁对象的可变性(推荐用 final Object lock = new Object();)。
  3. 临界区要尽量短小,避免长时间持锁。
  4. 对于复杂同步需求(如可中断、公平性),考虑使用 JUC 显式锁。

七、总结

  • synchronized 是 Java 并发的基石,提供 原子性、可见性、有序性 的保障。
  • 它依赖 字节码指令(monitorenter/monitorexit)+ 对象头 Mark Word + Monitor 实现。
  • 通过 偏向锁 → 轻量级锁 → 重量级锁 的升级机制,JVM 尽量减少锁竞争带来的开销。
  • 随着 JDK 的优化,synchronized 在现代应用中完全可以胜任高并发场景。

换句话说,synchronized 已经从“性能差”标签中走出,成为 语义清晰 + 高度优化 + 安全可靠 的同步工具。

posted @ 2025-09-04 16:47  NeoLshu  阅读(6)  评论(0)    收藏  举报  来源