深入理解 Java 中的 volatile 关键字

暮色四合,晚风轻拂,湖面上泛起点点波光,宛如撒下了一片星河。

文章目录

  • 前言
  • 一、CPU 三级缓存
  • 二、JMM
  • 三、并发编程正确性的基础
  • 四、volatile 关键字
  • 五、volatile 可见性
  • 六、volatile 有序性
    • 6.1 指令重排序
    • 6.2 volatile 禁止指令重排
    • 6.3 volatile 有序性的应用
  • 七、FAQ
  • 推荐阅读

前言

在多线程编程中,确保共享变量的可见性是非常重要的。volatile 关键字就是为了解决这个问题而设计的。本文将深入介绍 volatile 关键字的作用、原理以及在实际开发中的应用场景。

一、CPU 三级缓存

计算机中的三级缓存通常是指处理器芯片(CPU)上的 L1、L2 和 L3 缓存层次结构。这三级缓存按照其靠近处理器核心和主存的距离分布,具有不同的特点和作用:

  1. L1 Cache(一级缓存)
    • 位置:位于处理器核心内部或非常接近处理器核心。
    • 作用:L1 缓存是最接近处理器核心的缓存层次,主要用于存储处理器当前正在执行的指令和数据。由于其靠近处理器核心,访问速度非常快,但容量通常较小。
  2. L2 Cache(二级缓存)
    • 位置:位于处理器核心和主存之间,通常在处理器芯片上但比 L1 缓存更大更远。
    • 作用:L2 缓存用于存储 L1 缓存未命中的数据。它比 L1 缓存容量大,访问速度较慢但仍比主存快。
  3. L3 Cache(三级缓存)
    • 位置:通常位于处理器芯片上,被多个处理器核心共享。
    • 作用:L3 缓存用于存储 L2 缓存未命中的数据或者多个核心之间共享的数据。它的容量比 L2 更大,速度比主存快,但比 L2 和 L1 慢。

这三级缓存层次结构设计的目的是在处理器核心和主存之间提供多层次的快速访问存储,以提高数据访问速度和整体系统性能。 L1 缓存作为最快速但容量最小的缓存,L2 缓存作为 L1 缓存未命中时的备用存储,而 L3 缓存则更大、更慢但能提供更高的整体性能,因为在一个计算机系统中的多个核心之间共享数据。

workspace.png

缓存虽然可以提升系统性能,却也带来了两个非常严重的问题:

  1. 缓存如何才能保证一致性
  2. 多线程环境中如何保证正确性

二、JMM

想要 CPU 缓存与主内存保证一致性,这想想就很复杂,尤其是在多线程环境下。为了简化 JAVA 开发人员的工作,JAVA 定义了一个概念 —— JMM。

JMM(Java Memory Model,Java 内存模型)是 Java 平台定义的一种规范,用于规定 Java 程序中多线程之间的内存访问和操作行为。它定义了多线程程序中的共享内存模型,以及在共享内存模型下,对变量读写、锁的获取和释放等操作的具体规则。

workspace (1).png

JMM 主要解决了以下几个问题:

  1. 内存可见性:保证一个线程对共享变量的修改对其他线程是可见的。
  2. 指令重排序:禁止编译器和处理器对指令进行重排序优化。
  3. 原子性:保证一个操作(如读写变量)是原子的,即在执行过程中不会被中断。
  4. happens-before 关系:规定了程序中操作的执行顺序,确保线程之间的一致性。

JMM 通过对线程之间的内存交互行为进行规范,使得程序员能够编写出正确的多线程程序。

JMM 定义了 8 种原子性操作,以确保在多线程环境中对共享内存的访问和操作保持正确性和一致性。以下是这 8 种操作的具体用途:

  1. lock(锁定):这个操作作用于主内存的变量,它把一个变量标记为一条线程独占状态。这意味着,被锁定后,这个变量就只能被持有锁的线程访问。
  2. unlock(解锁):这个操作也作用于主内存的变量,它把一个处于锁定状态的变量释放出来。解锁后的变量才可以被其他线程锁定。
  3. read(读取):这个操作作用于主内存的变量,它把一个变量的值从主内存传送到线程的工作内存中,以便随后的load动作使用。
  4. load(载入):这个操作作用于工作内存的变量,它把read操作的值放入工作内存中的变量副本中。
  5. use(使用):这个操作作用于工作内存的变量,它把工作内存中的值传递给执行引擎,每当虚拟机遇到一个需要使用这个变量的指令时候,将会执行这个动作。
  6. assign(赋值):这个操作作用于工作内存的变量,它把从执行引擎获取的值赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的指令时候,执行该操作。
  7. store(存储):这个操作作用于工作内存的变量,它把工作内存中的一个变量传送给主内存中,以备随后的write操作使用。
  8. write(写入):这个操作作用于主内存的变量,它把 store 传送值放到主内存中的变量中。

workspace (1).png

这些操作都是原子的,不能被中断。它们共同支持了线程间的同步和并发控制,使得 Java 程序在各种平台下都能达到一致的并发效果。

三、并发编程正确性的基础

在并发编程中,有几个关键的概念是确保多线程程序正确性的基础:可见性、有序性和一致性。

  1. 可见性(Visibility):可见性指的是一个线程对共享变量的修改,是否能及时被其他线程看到。在多线程环境中,每个线程都有自己的工作内存(缓存),一个线程对变量的修改可能不会立即被写回主内存,其他线程也可能从自己的工作内存中读取变量的旧值,从而导致数据不一致。例如:

    public class VisibilityExample {// 一个共享变量,控制线程是否停止private static boolean stop = false;public static void main(String[] args) throws InterruptedException {// 启动一个新线程,运行一个无限循环Thread thread = new Thread(() -> {while (!stop) { // 循环检查 stop 变量// busy-wait 忙等待}});thread.start();Thread.sleep(1000); // 确保新线程启动并运行一段时间stop = true; // 更新 stop 变量,尝试让线程停止}
    }

    在上述代码中,主线程更新 stop 变量,但如果没有适当的同步机制,工作线程可能永远看不到这个更新。

  2. 有序性(Ordering):有序性指的是程序执行过程中指令的顺序。在单线程环境中,程序的执行顺序通常按照代码的编写顺序进行。然而,在多线程环境中,由于编译器优化、处理器重排序等原因,指令的实际执行顺序可能与代码的编写顺序不同,这可能导致线程间不一致的行为。例如:

    public class OrderingExample {private int a = 0;private boolean flag = false;public void writer() {a = 1;          // 1. 赋值操作1flag = true;    // 2. 赋值操作2}public void reader() {if (flag) {     // 3. 检查 flagint i = a;  // 4. 使用变量 a}}
    }
    

    在这个示例中,编译器和处理器可能会将指令重排序,使得 a = 1flag = true 的执行顺序不同于代码书写顺序,这会影响多线程环境下的正确性。

  3. 原子性(Atomicity):原子性指的是一个操作是不可分割的,即使在多线程环境下也是如此。原子操作执行时,其他线程不能中断或观察到它的部分完成状态。例如:

    public class AtomicityExample {private int count = 0;public void increment() {count++; // 递增操作(非原子性)}
    }
    

    在上述代码中,count++ 不是原子操作,它实际上由三个步骤组成:读取 count 的值增加值写回 count。在多线程环境中,可能会出现竞态条件,导致最终结果不正确。

四、volatile 关键字

在 Java 中,volatile 是一个关键字,用于声明变量。当一个变量被声明为 volatile 时,它告诉编译器和运行时系统,这个变量是可见的(即对其他线程可见)并且不会被缓存。换句话说,使用 volatile 修饰的变量能够确保对它的读取写入操作都是原子的,并且能够立即反映在其他线程中。

例如:当线程 1 执行写入操作之后,会立即执行写回主内存的操作,并通知其他线程缓存失效。当线程 2 执行读取操作时,会从主内存读取最新值到工作内存。

在这里插入图片描述

五、volatile 可见性

情景分析:假设有一个 volatile 变量 counter,初始值为 0。现在有两个线程 T1T2 同时读取这个变量,然后各自对其进行递增操作(即 counter++),最后将结果写回共享内存。

  1. 初始化
    • c o u n t e r = 0 counter = 0 counter=0
  2. 读取
    • 线程 T 1 T1 T1 T 2 T2 T2 同时读取 c o u n t e r counter counter 的值,都读到 0 0 0
  3. 修改
    • 线程 T 1 T1 T1 c o u n t e r counter counter的值加 1 1 1,得到 1 1 1
    • 线程 T 2 T2 T2 也将 c o u n t e r counter counter 的值加 1 1 1,得到 1 1 1
  4. 写回
    • 线程 T 1 T1 T1 1 1 1 写回 c o u n t e r counter counter
    • 线程 T 2 T2 T2 也将 1 1 1 写回 c o u n t e r counter counter
public class Counter {// 计数器变量声明为 volatile,以确保在多线程环境中的可见性private static volatile int counter;public static void main(String[] args) throws InterruptedException {// 循环运行测试方法 100 次for (int i = 0; i < 100; i++) {test(); // 调用测试方法counter = 0; // 重置计数器为0,以便下一次测试}}// 测试方法,用于演示多线程环境下的计数器操作static void test() throws InterruptedException {// 创建一个新线程 t1,该线程会在延迟后对计数器进行递增操作Thread t1 = new Thread(() -> {try {TimeUnit.MILLISECONDS.sleep(500); // 线程休眠 500 毫秒} catch (InterruptedException e) {throw new RuntimeException(e);}counter++; // 对计数器进行递增操作});// 创建另一个新线程 t2,该线程也会在延迟后对计数器进行递增操作Thread t2 = new Thread(() -> {try {TimeUnit.MILLISECONDS.sleep(500); // 线程休眠 500 毫秒} catch (InterruptedException e) {throw new RuntimeException(e);}counter++; // 对计数器进行递增操作});// 启动两个线程t1.start();t2.start();// 等待两个线程执行完成t1.join();t2.join();// 打印计数器的值System.out.println(counter);}
}

最终,测试结果如下图所示:

image.png

经多次测试,我们发现出现了并发问题。

为什么会出现问题?

这个问题的答案,常规回答是:

c o u n t e r + + counter++ counter++ 操作实际上分解为以下三个步骤:

  1. 读取 c o u n t e r counter counter 的当前值。
  2. 将读取到的值加 1 1 1
  3. 将计算后的新值写回 c o u n t e r counter counter

在多线程环境下,这些步骤不是原子的,多个线程可能会交替执行这些步骤,导致竞态条件。例如上面的例子中,两个线程都同时读取了 c o u n t e r counter counter 0 0 0,然后分别加 1 1 1 并写回,导致最终值错误。

这个回答没什么不对。但仔细思考一下,我们提出几个问题:

  1. 我们不是加了 volatile 关键字修饰吗?
  2. 难道 volatile 关键字不能解决上面的问题?
  3. 既然不能解决那为什么使用 volatile 关键字又有什么用?

回答问题之前,我们先回顾一下可见性的定义:
可见性指的是一个线程对共享变量的修改,是否能及时被其他线程看到。

volatile 关键字修饰的变量是满足可见性的,即一个线程对变量进行了修改,其他线程会及时看到。

即:无论是 T 1 T1 T1 线程还是 T 2 T2 T2 线程谁先修改了变量,相互之间应该及时收到对方修改之后变量的值。

例如:线程 T 1 T1 T1 c o u n t e r counter counter 的值先加 1 1 1,得到 1 1 1 时,线程 T 2 T2 T2 应该及时获取到最新值 1 1 1,然后在新值上执行 + + ++ ++ 操作。反之,亦成立。

可是,事实并非如此。

我们将这段逻辑的处理流程放大,看看究竟发生了什么?

在这里插入图片描述

很明显,问题出现在递增阶段。递增阶段,当 T1 线程写回前,T2 线程已经读取了变量。这不是和可见性相违背了吗?

该如何理解可见性?

可见性指的是一个线程对共享变量的修改,是否能及时被其他线程看到。

我们注意到,可见性是在修改变量之后立刻写回主存,并及时让其他线程看到,并非立刻让其他线程看到。

volatile 拥有可见性,但是不能保证原子性。所以,出现了上述的并发问题。

那么,想要解决这一问题,就需要使用同步机制保证原子性。

public class Counter {// 将计数器变量声明为 volatile,以确保在线程间的可见性private static volatile int counter;public static void main(String[] args) throws InterruptedException {// 运行测试方法 100 次for (int i = 0; i < 100; i++) {test(); // 调用测试方法counter = 0; // 为下一次测试重置计数器}}// 用于测试线程同步的方法static void test() throws InterruptedException {// 创建一个新线程 t1,在延迟后增加计数器值Thread t1 = new Thread(() -> {try {TimeUnit.MILLISECONDS.sleep(500); // 等待 500 毫秒} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (Counter.class) { // 在 Counter 类对象上同步counter++; // 在同步块中增加计数器值}});// 创建另一个线程 t2,也在延迟后增加计数器值Thread t2 = new Thread(() -> {try {TimeUnit.MILLISECONDS.sleep(500); // 等待 500 毫秒} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (Counter.class) { // 在 Counter 类对象上同步counter++; // 在同步块中增加计数器值}});// 启动两个线程t1.start();t2.start();// 等待两个线程执行完成t1.join();t2.join();// 在两个线程都完成后打印计数器的值System.out.println(counter);}
}

新的问题诞生了,可见性似乎很鸡肋。因为似乎可以不添加 volatile 关键字修饰,直接使用 synchronized 加锁同步。

public class Counter {// 普通变量,未使用 volatile 修饰private static int counter;public static void main(String[] args) throws InterruptedException {// 运行测试方法 100 次for (int i = 0; i < 100; i++) {test(); // 调用测试方法counter = 0; // 为下一次测试重置计数器}}// 用于测试线程同步的方法static void test() throws InterruptedException {// 创建一个新线程 t1,在延迟后增加计数器值Thread t1 = new Thread(() -> {try {TimeUnit.MILLISECONDS.sleep(500); // 等待 500 毫秒} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (Counter.class) { // 在 Counter 类对象上同步counter++; // 在同步块中增加计数器值}});// 创建另一个线程 t2,也在延迟后增加计数器值Thread t2 = new Thread(() -> {try {TimeUnit.MILLISECONDS.sleep(500); // 等待 500 毫秒} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (Counter.class) { // 在 Counter 类对象上同步counter++; // 在同步块中增加计数器值}});// 启动两个线程t1.start();t2.start();// 等待两个线程执行完成t1.join();t2.join();// 在两个线程都完成后打印计数器的值System.out.println(counter);}
}

上面的代码测试运行,我们会发现是正确的。

image.png

这是因为 sychronized 也是可以保证可见性的。这进一步证明了 volatile 似乎没有用。

然而,事实并非如此。我们需要有一个基本认知是:在并发编程中(即:多线程环境),有一些场景只需要保证可见性,而不需要保证原子性或有序性。

例如,以下场景只需保证可见性:

  1. 标志位:使用 volatile 变量作为标志位来控制线程的行为。

    public class FlagExample {private volatile boolean stop = false;public void runExample() {Thread task = new Thread(() -> {while (!stop) {// do work}});task.start();// 在其他线程中停止任务stop = true;}
    }
    
  2. 单次赋值的变量:一个变量只被赋值一次,然后被多个线程读取,但不会被其他线程修改。

    public class Configuration {// 使用 volatile 关键字修饰的变量,保证了其在多线程环境下的可见性private volatile Map<String, String> configMap;public Configuration() {// 在构造函数中,我们只对 configMap 变量赋值一次// 假设 loadConfig() 方法从某个配置文件中读取配置,并返回一个 Mapthis.configMap = loadConfig();}// 这个方法用于获取配置信息// 由于 configMap 是 volatile 的,所以每个线程都能看到它的最新值public String getConfig(String key) {return configMap.get(key);}// 这个方法用于加载配置信息// 在这个示例中,我们假设它返回一个空的 HashMap// 在实际应用中,你可能需要从文件、数据库或其他地方加载配置private Map<String, String> loadConfig() {// 加载配置的具体实现return new HashMap<>();}
    }
    

六、volatile 有序性

6.1 指令重排序

指令重排序是编译器和处理器为了优化程序性能而进行的一种优化技术,它可能会改变指令的执行顺序,但并不影响程序最终的执行结果。然而,在多线程环境下,指令重排序可能会导致线程间的竞态条件和不确定的结果。

public class ReorderExample {private static int x = 0, y = 0; // 共享变量x和yprivate static int a = 0, b = 0; // 共享变量a和bpublic static void main(String[] args) throws InterruptedException {// 线程one执行a=1,然后x=bThread one = new Thread(() -> {try {Thread.sleep(100); // 为了增加指令重排序的可能性,让线程睡眠100毫秒} catch (InterruptedException e) {e.printStackTrace();}a = 1;x = b;});// 线程other执行b=1,然后y=aThread other = new Thread(() -> {try {Thread.sleep(100); // 为了增加指令重排序的可能性,让线程睡眠100毫秒} catch (InterruptedException e) {e.printStackTrace();}b = 1;y = a;});one.start(); // 启动线程oneother.start(); // 启动线程otherone.join(); // 等待线程one执行完成other.join(); // 等待线程other执行完成// 输出(x, y)的值System.out.println("(x, y) = (" + x + ", " + y + ")");}
}

在上面的代码中,多执行几次可能会出现多种不同的结果,例如: ( x , y ) = ( 1 , 1 ) (x, y) = (1, 1) (x,y)=(1,1) ( x , y ) = ( 1 , 0 ) (x, y) = (1, 0) (x,y)=(1,0)

6.2 volatile 禁止指令重排

指令重排序是一种优化技术,但是在多线程环境中是会有问题的。所以,我们需要禁止指令重排。想要禁止指令重排,我们可以通过使用 volatile 关键字达到目的。

当一个变量被声明为 volatile 后,对这个变量的写操作就会有一个内存屏障(这是一种特殊的指令),这个屏障可以防止指令重排。具体来说,编译器和处理器在执行程序时,必须在读取 volatile 变量之前的操作都执行完毕,且在读取操作后,所有写入 volatile 变量的操作都未执行。

以下面代码为例:

volatile boolean ready = false;
int data = 0;void write() {data = 1;          // 操作1ready = true;      // 操作2
}void read() {if (ready) {       // 操作3int result = data; // 操作4}
}

在这个例子中,ready 是一个 volatile 变量。由于 volatile 变量的写操作(操作2)有一个内存屏障,所以操作1(data = 1;)必须在操作2(ready = true;)之前执行。这就保证了 write() 方法中的操作1 和操作2 的有序性。

同样,由于 volatile 变量的读操作(操作3)有一个内存屏障,所以操作4(int result = data;)必须在操作3(if (ready) { … })之后执行。这就保证了 read() 方法中的操作3和操作4的有序性。

6.3 volatile 有序性的应用

volatile 有序性最经典的一个运用便是在单例模式中。

public class Singleton {// 使用 volatile 关键字修饰,保证其在多线程环境下的可见性private static volatile Singleton instance;private Singleton() {// 私有构造函数,防止外部直接创建实例}public static Singleton getInstance() {// 第一次检查:如果实例不存在,则进入同步代码块if (instance == null) {synchronized (Singleton.class) {// 第二次检查:如果实例仍然不存在,则创建新的实例if (instance == null) {instance = new Singleton();}}}return instance;}
}

上述代码是单例模式的一种写法, getInstance() 方法首先检查 instance 是否已经被初始化。如果 instance 已经被初始化,那么就直接返回 instance,这样就避免了每次调用 getInstance() 时都需要进入同步代码块,从而减少了同步的开销。

如果 instance 还没有被初始化,那么就进入同步代码块。在同步代码块中,我们再次检查 instance 是否已经被初始化。如果 instance 仍然没有被初始化,那么就创建一个新的 Singleton 实例。

这种方式称为双重检查锁定(Double-Checked Locking,简称 DCL),因为我们进行了两次 instance == null 的检查:一次是在同步代码块外,一次是在同步代码块内。

为什么要在同步代码块内再检查一次呢?这是因为可能会有多个线程同时进入同步代码块外的 if (instance == null)。假设线程 A 和线程 B 同时进入了这个 if,线程 A 首先进入同步代码块,创建了一个新的 Singleton 实例,然后线程 B 进入同步代码块。如果没有第二次检查,线程 B 会创建另一个 Singleton 实例,这就违反了单例模式。

这里,volatile 关键字的作用就是保证 instance 字段的读写操作不会被 CPU 指令重排,从而保证了程序的有序性。具体来说,当一个线程创建新的 Singleton 实例时(即 instance = new Singleton()),这个操作实际上包含了以下三个步骤:

  1. 为 Singleton 对象分配内存空间。
  2. 初始化 Singleton 对象。
  3. 将 instance 变量指向分配的内存地址。

在 Java 中,这三个步骤可能会因为编译器优化而被重排序。例如,步骤2可能会在步骤1之后执行,也可能在步骤1之前执行。如果步骤2在步骤3之后执行,那么在多线程环境下,可能会出现一个线程获取到一个未完全初始化的 Singleton 对象。

使用 volatile 关键字可以禁止这种重排序。当 instance 变量被声明为 volatile 后,对它的写操作就会有一个内存屏障(这是一种特殊的指令),这个屏障可以防止重排序。这就是为什么我们需要在双重检查锁定模式中使用 volatile 关键字。

七、FAQ

情景分析:假设有一个 volatile 变量 counter,初始值为 0。现在有两个线程 T1T2 同时读取这个变量,然后各自对其进行递增操作,不过现在我们要求 T1 线程 +1T2 线程 +2,最后将结果写回共享内存。

public class Counter {private static volatile int counter; // 使用volatile修饰共享变量public static void main(String[] args) throws InterruptedException {for (int i = 0; i < 1000; i++) {test(); // 调用test方法counter = 0; // 重置counter的值}}// 测试方法static void test() throws InterruptedException {// 创建线程t1,对counter加1Thread t1 = new Thread(() -> {try {TimeUnit.MILLISECONDS.sleep(500); // 线程休眠500毫秒} catch (InterruptedException e) {throw new RuntimeException(e);}counter++; // 对共享变量counter加1});// 创建线程t2,对counter加2Thread t2 = new Thread(() -> {try {TimeUnit.MILLISECONDS.sleep(500); // 线程休眠500毫秒} catch (InterruptedException e) {throw new RuntimeException(e);}counter += 2; // 对共享变量counter加2});t1.start(); // 启动线程t1t2.start(); // 启动线程t2t1.join(); // 等待线程t1执行完毕t2.join(); // 等待线程t2执行完毕System.out.println(counter); // 输出counter的值}
}

正常情况下,如果不发生并发冲突,可以获取到正确值 3 3 3

在这里插入图片描述

我们知道上面的代码并不能保证线程安全,所以是有问题的。之前已经讨论过了,但是现在有一个问题:

这个错误的值是怎么得到的?

我们调整一下测试代码:

public class Counter {private static volatile int counter; // 使用volatile修饰共享变量public static void main(String[] args) throws InterruptedException {for (int i = 0; i < 1000; i++) {test(); // 调用test方法counter = 0; // 重置counter的值}}// 测试方法static void test() throws InterruptedException {// 创建线程t1,对counter加1Thread t1 = new Thread(() -> {try {TimeUnit.MILLISECONDS.sleep(500); // 线程休眠500毫秒} catch (InterruptedException e) {throw new RuntimeException(e);}counter++; // 对共享变量counter加1synchronized (Counter.class) {System.out.println("T1: " + counter); // 输出t1线程操作后的counter值}});// 创建线程t2,对counter加2Thread t2 = new Thread(() -> {try {TimeUnit.MILLISECONDS.sleep(500); // 线程休眠500毫秒} catch (InterruptedException e) {throw new RuntimeException(e);}counter += 2; // 对共享变量counter加2synchronized (Counter.class) {System.out.println("T2: " + counter); // 输出t2线程操作后的counter值}});t1.start(); // 启动线程t1t2.start(); // 启动线程t2t1.join(); // 等待线程t1执行完毕t2.join(); // 等待线程t2执行完毕System.out.println(counter); // 输出counter的值System.out.println(); // 输出空行,用于分隔不同次测试结果}
}

测试效果如下:

image.png

我们发现,测试结果少了一种情况:

  1. 初始化
    • c o u n t e r = 0 counter = 0 counter=0
  2. 读取
    • 线程 T 1 T1 T1 T 2 T2 T2 同时读取 c o u n t e r counter counter 的值,都读到 0 0 0
  3. 修改
    • 线程 T 1 T1 T1 c o u n t e r counter counter的值加 1 1 1,得到 1 1 1
    • 线程 T 2 T2 T2 也将 c o u n t e r counter counter 的值加 2 2 2,得到 2 2 2
  4. 写回
    • 线程 T 1 T1 T1 1 1 1 写回 c o u n t e r counter counter
    • 线程 T 2 T2 T2 也将 2 2 2 写回 c o u n t e r counter counter

即,结果是:

  1. T 1 : 1 T1: 1 T1:1
  2. T 2 : 2 T2: 2 T2:2
  3. 最终结果是: 2 2 2

  1. T 2 : 2 T2: 2 T2:2
  2. T 1 : 1 T1: 1 T1:1
  3. 最终结果是: 1 1 1

出现这个问题的原因是:

  1. 无论是线程 T 1 T1 T1 还是线程 T 2 T2 T2 写回之后,主内存就立刻通知其他线程缓存失效了
  2. 当其他线程发现缓存失效,便会重新从主内存中读取变量最新的值
  3. 紧接着执行未完成的步骤,从而导致了问题

在这里插入图片描述

推荐阅读

  1. Spring 三级缓存
  2. 深入了解 MyBatis 插件:定制化你的持久层框架
  3. 深入探究 Spring Boot Starter:从概念到实践
  4. Zookeeper 注册中心:单机部署
  5. 【JavaScript】探索 JavaScript 中的解构赋值

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/28631.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

如何区分人工智能生成的图像与真实照片(下)

4 功能上的不合理性 AI 生成的图像往往会因为缺乏对现实世界物体结构和相互作用的了解&#xff0c;而产生各种功能不合理之处。这些不合理之处主要表现在以下几个方面&#xff1a; 4.1 构图不合理 物体关系不合逻辑: AI 生成的图像中&#xff0c;物体和人物之间的关系可能不符…

python3GUI--记账助手By:PyQt5(附下载地址)

文章目录 一&#xff0e;前言二&#xff0e;开发环境三&#xff0e;预览1.登录&注册2.主界面3.新增账单1.当前日期2.选择日期3.添加成功 4.删除账单4.筛选账单5.账单数据汇总1.日账单2.月账单3.年账单 四&#xff0e;设计心得1.项目代码结构2.UI设计概览3.UI设计详细1.登录…

人员的社保缴纳情况直接影响设计资质的延续结果。

是的&#xff0c;人员的社保缴纳情况会直接影响设计资质的延续结果。社保缴纳情况是评估企业运营稳定性和合规性的重要指标之一&#xff0c;特别是在设计资质延续的审核过程中。 设计资质延续时&#xff0c;相关部门会要求企业提供涉及资质延续所需人员的社保缴纳证明&#xff…

kettle学习(利用jsonPath定位,json文件转换)

kettle学习&#xff08;利用jsonPath定位&#xff0c;json文件转换&#xff09; 于数据处理的广袤天地间&#xff0c;我们时常需应对各类繁杂状况与各式格式。Kettle 作为极具威力的数据集成利器&#xff0c;赋予了我们诸多功能与无限可能此次博客里&#xff0c;我们将重点投向…

如何在电磁仿真软件CST中获得多天线同频的SAR

上期介绍了多天线不同频率情况下如何计算SAR&#xff0c;不用考虑相位差&#xff1b;这期我们看看MIMO&#xff0c;多天线同频&#xff0c;考虑相位差&#xff1a; 简单模型&#xff0c;一只手和两个天线&#xff1a; 工作频率2GHz&#xff1a; 仿真结束查看S11&#xff1a; 查…

KEPServerEX远程配置功能一键Get

远程配置功能&#xff0c;其实是通过KEPServerEX REST 接口及 HTTP 协议命令&#xff0c;对 KEPServerEX Runtime 运行环境进行配置的查询及更改。 右键点击系统托盘处的KEPServerEX Administration 图标&#xff0c;选择Settings | Configuration API Service; 如图设置&…

Windows WPS ppt幻灯片 保存文件时候 PPT文件大小变大 ppt文件太大 解决方案

Windows WPS ppt 幻灯片 保存文件时候 PPT文件大小突然变大&#xff0c;原先只有10M的大小增加到40M 关于wps的ppt文件变大&#xff0c;指的是&#xff1a;明明ppt幻灯片页数和图片都不多&#xff0c;只是稍微修改两个文字&#xff0c;保存时&#xff0c;文件大小增加了不少&a…

android studio CreateProcess error=2, 系统找不到指定的文件

【问题记录篇】 在AndroidStudio编译开发jni相关工程代码的时候&#xff0c;编译遇到的这个报错&#xff1a; CreateProcess error2, 系统找不到指定的文件。排查处理步骤: 先查看Build Output的具体日志输出 2.了解到问题出在了NDK配置上&#xff0c;此时需要根据自己的gra…

模型算法—线性回归

线性回归是统计学中最常见的一种回归分析方法&#xff0c;用于建立自变量&#xff08;解释变量&#xff09;和因变量&#xff08;响应变量&#xff09;之间的线性关系。线性回归模型可以用来预测一个或多个自变量对应的因变量的值。 线性回归的基本形式如下&#xff1a; &…

【ElasticSearch】ElasticSearch基本概念

ES 是一个开源的高扩展的分布式全文检索引擎&#xff0c;它是对开源库 Luence 的封装&#xff0c;提供 REST API 接口 MySQL 更适合数据的存储和关系管理&#xff0c;即 CRUD&#xff1b;而 ES 更适合做海量数据的检索和分析&#xff0c;它可以秒级地从数据库中检索出我们感兴…

SSH概念、用途、详细使用方法

还是大剑师兰特&#xff1a;曾是美国某知名大学计算机专业研究生&#xff0c;现为航空航海领域高级前端工程师&#xff1b;CSDN知名博主&#xff0c;GIS领域优质创作者&#xff0c;深耕openlayers、leaflet、mapbox、cesium&#xff0c;canvas&#xff0c;webgl&#xff0c;ech…

Excel中多条件判断公式怎么写?

在Excel里&#xff0c;这种情况下的公式怎么写呢&#xff1f; 本题有两个判断条件&#xff0c;按照题设&#xff0c;用IF函数就可以了&#xff0c;这样查看公式时逻辑比较直观&#xff1a; IF(A2>80%, 4, IF(A2>30%, 8*(A2-30%),0)) 用IF函数写公式&#xff0c;特别是当…

【Java】已解决java.lang.ArrayIndexOutOfBoundsException异常

文章目录 一、问题背景二、可能出错的原因三、错误代码示例四、正确代码示例五、注意事项 已解决java.lang.ArrayIndexOutOfBoundsException异常 一、问题背景 java.lang.ArrayIndexOutOfBoundsException 是 Java 中一个非常常见的运行时异常&#xff0c;它表明程序试图访问数…

C语言入门系列:流程控制

一&#xff0c;C代码执行顺序 默认情况下&#xff0c;C语言程序从main()函数开始执行&#xff0c;随后按源代码中语句出现的顺序逐一执行。 这意味着&#xff0c;如果不考虑任何控制结构&#xff0c;程序会自上而下&#xff0c;逐行执行每条语句&#xff0c;直到遇到函数调用…

LabVIEW电源适应能力检测系统

随着工业自动化程度的提高&#xff0c;电源质量直接影响设备的稳定运行。利用LabVIEW开发一个单相电源适应能力检测系统&#xff0c;该系统通过智能化和自动化测试&#xff0c;提高了测试效率&#xff0c;减少了人为错误&#xff0c;保证了电源质量的可靠性。 项目背景 在现代…

Nuxt3 实战 (九):使用 Supabase 实现 Github 认证鉴权

前言 Supabase 使用的是 postgresql 的 Row Level Security (RLS)&#xff0c;可以限制不同用户对同一张表的不同数据行的访问权限。这种安全机制可以确保只有授权用户才能访问其所需要的数据行&#xff0c;保护敏感数据免受未授权的访问和操作。 Auth Providers 打开 Supab…

29.添加录入注入信息界面

上一个内容&#xff1a;28.启动与暂停程序 以 28.启动与暂停程序 它的代码为基础进行修改 效果图&#xff1a; 新建Dialog 给新建的dialog添加空间&#xff0c;如下图 给每个输入框创建一个变量 代码&#xff1a; void CWndAddGame::OnBnClickedButton1() {static TCHAR BASE…

035.搜索插入位置

题意 给定一个排序数组和一个目标值&#xff0c;在数组中找到目标值&#xff0c;并返回其索引。如果目标值不存在于数组中&#xff0c;返回它将会被按顺序插入的位置。 请必须使用时间复杂度为 O(log n) 的算法。 难度 简单 示例 输入: nums [1,3,5,6], target 5 输出: …

Open vSwitch 中的 vswitchd 事件上报

一、数据包转发流程与 vswitchd 事件上报 Open vSwitch 的数据包转发流程如下图所示&#xff1a; 在数据包的转发流程中&#xff0c;提到过慢速路径的概念&#xff1a;即当数据包在内核空间无法完全处理时&#xff0c;会产生 upcall 调用&#xff0c;将数据包从内核空间转发到用…

大众点评_token,mtgsig

声明 本文章中所有内容仅供学习交流使用&#xff0c;不用于其他任何目的&#xff0c;抓包内容、敏感网址、数据接口等均已做脱敏处理&#xff0c;严禁用于商业用途和非法用途&#xff0c;否则由此产生的一切后果均与作者无关&#xff01; 本文章未经许可禁止转载&#xff0…