Java 并发编程:synchronized 到 ReentrantLock 的演进

2 阅读2分钟

面试被问"synchronized 和 ReentrantLock 的区别",背了几条区别但说不清"为什么要搞两个"。从头梳理一下 Java 锁的演进。

synchronized — 最简单的锁

public class Counter {
    private int count = 0;

    // 方法锁:锁的是 this 对象
    public synchronized void increment() {
        count++;
    }

    // 代码块锁:可以精确控制锁的范围
    public void doSomething() {
        // 这里不需要锁
        synchronized (this) {
            count++;
        }
        // 这里也不需要
    }

    // 静态方法锁:锁的是 Class 对象
    public static synchronized void staticMethod() {
        // ...
    }
}

优点:简单,自动释放(不用手动 unlock)。 缺点:不够灵活——不能中断、不能超时、只能非公平。

ReentrantLock — 更灵活的锁

public class Counter {
    private int count = 0;
    private final ReentrantLock lock = new ReentrantLock();

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();  // 必须在 finally 里释放!
        }
    }

    // 可中断的锁
    public void incrementInterruptibly() throws InterruptedException {
        lock.lockInterruptibly();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }

    // 带超时的锁
    public boolean tryIncrement() {
        if (lock.tryLock(1, TimeUnit.SECONDS)) {
            try {
                count++;
                return true;
            } finally {
                lock.unlock();
            }
        }
        return false; // 1 秒内没拿到锁
    }
}

对比

synchronizedReentrantLock
使用方式关键字,自动释放API,手动 lock/unlock
可中断不可中断lockInterruptibly()
超时获取不支持tryLock(timeout)
公平锁不支持new ReentrantLock(true)
条件变量只有一个(wait/notify)可以有多个 Condition
性能JDK 6 后差不多差不多

公平锁 vs 非公平锁

// 非公平锁(默认):新来的线程可能直接抢到锁
ReentrantLock unfairLock = new ReentrantLock();

// 公平锁:按排队顺序来,先到先得
ReentrantLock fairLock = new ReentrantLock(true);

公平锁的代价是吞吐量下降——每次都要检查队列里有没有人在等。大部分场景用非公平锁就够了。

Condition — 比 wait/notify 更精确

public class BoundedQueue<T> {
    private final List<T> list = new ArrayList<>();
    private final int capacity;
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();   // 生产者等这里
    private final Condition notEmpty = lock.newCondition();  // 消费者等这里

    public BoundedQueue(int capacity) {
        this.capacity = capacity;
    }

    public void put(T item) throws InterruptedException {
        lock.lock();
        try {
            while (list.size() == capacity) {
                notFull.await();  // 满了,等"不满"的信号
            }
            list.add(item);
            notEmpty.signal();    // 通知消费者:有数据了
        } finally {
            lock.unlock();
        }
    }

    public T take() throws InterruptedException {
        lock.lock();
        try {
            while (list.isEmpty()) {
                notEmpty.await(); // 空的,等"不空"的信号
            }
            T item = list.remove(0);
            notFull.signal();     // 通知生产者:有空位了
            return item;
        } finally {
            lock.unlock();
        }
    }
}

synchronized 只有一个条件队列(wait/notifyAll),生产者和消费者互相干扰。ReentrantLock 可以有多个 Condition,各等各的。

ReadWriteLock — 读多写少场景

ReadWriteLock rwLock = new ReentrantReadWriteLock();
Lock readLock = rwLock.readLock();
Lock writeLock = rwLock.writeLock();

// 读锁:多个线程可以同时持有
public String getData() {
    readLock.lock();
    try {
        return data;
    } finally {
        readLock.unlock();
    }
}

// 写锁:独占
public void updateData(String newData) {
    writeLock.lock();
    try {
        data = newData;
    } finally {
        writeLock.unlock();
    }
}

读读不互斥、读写互斥、写写互斥。适合缓存这种读多写少的场景。

实际选择

简单同步 → synchronized(够用就行)
需要超时/中断/公平 → ReentrantLock
读多写少 → ReadWriteLock
只是原子操作 → AtomicXxx(比锁更轻量)
// 原子操作,不需要锁
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();  // 线程安全,比 synchronized 快得多

AtomicReference<User> userRef = new AtomicReference<>();
userRef.compareAndSet(oldUser, newUser);  // CAS 操作

总结

Java 锁的演进:synchronized(简单够用)→ ReentrantLock(灵活:可中断、超时、公平)→ ReadWriteLock(读多写少)→ AtomicXxx(无锁原子操作)。新项目优先用 java.util.concurrent 包里的工具,synchronized 留给简单场景。