“你好,阿米戈!你还记得艾莉跟你讲过多个线程尝试同时访问共享资源时出现的问题,是吗?”

“是的。”

“关键是,这还不是全部。还有另外一个小问题。”

如你所知,计算机装有存储数据和命令(代码)的内存,以及执行这些命令和处理数据的处理器。处理器从内存中读取数据,进行更改,然后将其写回到内存中。为了加快计算速度,处理器有其自己的内置“快速”内存:缓存。

通过将最常用的变量和内存区域复制到缓存中,处理器可以更快地运行。接下来,它将在此快速内存中进行所有更改。然后将数据复制回“慢速”内存。在此期间,慢速内存包含旧的(未更改的!)变量。

这就是问题所在。一个线程更改变量,如上例中的 isCancel 或 isInterrupted,由于这是在快速内存中发生的,所以第二个线程“看不到这一更改”。这是因为线程无法访问彼此的缓存。(处理器通常包含几个独立的内核,线程可以在物理上不同的内核上运行。)

我们来回想一下昨天的示例:

代码 说明
class Clock implements Runnable
{
private boolean isCancel = false;

public void cancel()
{
this.isCancel = true;
}

public void run()
{
while (!this.isCancel)
{
Thread.sleep(1000);
System.out.println("Tick");
}
}
}
线程“不知道”存在其他线程。

在 run 方法中,首次使用 isCancel 变量时将其放入子线程的缓存中。此操作等效于以下代码:

public void run()
{
boolean isCancelCached = this.isCancel;
while (!isCancelCached)
{
Thread.sleep(1000);
System.out.println("Tick");
}
}

从另一个线程中调用 cancel 方法将更改常规(慢速)内存中的 isCancel 值,而不更改其他线程缓存中的值。

public static void main(String[] args)
{
Clock clock = new Clock();
Thread clockThread = new Thread(clock);
clockThread.start();

Thread.sleep(10000);
clock.cancel();
}

“哇!他们是否也为此提出了一个完美的解决方法,就像 synchronized 一样?”

“你不会相信的!”

首先想到的是禁用缓存,但这使程序的运行速度慢了好几倍。然后出现了另一种解决方法。

创建了关键字 volatile。我们将此关键字放在变量声明之前,以指示不得将其值放入缓存中。更准确地说,并不是不能将该关键字放入缓存中,而是必须始终在常规(慢速)内存中读取和写入它。

通过以下方式修复我们的解决方法,可以使一切正常运行:

代码 说明
class Clock implements Runnable
{
private volatile boolean isCancel = false;

public void cancel()
{
this.isCancel = true;
}

public void run()
{
while (!this.isCancel)
{
Thread.sleep(1000);
System.out.println("Tick");
}
}
}
volatile 修饰符使得始终在所有线程共享的常规内存中读取和写入变量。
public static void main(String[] args)
{
Clock clock = new Clock();
Thread clockThread = new Thread(clock);
clockThread.start();

Thread.sleep(10000);
clock.cancel();
}

“完了?”

“就是这样。简单而美妙。”