CodeGym /Java 课程 /Java 核心 /本地缓存:多线程问题。volatile

本地缓存:多线程问题。volatile

Java 核心
第 7 级 , 课程 5
可用

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

“是的。”

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

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

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

这就是问题所在。一个线程更改变量,如上例中的 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();
}

“完了?”

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

评论 (8)
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION
z18335776829 级别 19,China,China
12 六月 2023
多线程共享变量有一个特点: 访问与修改都只在主存中进行 而非多线程共享变量: 为了高效访问, 第一次访问时会加载到线程工作内存中, 之后的访问会直接使用线程工作内存中的缓存; 修改时会在线程工作内存中完成修改(此时内存中的原变量并没有被修改)在切换线程前会将线程工作内存所缓存的数据 替换/覆盖 主存中的原数据以保证数据即能高效访问又能不影响其他线程的继续执行. 在本节课的代码中: run()方法访问isCancel时会将 该变量加载到线程工作内存中, isCancel变量被主线程以及Clock线程所访问所以属于多线程共享变量, 被volatile关键字声明后isCancel变量只能在主存中访问/修改,同时又能避免run()方法第一次访问时将该变量加载到线程工作内存中 所以 volatile 关键字在本节课中只起到了不让isCancel变量在run()方法第一次执行时被缓存到线程工作内存中吗? 以下是ChatGPT给出的回答: 其实,Java虚拟机中线程之间的内存可见性是有一定的延迟的,可能一个线程修改了共享变量,但是另一 个线程并不会立刻看到这个修改。这是因为在多级缓存的硬件系统中,有可能会存在一些缓存一致性的问 题,例如缓存未及时更新、写缓冲区等等。因此,需要使用volatile关键字来保证共享变量的内存可见性, 从而避免不一致的情况。 在你提到的例子中,使用volatile关键字可以使得isCancel变量在任何时刻的值都是即时更新的,并且对其 他线程可见。在run()方法中如果不使用volatile关键字,可能会出现一个线程修改了isCancel变量后,另一 个线程仍然使用旧值(即缓存中的值)。有可能导致在一台计算机中运行正常另外一台计算机就无法正常 工作为了避免这种情况,使用volatile关键字可以确保共享变量的可见, 性和即时更新,从而避免了出现不 同步的问题。这也是volatile关键字的主要作用。 上面看不懂可以看下面:这是第二个gpt给出的答案 在Java中,如果多线程共享一个变量,即使没有使用volatile关键字,该变量也具有查找/修改都只在主存内执行的特性。这是因为Java的内存模型规定,所有的共享变量都存储在主存中,而每个线程都有自己的工作内存,线程操作变量时都是从主存中复制变量到自己的工作内存中进行读写操作,当线程操作完成时,再将该变量的值刷新回主存中。因此,即使没有使用volatile关键字,该变量也会在主存中被修改和查找,而不是在线程的工作内存中。但是,没有使用volatile关键字的情况下,线程在操作变量时不会立即通知其他线程有变量值的改变,可能需要等待一段时间才能获取到最新值。这种延迟更新的情况可能会导致线程安全问题,例如数据竞争等。因此,对于多线程共享的变量,最好使用volatile关键字来保证其可见性和避免数据竞争问题。
阿狼 级别 32,Zhengzhou,China
19 六月 2022
没懂这节的意思,变量如果不能进高速缓存,那是不是意味着CPU和内存之间时刻存在调度,要查这个被存在内存中的变量的值?
有梦想的咸鱼 级别 30,Hangzhou,China
31 五月 2023
是的,而从磁盘上读取内存速度很慢的,所以一般需要借用缓存来加快cpu访问数据速度
Close To The Sun 级别 22,Nanchang,China
17 四月 2022
线程与缓存解释的不是很能理解
太古天霸 级别 22,Huizhou,China
7 十二月 2021
不能从例子中看到区别😬
hello world 级别 22,shanghai,China
15 十一月 2021
volatile使得修饰符始终在所有线程共享的常规线程中读取和写入变量
4 十月 2021
线程公共变量声明关键字 volatile
Jimbo 级别 20,Shenzhen
27 六月 2020
Nice