同步

这篇文章中展示的中心概念也适用于Java的旧版本,然而代码示例适用于Java 8,并严重依赖于lambda表达式和新的并发特性。如果你还不熟悉lambda,我推荐你先阅读 简要说明

出于简单的因素,这个教程的代码示例使用了定义在这里的两个辅助函数sleep(seconds)stop(executor)

线程和执行器 中,我们学到了如何通过执行器服务同时执行代码。当我们编写这种多线程代码时,我们需要特别注意共享可变变量的并发访问。假设我们打算增加某个可被多个线程同时访问的整数。

我们定义了count字段,带有increment()方法来使count加一:

int count = 0;
void increment() {
    count = count + 1;
}

当多个线程并发调用这个方法时,我们就会遇到大麻烦:

ExecutorService executor = Executors.newFixedThreadPool(2);
IntStream.range(0, 10000)
    .forEach(i -> executor.submit(this::increment));
stop(executor);
System.out.println(count);  // 9965

我们没有看到count为10000的结果,上面代码的实际结果在每次执行时都不同。原因是我们在不同的线程上共享可变变量,并且变量访问没有同步机制,这会产生竞争条件

增加一个数值需要三个步骤:(1)读取当前值,(2)使这个值加一,(3)将新的值写到变量。如果两个线程同时执行,就有可能出现两个线程同时执行步骤1,于是会读到相同的当前值。这会导致无效的写入,所以实际的结果会偏小。上面的例子中,对count的非同步并发访问丢失了35次增加操作,但是你在自己执行代码时会看到不同的结果。

幸运的是,Java自从很久之前就通过synchronized关键字支持线程同步。我们可以使用synchronized来修复上面在增加count时的竞争条件。

synchronized void incrementSync() {
    count = count + 1;
}

在我们并发调用incrementSync()时,我们得到了count为10000的预期结果。没有再出现任何竞争条件,并且结果在每次代码执行中都很稳定:

ExecutorService executor = Executors.newFixedThreadPool(2);
IntStream.range(0, 10000)
    .forEach(i -> executor.submit(this::incrementSync));
stop(executor);
System.out.println(count);  // 10000

synchronized关键字也可用于语句块:

void incrementSync() {
    synchronized (this) {
        count = count + 1;
    }
}

Java在内部使用所谓的“监视器”(monitor),也称为监视器(monitor lock)或内在锁( intrinsic lock)来管理同步。监视器绑定在对象上,例如,当使用同步方法时,每个方法都共享相应对象的相同监视器。

所有隐式的监视器都实现了重入(reentrant)特性。重入的意思是绑定在当前线程上。线程可以安全地多次获取相同的锁,而不会产生死锁(例如,同步方法调用相同对象的另一个同步方法)。