synchronized

82 阅读4分钟

一、synchronized 的核心作用

synchronized 是 Java 中实现线程同步的内置锁机制,提供以下保障:

  1. 原子性:同一时刻仅一个线程能执行同步代码块。
  2. 可见性:锁释放时,所有共享变量的修改对其他线程可见。
  3. 有序性:同步代码块内的指令不会被重排序到块外。

二、synchronized 的三种使用方式

1. 修饰实例方法

  • 锁对象:当前实例对象(this)。
  • 适用场景:保护非静态变量的并发访问。
public class Counter {
    private int count = 0;
    
    public synchronized void increment() {
        count++; // 原子操作
    }
}

2. 修饰静态方法

  • 锁对象:类的 Class 对象(如 Counter.class)。
  • 适用场景:保护静态变量或类级别操作。
public class Logger {
    private static int logCount = 0;
    
    public static synchronized void log() {
        logCount++; // 原子操作
    }
}

3. 修饰代码块

  • 锁对象:显式指定的任意对象(通常使用 this 或私有锁对象)。
  • 适用场景:减少锁粒度,提升并发性能。
public class Cache {
    private final Object lock = new Object();
    private Map<String, String> data = new HashMap<>();
    
    public void put(String key, String value) {
        synchronized (lock) { // 锁定私有对象,避免暴露锁
            data.put(key, value);
        }
    }
}

三、synchronized 的实现原理

1. 对象头与 Monitor

  • 对象头结构:每个 Java 对象在内存中包含一个对象头,其中 Mark Word 字段存储锁状态信息。

    • 无锁状态:存储哈希码、分代年龄等。
    • 偏向锁/轻量级锁/重量级锁:存储指向 Monitor 的指针或线程 ID。
  • Monitor(监视器锁)

    • 每个对象关联一个 Monitor,通过 ObjectMonitor 实现(C++ 结构)。
    • 包含 _owner(持有锁的线程)、_EntryList(竞争锁的线程队列)、_WaitSet(等待队列)等字段。

2. 锁升级过程

为提高性能,JVM 对 synchronized 锁进行优化,经历以下阶段:

  1. 偏向锁

    • 适用场景:单线程重复访问同步块。
    • 机制:在对象头记录线程 ID,避免 CAS 操作。
    • 升级条件:检测到其他线程竞争时(通过 epoch 机制)。
  2. 轻量级锁(自旋锁)

    • 适用场景:多线程竞争不激烈(短时间等待)。
    • 机制:线程通过 CAS 将对象头替换为锁记录指针,失败后自旋重试。
    • 升级条件:自旋超过阈值(默认 10 次)或竞争加剧。
  3. 重量级锁

    • 适用场景:高并发竞争。
    • 机制:线程进入阻塞状态,依赖操作系统 Mutex Lock 调度。
    • 缺点:上下文切换开销大。

四、synchronized 与 JMM 的关系

1. 可见性保障

  • 锁释放(unlock) : 线程退出同步块时,强制将工作内存的修改刷新到主内存。
  • 锁获取(lock) : 线程进入同步块时,清空工作内存,从主内存重新加载共享变量。

2. 有序性保障

  • as-if-serial 语义:同步块内的代码不会被重排序到块外。
  • 内存屏障:插入 LoadLoadLoadStoreStoreStoreStoreLoad 屏障。

五、synchronized 的优缺点

优点

  • 简单易用:语法简洁,无需手动释放锁。
  • JVM 优化:锁升级机制减少性能开销。
  • 稳定性:Java 语言原生支持,兼容性好。

缺点

  • 不可中断:线程等待锁时无法响应中断(InterruptedException)。
  • 灵活性不足:不支持尝试获取锁(tryLock)或超时机制。
  • 锁粒度固定:若使用不当(如锁住大对象),可能导致性能问题。

六、synchronized 的优化实践

1. 减小锁粒度

  • 反例:锁住整个方法或大对象。
  • 正例:使用细粒度锁(如 ConcurrentHashMap 的分段锁)。
public class FineGrainedLock {
    private final Object[] locks;
    private int[] data;
    
    public FineGrainedLock(int size) {
        locks = new Object[size];
        data = new int[size];
        for (int i = 0; i < size; i++) {
            locks[i] = new Object();
        }
    }
    
    public void update(int index, int value) {
        synchronized (locks[index]) { // 仅锁定特定段
            data[index] = value;
        }
    }
}

2. 避免死锁

  • 按顺序加锁:统一锁的获取顺序。
  • 超时回退:结合 try-catchwait(timeout) 实现。
public void transfer(Account from, Account to, int amount) {
    int fromHash = System.identityHashCode(from);
    int toHash = System.identityHashCode(to);
    
    // 按哈希值顺序加锁
    if (fromHash < toHash) {
        synchronized (from) {
            synchronized (to) {
                from.withdraw(amount);
                to.deposit(amount);
            }
        }
    } else if (fromHash > toHash) {
        synchronized (to) {
            synchronized (from) {
                from.withdraw(amount);
                to.deposit(amount);
            }
        }
    } else {
        // 哈希冲突时使用额外锁
        synchronized (tieLock) {
            synchronized (from) {
                synchronized (to) {
                    from.withdraw(amount);
                    to.deposit(amount);
                }
            }
        }
    }
}

七、synchronized 与其他同步工具对比

特性synchronizedReentrantLock(基于 AQS)volatile
锁类型内置锁显式锁无锁,仅内存语义
中断支持不支持支持 lockInterruptibly()不适用
尝试获取锁不支持支持 tryLock()不适用
公平性非公平支持公平/非公平不适用
性能JVM 优化后接近显式锁灵活但需手动管理轻量级
适用场景简单同步需求复杂同步控制(如条件变量)状态标志、DCL 单例

八、总结

synchronized 是 Java 并发编程的基石,通过内置锁机制保障原子性、可见性和有序性。其锁升级优化(偏向锁 → 轻量级锁 → 重量级锁)在多数场景下平衡了性能与安全性。 最佳实践

  • 优先使用 synchronized 处理简单同步需求。
  • 结合锁粒度优化和顺序加锁避免死锁。
  • 在需要高级功能(如超时、中断)时,选择 ReentrantLock

理解其底层原理(如对象头、Monitor)和 JMM 内存语义,有助于编写高效、线程安全的并发代码。