Java Synchronizer 同步器解剖

更新于 2025-12-29

Jakob Jenkov 2014-10-01

即使许多同步器(如锁、信号量、阻塞队列等)在功能上各不相同,它们的内部设计往往并没有那么大的差异。换句话说,它们通常由相同(或相似)的基本部分组成。了解这些基本组成部分,对于设计同步器将大有裨益。本文将深入探讨这些组成部分。

大多数同步器的目的,都是保护代码中的某个区域(临界区)免受多个线程的并发访问。为此,同步器通常需要以下组成部分:

  • 状态(State)
  • 访问条件(Access Condition)
  • 状态变更(State Changes)
  • 通知策略(Notification Strategy)
  • 测试并设置方法(Test and Set Method)
  • 设置方法(Set Method)

并非所有同步器都包含上述全部部分,即使包含,其具体实现也可能与本文描述略有不同。但通常你总能找到其中的一个或多个部分。


状态(State)

同步器的状态用于访问条件判断线程是否可以被授予访问权限。

  • Lock(锁) 中,状态以一个 boolean 变量表示,说明该锁当前是否已被锁定。
  • 有界信号量(Bounded Semaphore) 中,内部状态由一个计数器(int signals)和一个上限值(int bound)组成,分别表示当前“获取”次数和最大允许“获取”次数。
  • 阻塞队列(Blocking Queue) 中,状态由队列中元素的 List 和最大队列容量(int,如果有的话)共同表示。

以下是 LockBoundedSemaphore 的代码片段,其中状态部分已加粗:

public class Lock {
    // 状态保存在这里
    private boolean isLocked = false;

    public synchronized void lock() throws InterruptedException {
        while (isLocked) {
            wait();
        }
        isLocked = true;
    }
    ...
}
public class BoundedSemaphore {
    // 状态保存在这里
    private int signals = 0;
    private int bound = 0;

    public BoundedSemaphore(int upperBound) {
        this.bound = upperBound;
    }

    public synchronized void take() throws InterruptedException {
        while (this.signals == bound)
            wait();
        this.signals++;
        this.notify();
    }
    ...
}

访问条件(Access Condition)

访问条件决定了调用“测试并设置状态”方法的线程是否被允许修改状态。访问条件通常基于同步器的状态进行判断,并且通常在一个 while 循环中进行检查,以防止虚假唤醒(Spurious Wakeups)。访问条件的评估结果要么为 true,要么为 false

  • Lock 中,访问条件就是简单地检查 isLocked 成员变量的值。
  • 有界信号量(Bounded Semaphore) 中,实际上存在两个访问条件,取决于线程是尝试“获取”还是“释放”信号量:
    • 如果尝试获取,则将 signals 与上限 bound 比较;
    • 如果尝试释放,则将 signals 与 0 比较。

以下是 LockBoundedSemaphore 的代码片段,其中访问条件已加粗。注意,这些条件始终在 while 循环内进行检查。

public class Lock {
    private boolean isLocked = false;

    public synchronized void lock() throws InterruptedException {
        // 访问条件
        while (isLocked) {
            wait();
        }
        isLocked = true;
    }
    ...
}
public class BoundedSemaphore {
    private int signals = 0;
    private int bound = 0;

    public BoundedSemaphore(int upperBound) {
        this.bound = upperBound;
    }

    public synchronized void take() throws InterruptedException {
        // 访问条件
        while (this.signals == bound)
            wait();
        this.signals++;
        this.notify();
    }

    public synchronized void release() throws InterruptedException {
        // 访问条件
        while (this.signals == 0)
            wait();
        this.signals--;
        this.notify();
    }
}

状态变更(State Changes)

一旦线程获得对临界区的访问权限,它就必须更改同步器的状态,以(可能)阻止其他线程进入。换句话说,状态必须反映出当前有线程正在临界区内执行。这会影响其他试图获取访问权限的线程的访问条件。

  • Lock 中,状态变更是将 isLocked = true
  • 在信号量中,状态变更是 signals--signals++

以下是加粗标出状态变更部分的代码片段:

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();
    }
}
public class BoundedSemaphore {
    private int signals = 0;
    private int bound = 0;

    public BoundedSemaphore(int upperBound) {
        this.bound = upperBound;
    }

    public synchronized void take() throws InterruptedException {
        while (this.signals == bound)
            wait();
        // 状态变更
        this.signals++;
        this.notify();
    }

    public synchronized void release() throws InterruptedException {
        while (this.signals == 0)
            wait();
        // 状态变更
        this.signals--;
        this.notify();
    }
}

通知策略(Notification Strategy)

当一个线程更改了同步器的状态后,有时需要通知其他等待中的线程该状态已发生变化。因为这一变化可能会使其他线程的访问条件变为 true

通知策略通常分为三类:

  1. 通知所有等待线程
    所有等待线程都在同一个对象上调用 wait()。通知线程只需在该对象上调用 notifyAll()

  2. 通知 N 个等待线程中的一个随机线程
    通知线程在等待线程调用 wait() 的对象上调用 notify()。由于 notify() 不保证唤醒哪一个线程,因此称为“随机”。

  3. 通知 N 个等待线程中的某一个特定线程
    例如,若需确保按特定顺序(如调用顺序或优先级顺序)唤醒线程,则每个等待线程必须在自己独立的对象上调用 wait()。通知线程则在目标线程对应的对象上调用 notify()。相关示例可参见 饥饿与公平性(Starvation and Fairness)

以下代码片段展示了“通知一个随机等待线程”的策略(已加粗):

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(); // 通知策略
    }
}

测试并设置方法(Test and Set Method)

同步器通常包含两种类型的方法,其中第一种就是“测试并设置”方法(另一种是设置方法)。

“测试并设置”意味着:调用该方法的线程首先根据访问条件测试同步器的内部状态;如果条件满足,则设置内部状态,以反映该线程已获得访问权限。

这种状态转换通常会导致其他线程的访问条件变为 false,但并非总是如此。例如,在 读写锁(Read-Write Lock) 中,一个线程获得读访问权限后,会更新读写锁的状态,但只要没有线程请求写访问,其他请求读访问的线程仍可被授予访问权限。

关键点:测试与设置操作必须是原子的,即在测试和设置之间不能有其他线程插入执行。

“测试并设置”方法的典型流程如下:

  1. (如有必要)在测试前先设置状态;
  2. 将状态与访问条件进行比较;
  3. 若访问条件不满足,则等待;
  4. 若访问条件满足,则设置状态,并在必要时通知等待线程。

以下 ReadWriteLock 类中的 lockWrite() 方法就是一个“测试并设置”方法的示例。调用 lockWrite() 的线程首先在测试前设置状态(writeRequests++),然后在 canGrantWriteAccess() 方法中测试内部状态是否满足访问条件。如果测试成功,则在方法退出前再次设置状态。注意,此方法并未通知等待线程。

public class ReadWriteLock {
    private Map<Thread, Integer> readingThreads = new HashMap<Thread, Integer>();
    private int writeAccesses = 0;
    private int writeRequests = 0;
    private Thread writingThread = null;
    ...

    public synchronized void lockWrite() throws InterruptedException {
        writeRequests++;
        Thread callingThread = Thread.currentThread();
        while (!canGrantWriteAccess(callingThread)) {
            wait();
        }
        writeRequests--;
        writeAccesses++;
        writingThread = callingThread;
    }
    ...
}

下面的 BoundedSemaphore 类包含两个“测试并设置”方法:take()release()。这两个方法都会测试并设置内部状态。

public class BoundedSemaphore {
    private int signals = 0;
    private int bound = 0;

    public BoundedSemaphore(int upperBound) {
        this.bound = upperBound;
    }

    public synchronized void take() throws InterruptedException {
        while (this.signals == bound)
            wait();
        this.signals++;
        this.notify();
    }

    public synchronized void release() throws InterruptedException {
        while (this.signals == 0)
            wait();
        this.signals--;
        this.notify();
    }
}

设置方法(Set Method)

设置方法是同步器通常包含的第二种方法类型。与“测试并设置”不同,设置方法直接修改同步器的内部状态,而无需事先测试。典型的例子是 Lock 类中的 unlock() 方法:持有锁的线程总是可以直接解锁,而无需检查锁是否已被解锁。

设置方法的典型流程如下:

  1. 设置内部状态;
  2. 通知等待线程。

以下是一个 unlock() 方法的示例:

public class Lock {
    private boolean isLocked = false;

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