这篇文章中展示的中心概念也适用于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)特性。重入的意思是锁绑定在当前线程上。线程可以安全地多次获取相同的锁,而不会产生死锁(例如,同步方法调用相同对象的另一个同步方法)。