Java 中的锁

更新于 2025-12-28

Jakob Jenkov 2014-06-23

锁是一种线程同步机制,类似于 synchronized 块,但锁可以比 Java 的 synchronized 块更复杂。锁(以及其他更高级的同步机制)是使用 synchronized 块实现的,因此我们并不能完全摆脱 synchronized 关键字。

从 Java 5 开始,java.util.concurrent.locks 包中包含了多个锁的实现,因此你可能不需要自己实现锁。但你仍然需要知道如何使用它们,并且了解其背后的实现原理仍然是有益的。


一个简单的锁(A Simple Lock)

我们先来看一段使用 synchronized 块的 Java 代码:

public class Counter {
    private int count = 0;

    public int inc() {
        synchronized(this) {
            return ++count;
        }
    }
}

注意 inc() 方法中的 synchronized(this) 块。该块确保同一时间只有一个线程能执行 return ++count。虽然同步块中的代码可以更复杂,但这里简单的 ++count 已足以说明问题。

也可以用 Lock 替代 synchronized 块来重写 Counter 类:

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

    public int inc() {
        lock.lock();
        int newCount = ++count;
        lock.unlock();
        return newCount;
    }
}

lock() 方法会锁定 Lock 实例,使得所有调用 lock() 的线程都被阻塞,直到执行了 unlock()

下面是一个简单的 Lock 实现:

public class Lock {
    private boolean isLocked = false;

    public synchronized void lock() throws InterruptedException {
        while (isLocked) {
            wait();
        }
        isLocked = true;
    }

    public synchronized void unlock() {
        isLocked = false;
        notify();
    }
}

注意 while(isLocked) 循环,也称为“自旋锁”(spin lock)。自旋锁以及 wait()notify() 方法在 线程通信(Thread Signaling) 一文中会有更详细的介绍。

isLockedtrue 时,调用 lock() 的线程会在 wait() 调用中被挂起等待。如果线程在未收到 notify() 调用的情况下意外地从 wait() 返回(即发生 虚假唤醒(Spurious Wakeup)),线程会重新检查 isLocked 条件,以判断是否安全继续执行,而不是假设被唤醒就意味着可以继续。如果 isLockedfalse,线程将退出 while(isLocked) 循环,并将 isLocked 重新设为 true,从而为其他调用 lock() 的线程锁定该 Lock 实例。

当线程完成临界区(critical section,即 lock()unlock() 之间的代码)的操作后,会调用 unlock()。执行 unlock() 会将 isLocked 重置为 false,并通知(唤醒)在 lock() 方法中 wait() 调用里等待的某个线程(如果有的话)。


锁的可重入性(Lock Reentrance)

Java 中的 synchronized 块是可重入的(reentrant)。这意味着,如果一个 Java 线程进入了一个同步代码块,从而获得了该同步块所关联监视器对象的锁,那么该线程可以进入其他同样基于该监视器对象同步的代码块。

示例如下:

public class Reentrant {
    public synchronized void outer() {
        inner();
    }

    public synchronized void inner() {
        // do something
    }
}

注意 outer()inner() 都声明为 synchronized,这在 Java 中等价于 synchronized(this) 块。如果一个线程调用了 outer(),那么从 outer() 内部调用 inner() 不会有任何问题,因为两个方法(或块)都是在同一个监视器对象(this)上同步的。如果一个线程已经持有了某个监视器对象的锁,它就有权访问所有在该监视器对象上同步的代码块。这被称为可重入性(reentrance)——线程可以重新进入它已持有锁的任何代码块。

然而,即使 synchronized 块是可重入的,前面展示的 Lock 类却不是可重入的。如果我们像下面这样重写 Reentrant 类,那么调用 outer() 的线程将在 inner() 方法中的 lock.lock() 处被阻塞:

public class Reentrant2 {
    Lock lock = new Lock();

    public void outer() {
        lock.lock();
        inner();
        lock.unlock();
    }

    public void inner() {
        lock.lock();
        // do something
        lock.unlock();
    }
}

调用 outer() 的线程首先会锁定 Lock 实例,然后调用 inner()。在 inner() 方法内部,该线程会再次尝试锁定同一个 Lock 实例。这将失败(即线程会被阻塞),因为在 outer() 方法中该 Lock 实例已经被锁定了。

之所以第二次调用 lock() 会被阻塞(在没有先调用 unlock() 的情况下),从 lock() 的实现中可以看出原因:

public class Lock {
    boolean isLocked = false;

    public synchronized void lock() throws InterruptedException {
        while (isLocked) {
            wait();
        }
        isLocked = true;
    }
    ...
}

决定线程是否允许退出 lock() 方法的条件就是 while 循环中的判断。当前的条件是:只有当 isLockedfalse 时才允许退出,而不管是谁锁定了它。

要使 Lock 类支持可重入,我们需要做一点小改动:

public class Lock {
    boolean isLocked = false;
    Thread lockedBy = null;
    int lockedCount = 0;

    public synchronized void lock() throws InterruptedException {
        Thread callingThread = Thread.currentThread();
        while (isLocked && lockedBy != callingThread) {
            wait();
        }
        isLocked = true;
        lockedCount++;
        lockedBy = callingThread;
    }

    public synchronized void unlock() {
        if (Thread.currentThread() == this.lockedBy) {
            lockedCount--;
            if (lockedCount == 0) {
                isLocked = false;
                notify();
            }
        }
    }
    ...
}

注意现在 while 循环(自旋锁)也考虑了锁定 Lock 实例的线程。如果锁未被占用(isLocked == false)或者当前调用线程就是持有该锁的线程,那么 while 循环就不会执行,调用 lock() 的线程将被允许退出该方法。

此外,我们还需要记录同一个线程对锁的加锁次数。否则,一次 unlock() 调用就会释放锁,即使该锁已被同一线程多次加锁。我们不希望锁被释放,除非持有它的线程执行了与 lock() 调用次数相等的 unlock() 调用。

经过上述修改,Lock 类现在就支持可重入了。


锁的公平性(Lock Fairness)

Java 的 synchronized不保证试图进入它的线程获得访问权限的顺序。因此,如果有多个线程持续竞争同一个 synchronized 块的访问权,就存在某些线程永远无法获得访问权的风险——访问权总是被授予其他线程。这种情况称为饥饿(starvation)。

为了避免饥饿,锁应当具备公平性(fairness)。由于本文展示的 Lock 实现在内部使用了 synchronized 块,因此它们不保证公平性。关于饥饿和公平性的更多讨论,请参见 饥饿与公平性(Starvation and Fairness)


在 finally 子句中调用 unlock()(Calling unlock() From a finally-clause)

当使用 Lock 保护临界区,且临界区代码可能抛出异常时,必须finally 子句中调用 unlock() 方法。这样做可以确保 Lock 被正确释放,以便其他线程可以获取它。

示例如下:

lock.lock();
try {
    // 执行临界区代码,可能会抛出异常
} finally {
    lock.unlock();
}

这个小结构确保了即使临界区代码抛出异常,Lock 也会被解锁。如果 unlock() 没有放在 finally 子句中,而临界区又抛出了异常,那么 Lock永远保持锁定状态,导致所有在该 Lock 实例上调用 lock() 的线程无限期地阻塞。