从synchronized到ReentrantLock,我终于理清了Java锁的那些事

从synchronized到ReentrantLock,我终于理清了Java锁的那些事

Java 并发编程里,锁是最常用的同步机制。但 synchronized 和 ReentrantLock 有什么区别?什么时候该用哪个?锁升级是怎么回事?偏向锁为什么被废弃?

这些问题在面试里经常被问到,但在实际项目中踩过坑才真正理解。

synchronized 的演进

很多人以为 synchronized 就是最老的那套重量级锁实现,性能很差。这是个误解。

JVM 对 synchronized 做了大量优化,包括偏向锁、轻量级锁、适应性自旋等。

锁的四种状态

synchronized 有四种锁状态,级别从低到高:

  1. 无锁:没有线程访问共享资源
  2. 偏向锁:只有一个线程反复进入同步块
  3. 轻量级锁:多个线程交替进入同步块,没有并发争抢
  4. 重量级锁:多个线程并发争抢共享资源

锁只能升级,不能降级(除非偏向锁被撤销后可以重偏向)。

偏向锁的原理

当一个线程第一次进入同步块时,在对象头的 Mark Word 里记录这个线程的 ID。之后这个线程再进入同步块,只需要比较 Mark Word 里的线程 ID,不需要加锁和释放锁。

public class偏向锁演示 {
    private final Object lock = new Object();

    public void method() {
        synchronized (lock) {
            // 第一次进入:记录线程ID到Mark Word
            // 之后进入:比较Mark Word中的线程ID
        }
    }
}

偏向锁的优点是:如果同步块一直被同一个线程访问,这个线程几乎零成本。

偏向锁的问题

偏向锁在多线程并发时会有问题:如果一个线程获得偏向锁后,另一个线程尝试进入同步块,需要撤销偏向锁。这个过程会 Stop The World(STW),在某些场景下反而更慢。

所以 JDK 15 废弃了偏向锁,JDK 18 直接移除了。

轻量级锁

当有多个线程竞争时,JVM 会尝试用 CAS(Compare And Swap)把锁升级为轻量级锁。

线程在进入同步块前,在自己的栈帧里创建一个锁记录(Lock Record),然后用 CAS 把对象头的 Mark Word 指向这个锁记录:

// 轻量级锁加锁过程(伪代码)
public void lock() {
    // 1. 在当前线程栈帧创建锁记录
    LockRecord lockRecord = new LockRecord();

    // 2. 用CAS尝试把对象头的Mark Word更新为指向锁记录的指针
    if (casUpdateMarkWord(lockRecord)) {
        // 成功:当前线程获得了轻量级锁
    } else {
        // 失败:可能有其他线程在竞争,升级为重量级锁
        inflateToHeavyweightLock();
    }
}

轻量级锁适用于“线程交替执行同步块”的场景,没有实际的操作系统 mutex 参与。

重量级锁

当轻量级锁 CAS 失败,或者有线程在持有轻量级锁时阻塞,锁会膨胀为重量级锁。

重量级锁依赖操作系统的 Mutex 实现,需要用户态到内核态的切换,开销较大。

// synchronized 默认的锁升级过程
public class锁升级演示 {
    private final Object lock = new Object();

    public void method() {
        synchronized (lock) {
            // 第一次:无锁 → 偏向锁(记录线程ID)
            // 第二次进入:偏向锁(比较线程ID)
            // 有其他线程竞争:偏向锁 → 轻量级锁(CAS)
            // CAS失败或 contention:轻量级锁 → 重量级锁
        }
    }
}

ReentrantLock vs synchronized

基本用法对比

// synchronized 用法
public class SynchronizedDemo {
    private final Object lock = new Object();

    public void method() {
        synchronized (lock) {
            // 临界区
        }
    }
}

// ReentrantLock 用法
public class ReentrantLockDemo {
    private final ReentrantLock lock = new ReentrantLock();

    public void method() {
        lock.lock();
        try {
            // 临界区
        } finally {
            lock.unlock();  // 必须手动释放
        }
    }
}

关键区别

特性synchronizedReentrantLock
锁释放自动释放必须手动 unlock()
尝试获取阻塞tryLock() 可非阻塞尝试
超时等待不支持tryLock(timeout) 支持
公平锁不支持可配置公平/非公平
可中断不支持lockInterruptibly() 支持
条件变量newCondition() 支持多个

tryLock 非阻塞获取

ReentrantLock 支持非阻塞获取锁:

public class TryLockDemo {
    private final ReentrantLock lock = new ReentrantLock();

    public void method() {
        // 尝试获取锁,立即返回
        if (lock.tryLock()) {
            try {
                // 获取成功
            } finally {
                lock.unlock();
            }
        } else {
            // 获取失败,做降级处理
            fallback();
        }
    }
}

tryLock 带超时

获取锁时设置超时,避免无限等待:

public class TryLockTimeoutDemo {
    private final ReentrantLock lock = new ReentrantLock();

    public void method() {
        try {
            // 等待3秒,还拿不到就放弃
            if (lock.tryLock(3, TimeUnit.SECONDS)) {
                try {
                    // 临界区
                } finally {
                    lock.unlock();
                }
            } else {
                // 超时后的降级处理
                handleLockTimeout();
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            handleInterrupt();
        }
    }
}

synchronized 无法实现超时,线程只能阻塞等锁。

可中断锁

lockInterruptibly() 允许在等待过程中响应中断:

public class InterruptibleLockDemo {
    private final ReentrantLock lock = new ReentrantLock();

    public void method() throws InterruptedException {
        lock.lockInterruptibly();
        try {
            // 临界区
        } finally {
            lock.unlock();
        }
    }
}

如果线程被中断,lockInterruptibly() 会抛出 InterruptedException,而不是继续等待。

公平锁

ReentrantLock 可以创建公平锁,按等待顺序分配锁:

// 公平锁:按等待顺序获取锁
private final ReentrantLock fairLock = new ReentrantLock(true);

// 非公平锁(默认):允许插队,可能饥饿
private final ReentrantLock unfairLock = new ReentrantLock(false);

公平锁的开销比非公平锁大,因为需要维护一个等待队列。一般情况下用非公平锁即可。

多个条件变量

synchronized 只有一个条件队列(wait/notify),ReentrantLock 可以创建多个条件变量:

public class ConditionDemo {
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();   // 队列不满
    private final Condition notEmpty = lock.newCondition(); // 队列不空

    private final Object[] items = new Object[100];
    private int count = 0;

    public void put(Object item) throws InterruptedException {
        lock.lock();
        try {
            while (count == items.length) {
                notFull.await();  // 队列满,等待
            }
            items[count++] = item;
            notEmpty.signal();  // 唤醒消费者
        } finally {
            lock.unlock();
        }
    }

    public Object take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0) {
                notEmpty.await();  // 队列空,等待
            }
            Object item = items[--count];
            notFull.signal();  // 唤醒生产者
            return item;
        } finally {
            lock.unlock();
        }
    }
}

这实现了一个有界阻塞队列,生产者和消费者用不同的条件变量等待。

读写锁优化并发读

如果业务场景是读多写少,用 ReentrantReadWriteLock 可以进一步提升性能:

public class CacheDemo {
    private final Map<String, String> cache = new HashMap<>();
    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

    // 读操作:共享锁,多线程可以并发读
    public String get(String key) {
        rwLock.readLock().lock();
        try {
            return cache.get(key);
        } finally {
            rwLock.readLock().unlock();
        }
    }

    // 写操作:独占锁
    public void put(String key, String value) {
        rwLock.writeLock().lock();
        try {
            cache.put(key, value);
        } finally {
            rwLock.writeLock().unlock();
        }
    }
}

读写锁的规则:

  • 读锁共享:多个线程可以同时持有读锁
  • 写锁独占:持有写锁时,其他线程不能持有任何锁
  • 读写互斥:读锁和写锁不能同时持有

什么时候用什么锁

用 synchronized 的场景

  1. 简单同步需求:只需要互斥,不需要超时、中断等高级特性
  2. 代码块很小:synchronized 会自动优化,短临界区影响小
  3. 不需要公平锁:synchronized 不支持公平策略
// synchronized 适用场景
public class SimpleCounter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }
}

用 ReentrantLock 的场景

  1. 需要超时或尝试获取:用 tryLock()
  2. 需要可中断等待:用 lockInterruptibly()
  3. 需要公平锁:用 new ReentrantLock(true)
  4. 需要多个条件变量:用 newCondition()
  5. 读多写少:用 ReentrantReadWriteLock
// ReentrantLock 适用场景
public class InventoryService {
    private final ReentrantLock lock = new ReentrantLock();
    private final Map<Long, Integer> inventory = new ConcurrentHashMap<>();

    public boolean deduct(Long productId, int quantity) {
        lock.lock();
        try {
            int stock = inventory.getOrDefault(productId, 0);
            if (stock < quantity) {
                return false;
            }
            inventory.put(productId, stock - quantity);
            return true;
        } finally {
            lock.unlock();
        }
    }
}

性能对比

在低并发场景下,synchronized 和 ReentrantLock 性能差别不大。

在高并发场景:

  • 无竞争:synchronized(偏向锁)性能最好
  • 轻度竞争:轻量级锁 CAS 性能好
  • 重度竞争:两者都需要 OS 介入,性能相近

JDK 16+ 对 synchronized 做了大量优化(锁膨胀优化、自适应偏向),在大多数场景下性能已经不输 ReentrantLock。

总结

Java 锁的核心知识点:

  1. synchronized 有锁升级:偏向锁 → 轻量级锁 → 重量级锁(偏向锁已被废弃)
  2. ReentrantLock 更灵活:支持超时、尝试获取、可中断、公平锁
  3. 读写锁适合读多写少:ReentrantReadWriteLock
  4. 选择依据:简单互斥用 synchronized,需要高级特性用 ReentrantLock

实际项目中,大多数 synchronized 的场景已经足够用。但如果需要更精细的锁控制,ReentrantLock 和 ReentrantReadWriteLock 是更好的选择。

最后更新 4/20/2026, 4:48:48 AM