【java】synchronized

59 阅读6分钟

synchronized 实现原理

synchronized 是 Java 中最基本的线程同步机制,其实现原理涉及 JVM 底层机制和操作系统交互。以下是其核心实现原理:


## 1. 基本实现层次

`synchronized` 的实现分为三个层次,根据竞争情况会逐步升级(锁膨胀):

1.  **偏向锁 (Biased Locking)**  - 无竞争时的优化
1.  **轻量级锁 (Thin Lock)**  - 轻度竞争时的优化
1.  **重量级锁 (Heavyweight Lock)**  - 高竞争时的实现

## 2. 对象头与 Mark Word

每个 Java 对象在内存中的布局包含对象头,其中 **Mark Word** 是实现同步的关键:

```text
|---------------------------------------------------|--------------------|
|                  Mark Word (64 bits)              |       State        |
|---------------------------------------------------|--------------------|
| unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | 0 | Normal(无锁)
|---------------------------------------------------|--------------------|
| thread:54 |       epoch:2        | unused:1 | age:4 | biased_lock:1 | 1 | Biased(偏向锁)
|---------------------------------------------------|--------------------|
|               ptr_to_lock_record:62               |              00    | Lightweight Locked(轻量级锁)
|---------------------------------------------------|--------------------|
|               ptr_to_heavyweight_monitor:62       |              10    | Heavyweight Locked(重量级锁)
|---------------------------------------------------|--------------------|
|                                               |   11    | Marked for GC |
|---------------------------------------------------|--------------------|

3. 锁升级过程

3.1 偏向锁阶段

  • 适用场景:始终只有一个线程访问同步块

  • 实现

    • 在 Mark Word 中记录线程 ID
    • 后续进入同步块只需简单检查线程 ID
    • 无 CAS 操作,性能最高
  • 退出:不实际释放锁,只是离开同步块

3.2 轻量级锁阶段

  • 触发条件:有第二个线程尝试获取锁(发生竞争)

  • 实现

    1. JVM 在当前线程栈帧中创建 Lock Record
    2. 通过 CAS 将 Mark Word 复制到 Lock Record(Displaced Mark Word)
    3. 尝试用 CAS 将 Mark Word 替换为指向 Lock Record 的指针
  • 成功:获得轻量级锁

  • 失败:说明存在竞争,升级为重量级锁

3.3 重量级锁阶段

  • 实现

    • 指向操作系统级别的互斥量(mutex)
    • 未获取锁的线程进入阻塞状态
    • 依赖操作系统的线程调度
  • 特点

    • 涉及用户态到内核态的切换
    • 性能开销大
    • 支持 wait/notify 机制

4. 同步代码块实现

对于同步代码块,JVM 使用 monitorenter 和 monitorexit 指令:

public void syncMethod() {
    synchronized(this) {
        // 代码块
    }
}

编译后的字节码:

aload_0          // 加载this引用
dup              // 复制引用
astore_1         // 存储引用到局部变量1
monitorenter     // 进入监视器(获取锁)
// 同步代码块
aload_1          // 加载局部变量1
monitorexit      // 退出监视器(释放锁)
goto 14
// 异常处理路径
aload_1
monitorexit      // 确保异常时也能释放锁
athrow

5. 同步方法实现

对于同步方法,JVM 使用 ACC_SYNCHRONIZED 标志:

public synchronized void syncMethod() {
    // 方法体
}
  • 方法调用时检查 ACC_SYNCHRONIZED 标志
  • 自动获取对象锁(实例方法)或类锁(静态方法)
  • 方法返回/异常时自动释放锁

6. 性能优化技术

  1. 锁消除 (Lock Elision)

    • 逃逸分析确定对象不会逃逸时,移除不必要的同步
  2. 锁粗化 (Lock Coarsening)

    • 将相邻的同步块合并,减少锁获取/释放次数
  3. 自适应自旋 (Adaptive Spinning)

    • 在轻量级锁竞争时,线程执行忙等待(自旋)
    • 自旋次数根据历史成功率动态调整

7. 与 ReentrantLock 对比

特性synchronizedReentrantLock
实现机制JVM 内置实现JDK 代码实现 (AQS)
锁获取方式隐式获取/释放显式 lock()/unlock()
灵活性有限支持公平锁、条件变量等
性能Java 6+ 优化后相当竞争激烈时可能更好
可中断性不支持支持 lockInterruptibly()
超时机制不支持支持 tryLock(timeout)

现代 JVM 对 synchronized 进行了大量优化,在大多数场景下其性能已与 ReentrantLock 相当,但后者提供了更灵活的锁操作方式。

8. 使用方式:主要有4种:

1:用在代码块上:
 public void test() {
        //用在代码块上
        synchronized (this) {
            // do sth
        }
    }
2:用在对象上:
    public void test2() {
        // 用在对象上
        synchronized (object) {
            //  do sth
        }
    }
3:用在方法上:
 public synchronized void test3() {
        // do sth
    }

4:用在静态方法上:(如单例模式)
 public static void test4() {
        synchronized (Demo_1.class) {
            // do sth
        }
    }

9. synchronized 与 wait()/notify() 的协同工作机制

synchronized 配合 wait() 和 notify()/notifyAll() 实现了 Java 中最基础的线程间协作机制,这种组合构成了 Java 内置的 监视器模式(Monitor Pattern)。

1. 基本关系

  • 锁对象的三位一体

    • 每个 Java 对象都关联:

      1. 一个 (用于同步)
      2. 一个 等待队列(用于 wait() 的线程)
      3. 一个 入口队列(竞争锁的线程)
  • 方法来源

    • wait()/notify() 是 Object 类的方法
    • 必须在 synchronized 块内调用(否则抛出 IllegalMonitorStateException

2. 核心方法解析

2.1 wait() 方法

public final void wait() throws InterruptedException;

作用

  • 释放当前持有的锁
  • 使当前线程进入 WAITING 状态
  • 将线程放入对象的等待队列

特点

  • 调用前必须持有对象锁
  • 被唤醒后需要重新竞争锁
  • 通常配合条件检查使用(避免虚假唤醒)

标准使用模式

synchronized(lockObj) {
    while(!condition) {  // 必须用while循环检查条件
        lockObj.wait();
    }
    // 条件满足后执行操作
}

2.2 notify() 方法

public final void notify();

作用

  • 随机唤醒一个在该对象上等待的线程
  • 被唤醒的线程从等待队列移到入口队列
  • 不立即释放锁,要等到同步块结束

特点

  • 调用前必须持有对象锁
  • 不保证唤醒哪个等待线程
  • 被唤醒线程需要重新获取锁才能继续执行

2.3 notifyAll()

public final void notifyAll();

与 notify() 的区别

  • 唤醒所有在该对象上等待的线程
  • 所有被唤醒线程竞争锁
  • 适用于多条件谓词的情况

3. 工作流程示例

生产者-消费者模型示例:

class Buffer {
    private Queue<Integer> queue = new LinkedList<>();
    private int capacity;
    
    public Buffer(int capacity) {
        this.capacity = capacity;
    }
    
    public synchronized void produce(int item) throws InterruptedException {
        while(queue.size() == capacity) {
            wait();  // 缓冲区满时等待
        }
        queue.add(item);
        notifyAll(); // 唤醒可能等待的消费者
    }
    
    public synchronized int consume() throws InterruptedException {
        while(queue.isEmpty()) {
            wait();  // 缓冲区空时等待
        }
        int item = queue.remove();
        notifyAll(); // 唤醒可能等待的生产者
        return item;
    }
}

4. 关键实现原理

4.1 等待队列管理

  • 每个对象关联一个  _WaitSet 队列
  • wait() 将线程加入此队列
  • notify() 从队列中移出线程

4.2 锁释放与重新获取

  • wait() 调用时:

    • 原子性地释放锁
    • 将线程状态改为 WAITING
    • 进入等待队列
  • 被唤醒后:

    • 从 WAITING 变为 BLOCKED
    • 尝试重新获取锁(可能竞争失败继续等待)

4.3 与 synchronized 的交互

synchronized(obj) {       // 1. 获取锁成功
    while(condition) {
        obj.wait();       // 2. 释放锁并等待
    }                     // 4. 重新获取锁后继续
}                         // 5. 释放锁

5. 使用注意事项

  1. 必须持有锁

    • 调用 wait()/notify() 前必须获取对象锁
    • 否则抛出 IllegalMonitorStateException
  2. 条件检查

    • 必须用 while 而不是 if 检查条件
    • 防止虚假唤醒(spurious wakeup)
  3. 优先使用 notifyAll()

    • notify() 可能引起"信号劫持"
    • 除非能确保只唤醒正确类型的线程
  4. 超时机制

    • 使用 wait(long timeout) 避免永久等待
    • 结合系统时间检查实现更精确的超时
  5. 中断处理

    • wait() 可能抛出 InterruptedException
    • 需要合理处理中断

6. 现代替代方案

虽然 synchronized + wait/notify 是基础机制,但现代开发更推荐:

  1. java.util.concurrent 包

    • ReentrantLock + Condition
    • 提供更灵活的等待/通知机制
  2. 高级同步工具

    • CountDownLatch
      • 表示剩余需要count down的次数(初始值为构造函数指定的计数)
      • 递减计数,当state=0时释放所有等待线程
    • CyclicBarrier
      • 表示尚未到达屏障的线程数(初始值为构造函数指定的参与线程数)
      • 递减计数,当state=0时触发屏障动作并重置
    • Semaphore
      • 表示当前可用的许可数(初始值为构造函数指定的许可数)
      • 获取许可时递减,释放许可时递增
    • BlockingQueue