将 脚本之家 设为“星标⭐”
第一时间收到文章更新
原创:程序员牛肉(ID:gh_8adabf391378)
我们在看各种八股的时候,经常会看到“Volatile”这个关键字。只要提到这个关键字,就会强调它解决了Java多线程并发下的可见性问题。
图片文字来自小林coding
但你真的了解可见性问题的成因以及Volatile关键字是如何解决可见性问题的吗?
为什么会发生“可见性”的问题要从CPU开始说起了。
在计算机执行程序时,CPU负责处理每条指令。由于CPU的速度非常快,而访问物理内存中的数据速度相对较慢,这种速度差异会导致CPU在等待数据时出现闲置。为了解决这个问题,CPU内部集成了高速缓存。
具体来说,当程序运行时,它会预先将需要处理的数据从物理内存复制到CPU的高速缓存里。这样,CPU在进行计算时就可以直接从高速缓存中读取或写入数据,而不必每次都等待从物理内存中读取,从而提高了处理速度。计算完成后,高速缓存中的数据会被同步更新到物理内存中。
[由于CPU的读写速度远大于内存的读写速度,为了平衡二者的关系,我们在CPU和内存之间创建了缓存区。]
最早的单核CPU是这样处理指令的:
在单核CPU下就是有一百个线程,也不会触发线程安全问题,因为所有的线程操作的都是一个缓存变量。
可是坏就坏在由于技术的不断发展,人们对CPU的性能提出了更高的需求,希望其可以处理更加复杂的任务。
多核CPU应运而生,而在多核CPU下,线程可能不再操作同一个缓存区。
这个时候CPU与内存的数据一致性问题旧爆发了,当多个线程在不同的CPU上执行的时候,这些线程操作的是不同的CPU缓存。
就比如上图中,线程AB操作的是CPU核心1的缓存区,线程CB操作的是CPU核心2的缓存区。
此时线程AB对变量的操作就对于线程CD来讲是“不可见的”,而线程CD对变量的操作对于线程AB来讲也是不可见的。
现在你搞懂什么是“不可见性”以及为什么多线程下会有可见性问题了吧,我们来举一个例子:
这个案例不太正确,因为count++这个操作并不是原子性的。但是它可以帮助你更好的理解Volatile。
public class SimpleCounter {
public static void main(String[] args) {
final int[] count = {0}; // 使用数组来存储共享变量
Thread threadA = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
count[0]++;
}
});
Thread threadB = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
count[0]++;
}
});
threadA.start();
threadB.start();
System.out.println("Final count: " + count[0]);
}}
在这个示例代码中,我们使用了两个线程在不做任何安全措施的情况下进行自增操作。
两个线程分别执行10000次自增,但是结果却不是20000。
究其原因就是因为两个线程操作的并不都是当前变量的最新结果,可能是自己CPU的缓存值。而这两个线程所属的CPU核心又不相同,操作的也不是一个同一个缓存
[线程A都把count自增为900了,线程B还在自增自己的CPU缓存中的值为50的count]
这下明白为什么两个线程对变量做自增操作会导致结果并不是我们的预想结果了吧。
那我们的Volatile关键字是如何做到保证可见性呢?
这其实很好分析。为什么会出现我们上述代码的问题?
不就是因为线程操作的都是自己所属CPU核心中的缓冲存区吗?那我们直接禁用CPU缓存区不就好了嘛?
等等!我们可不能废寝忘食。
CPU缓存器本来就是我们为了协调CPU与内存的速度差异所创造的产物,现在我们怎么可以因为CPU缓存区会出现可见性问题就禁用CPU缓存区呢?
我们是不是应该按需禁用缓存呢?比如在操作某个变量的时候,禁用CPU缓存区,让我们对这个变量的修改是直接从内存中读取或者写入。
当你能想到这里的时候,恭喜你发明了“Volatile关键字”。
[当一个线程对volatile变量执行写操作时,它会将新值直接写入主内存,而不是线程的工作内存;当另一个线程对该变量执行读操作时,它会从主内存中读取最新的值,而不是它自己的工作内存。]
今天关于Volatile关键字的作用就讲解到这里了。希望通过我的介绍,你可以了解什么是“Volatile关键字的作用”。
对于Volatile关键字,你有什么想说的嘛?欢迎在评论区留言。
推荐阅读:
推荐阅读: