并发编程-volatile

并发编程-volatile

在上一篇文章中我们研究了影响线程安全原子性问题的本质以及synchronized关键字的原理,本篇我们将研究另外两个问题:可见性问题、有序性问题。以及volatile关键字的原理

什么是可见性问题

对于可见性问题我们可以这样进行理解:一个线程修改了共享变量,另一个线程不能立刻看到。这跟我们数据库中事务问题有些相似,我们通过一段代码来感受一下可见性问题

public class VolatileDemo {
    public static boolean stop=false;
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(()->{
            int i=0;
            while(!stop){
                i++;
            }
        });
        t1.start();
        System.out.println("t1线程执行-----");
        Thread.sleep(1000);
        stop=true;
    }
}

这段代码的含义是t1线程通过stop来判断执行i++的操作,我们在main方法中将stop修改成true 理论上t1中的while循环会停止,整个代码执行结束。实际真的是这样吗,我们运行一下这段代码后发现
在这里插入图片描述
程序会一直运行,也就是说main线程修改了stop=true;而t1线程仍然是获取到 stop=false。所以while循环会一直执行,这就是一个线程修改了共享变量,另一个线程并没有看到,可见性问题

如何解决可见性问题

出了问题就得解决,如何解决可见性问题,我们可以通过volatile关键字来解决

public class VolatileDemo {
    public volatile static boolean stop=false;
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(()->{
            int i=0;
            while(!stop){
                i++;
            }
        });
        t1.start();
        System.out.println("t1线程执行-----");
        Thread.sleep(1000);
        stop=true;
    }
}

这时候有的小伙伴可能有疑问,这个问题是在于线程修改了一个变量另一个线程无法立刻看到,那如果让t1线程等一会呢,会有什么影响?我们把代码稍作调整

public class VolatileDemo {
    public static boolean stop=false;
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(()->{
            int i=0;
            while(!stop){
                i++;
                try {
                    //让t1线程sleep一秒
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t1.start();
        System.out.println("t1线程执行-----");
        Thread.sleep(1000);
        stop=true;
    }
}

运行后我们发现
在这里插入图片描述
程序能够执行结束,也就说让t1线程sleep之后可见性问题也得到了解决。核心是因为程序在执行过程中CPU发生了时间片的切换,使main线程修改的变量对t1线程可见。

还有一种情况就是我们常用的System.out.println()

public class VolatileDemo {
    public static boolean stop=false;
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(()->{
            int i=0;
            while(!stop){
                i++;
                //打印一下i的值
                System.out.println("打印一下 i:"+i);
            }
        });
        t1.start();
        System.out.println("t1线程执行-----");
        Thread.sleep(1000);
        stop=true;
    }
}

运行结果
在这里插入图片描述
程序也能够执行结束,我们进入源码可以得到答案

public void println(String x) {
    synchronized (this) {
        print(x);
        newLine();
    }
}

原因是加了同步锁的。synchronized也能解决可见性问题

探究可见性问题的本质

回归到我们本篇文章的中心——volatile关键字。我们看不到volatile的任何java代码,只是个单纯的关键字。那volatile关键字是如何解决可见性问题的呢?这还得从CPU层面的说起

CPU高速缓存

我们平常开发项目时都会用到Redis,每次通过数据库查询数据不方便且速度慢就会先放到Redis里面缓存起来,需要的时候直接访问Redis。同理,CPU为了提升效率也引入了缓存机制,因为CPU在做计算时,与内存的IO操作是无法避免的,而这个IO过程相对于CPU的计算速度来说是非常耗时,基于这样一个问题,所以在CPU层面设计了高速缓存,这个缓存行可以缓存存储在内存中的数据, CPU每次会先从缓存行中读取需要运算的数据,如果缓存行中不存在该数据,才会从内存中加载,通过这样一个机制可以减少CPU和内存的交互开销从而提升CPU的利用率。目前CPU有三种级别缓存 L1/L2/L3 从任务管理器中我们能初步看到
在这里插入图片描述
它们的结构大致是这样的
在这里插入图片描述
离着CPU越近,速度越快,容量越小。从图上我们可以看出这三个缓存级别中L3缓存是共享的,L1缓存和L2缓存是每个CPU核心独占的,很明显这样就会存在一个问题——缓存一致性问题。

缓存一致性问题

在多线程环境中,当多个线程并行执行加载同一块内存数据时,由于每个CPU都有自己独立的L1、L2缓存,所以每个CPU的这部分缓存空间都会缓存到相同的数据,并且每个CPU执行相关指令时,彼此之间 不可见,就会导致缓存的一致性问题
在这里插入图片描述
当cpu 0 和cpu 1 同时从内存中加载到i = 0;因为L1和L2的缓存时cpu核心独占的。此时cpu 1将i = 0改为了i = 1且内存也已经改成了i=1,而cpu 0 不知道,仍然以为 i = 0。这就出现了缓存的不一致。其实这也就说明了出现可见性问题的原因。这时候我们不妨思考一下,出现这种情况我们通常会怎么做呢?加锁,我们可以给缓存行加锁。那么问题来了,加了锁之后怎么让其他cpu知道呢?那可不可以让cpu1修改完成之后通知一下cpu 0 ,cpu 0再执行操作的时候先去读取最新的数据,这样是不是更合理一些?由此我们需要引入MESI(缓存一致性协议)

MESI协议

缓存一致性协议MESI(Memory Element State Encoding and Identification)是指一种用于管理缓存数据的协议。每个缓存行(Cache line)有4个状态,分别是M(修改)、E(独占)、S(共享)和I(无效)。

这里分享一个网站,通过动画的形式帮助理解MESI协议 VivioJS MESI help (tcd.ie)
在这里插入图片描述
我们先假设只有CPU 1 在做读写操作,首先 CPU 1去读取a0 ,我们通过动画可以清楚的看到 CPU 1做了哪几步

  1. CPU 1 首先通过地址总线找到内存中a0的地址。
  2. 找到a0 并读取到a0的值通过数据总线写入到缓存行中,此时CPU 1的缓存行由初始的I(无效)状态 改为了E(独占)状态。
  3. 地址总线将 CPU 1读取a0 的操作告知 CPU 0 和 CPU2(注:这个地方的告知操作是通过动画演示猜测的结论,具体详情还得参照专业资料进行验证)。
  4. 因为只有CPU 1读取 a0 所以缓存行直接改为了E(独占)状态。
    在这里插入图片描述
    当 CPU 1 对a0进行写操作时 此时缓存行中的状态 改为 M(修改)状态
    在这里插入图片描述
    我们恢复到初始状态,正常状态下肯定是三个CPU核心同时工作,那三个CPU核心同时读取又会有怎样的变化
    在这里插入图片描述
  5. 每个CPU核心都会通过地址总线寻址 数据总线读取数据写入到缓存行中,将状态 I(无效)改为了S(共享)。
  6. 每个CPU核心的读取的操作都会通过地址总线告知其他CPU核心。

我们还是假设CPU 1 进行写操作会发生什么样的变化
在这里插入图片描述

  1. CPU 1 的缓存行由 S(共享)改为了 E(独占),同时将 a0 = 1通过数据总线写入到内存中。
  2. 通过地址总线告知其他CPU核心,此时其余CPU核心的缓存行状态由 S(共享)改为了 I(无效)。
  3. 如果 CPU 0 要对a0 进行读写操作,则需要重新读取a0的值,同理 CPU 2也一样。

如此反复进行的状态变化,确保了各个CPU 核心再进行读写操作时都能获取到最新的数据,缓存一致性问题得到解决。说了这么多,那跟volatile关键字有什么关系吗,接下来我们把思绪拉回来,回到volatile关键字上来。我们之前猜测的解决缓存一致性问题最初的方案是加锁,那这个锁应该加到什么地方,换句话说它是怎么进行加锁的呢?

我们可以打印查看一下volatile在汇编指令层面是怎么做的,在打印之前要做如下前置工作

  1. 下载 hsdis-amd64.dll

  2. 将 hsdis-amd64.dll 放入你的
    在这里插入图片描述

    jre1.8.0_301\jre\bin\server 注意是 jre目录下

  3. 在idea中编辑指令 -server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=compileonly,VolatileDemo.
    在这里插入图片描述
    运行结果,我们会看到一堆汇编指令着实看不懂,不过不要紧,锁它的英文是lock 那我们直接搜一下有没有lock相关的指令

  0x000002758a247a75: add    %al,(%rax)
  0x000002758a247a77: add    %al,(%rax)
  0x000002758a247a79: add    %bh,0x0(%rdi)
  0x000002758a247a7f: mov    %dil,0x68(%rsi)
  0x000002758a247a83: lock addl $0x0,(%rsp)     ;*putstatic stop
                                                ; - com.example.demo.ThreadExample.VolatileDemo::<clinit>@1 (line 4)

  0x000002758a247a88: add    $0x30,%rsp
  0x000002758a247a8c: pop    %rbp
  0x000002758a247a8d: test   %eax,-0x1e17993(%rip)        # 0x0000027588430100
                                                ;   {poll_return}

我们可以看到 在代码第四行加了个锁,这不就是我们写的volatile关键字
在这里插入图片描述
至此我们明白了,原来volatile解决可见性问题本质也是加了锁,只是在汇编指令层面。

什么是有序性问题

有序性问题是指程序中代码的执行顺序问题, 简单来说就是CPU为了提高代码执行效率,会对执行顺序进行重排序,在单线程环境下不存在问题,但在多线程环境下就会出现指令重排序问题。我们依旧按事实说话,通过一段代码来展示指令重排序问题

/**
 * 测试类
 */
public class Test {
    private static int x = 0, y = 0;
    private static int a = 0, b = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            a = 1;
            x = b;
        });

        Thread t2 = new Thread(() -> {
            b = 1;
            y = a;
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("x: " + x + ", y: " + y);
    }
}

首先我们看到这段代码理论上能够得出 x:0 y:1 但由于指令重排序得原因 结果可能有 x:0 y:1 / x:1 y:0 / x:1 y:1 / x:0 y:0 多运行几次之后会出现如下的结果
在这里插入图片描述
在这里插入图片描述
由此我们可以看到指令重排序问题对运行结果产生的影响

如何解决有序性问题

既然问题出现了,那就得把问题解决掉。如何解决指令重排序非常简单,还是围绕本篇重点volatile 关键字

public class Test {
    private volatile static int x = 0, y = 0;
    private volatile static int a = 0, b = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            a = 1;
            x = b;
        });

        Thread t2 = new Thread(() -> {
            b = 1;
            y = a;
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("x: " + x + ", y: " + y);
    }
}

加了 volatile关键字之后 无论执行多少遍 都是 x:0 y:1

探究有序性问题的本质

那 volatile 关键字是如何解决有序性问题的,这还得从CPU层面说起,在文章的上半部分我们知道到了CPU引入高速缓存来提升效率,效率是提升了可是出现缓存一致性问题,为了解决缓存一致性问题又引入了MESI协议 ,但是我们细品是可以发现缓存一致性协议可能会影响CPU的使用率
在这里插入图片描述
假设有S(共享)状态的缓存行 如果CPU0 要对这个缓存行进行修改,那么CPU0会发送 invalidate消息给 CPU1 ,CPU1收到后会发送一个ack应答给CPU0。 在这个一问一答的过程中CPU0就一直处于闲置的状态。我们知道CPU的资源是很宝贵的,能充分利用就充分利用。所以为了减少这个闲置的情况,CPU做了优化引入了 Store Buffers

Store Buffers

Store Buffers是处理器中的一种硬件结构,用于存储已完成的store(存储)指令的结果,以便后续的load(加载)操作可以从中读取。 当处理器执行store指令时,它不会立即将结果写入内存,而是会先将结果存储在Store Buffers中。这样可以提高处理器的性能
在这里插入图片描述

具体是什么意思呢,我们可以这样理解,有了Store Buffers之后, CPU0会先发送一个invalidate消息给CPU1,并把当前修改的数据直接放入Store Buffers中,然后继续执行后续指令。等到CPU1发送ack之后,CPU0再从Store Buffers读取出来。这个操作跟常见消息中间件异步方式类似,总的来说就是为了不让CPU闲下来,直接让它执行后续操作。对于CPU引入Store Buffers确实带来效率的提升,但同样也存在问题比如说有这样一段代码

int a = 1
int b = a+1
assert(b==2)

理论上的结果 b==2 为true ,但实际可能得到的结果为false ,有的小伙伴会问,这是为什么呢,我们可以简单的理解为

  1. 假设有缓存行中的a=0, CPU0 将 a = 1放入到 Store Buffers 并发送invalidate 给CPU1 。
  2. 此时 缓存行中的数据还是a = 0,Store Buffers 中的是 a = 1。
  3. 而 CPU1在没有发送ack 给CPU0之前 CPU0 不用等待,直接执行后续代码 也就是 b = a+1 ,而这时候a = 0

其实在我们实际开发过程中写代码的时候涉及到缓存数据操作都会注意缓存与DB数据的一致性。同理这个地方的 Store Buffers 相当于一个异步操作,当缓存行的数据和Store Buffers 中的数据不一致时就会对最终的运行结果产生影响,这其实也是可见性问题的源头。

既然收到ack后才去加载Store Buffers加载会有问题,那能不能直接进行读取呢?CPU肯定也会继续优化,所以引入了 Store Forwarding

Store Forwarding

Store Forwarding 是指每个CPU在加载数据之前,会先引用当前CPU的 Store Buffers,也就是说后续的操作时也能引用到Store Buffers 中的数据,而不是去先读取缓存行。
在这里插入图片描述
我们还是用刚才那段代码

int a = 1
int b = a+1
assert(b==2)

假设 CPU0 在执行 b = a+1 时,会从缓存行中加载a的值,在引入Store Forwarding机制后,CPU0会直接从Store Buffers读取数据,而Store Buffers中a=1 ,所以得出b=2。哎!这么一看这样好像就把问题给解决了,但真的是这样吗?我们来看下面一个例子

int a=0;
int b=0;
void cpu0(){
    a=1;
    b=1;
}
void cpu1(){
  while(b==1){
    assert(a==1);
  }
}

在这段代码示例中 a、b的初始值都为0,假设CPU0 在执行cpu0() 方法,CPU1执行cpu1()方法。且a存在于CPU1的高速缓存中,b存在于CPU0的缓存行中,a和b都是Execution(独占)状态 那这个例子可能会 出现cpu1方法中 b == 1成立 而a==1断言失败。出现问的原因是什么呢?我们可以来简单推导一下这个过程:

  1. 假设在多线程环境下 CPU0 在执行cpu0() 方法,CPU1执行cpu1()方法,且a和b没有强依赖关系,是能够出现指令重排序的。
  2. 当CPU0执行a=1时,因为a不存在于CPU0的缓存行中,所以a=1会直接放入Store Buffers中,并送消息告诉CPU1
  3. 这时候CPU1执行到 while(b==1) 因为CPU1缓存行中没有b,所以要去读取b,同样的也发送个消息给到CPU0
  4. 接下来 CPU0执行b=1,因为b=0在CPU0的缓存行中,直接写入b=1到缓存行中。同时收到了CPU1的消息,返回给CPU1并修改b=1为共享状态
  5. 问题就出来了,都操作的是b,没人去管a。当CPU1获取到b=1后,b == 1为true 。再执行assert(a == 1)时就会断言失败,因为此时a还在Store Buffer里面,没有同步到缓存行,所以此时a==0。

本质是CPU不知道a和b之间的数据依赖,在多线程环境下会出现b比a先在缓存行中生效,导致当CPU1读到b==1时,a依旧在Store Buffers中。其实还是因为异步操作所导致的。

Invalidate Queues

在推导 Store Forwarding出现的问题的过程中我们不难发现,虽然写入Store Buffers会一定程度上提升效率,但为了遵循缓存一致性协议,还是要发送 Invalidate Acknowledge,并等着返回,如果CPU处于繁忙状态,返回也会变慢。另外Store Buffers本身的存储也是有限的,那能不能直接返回,然后再进行逐个处理呢。这就跟消息队列的思想接近了。所以CPU又做了一次优化 ,引入了Invalidate Queues。
在这里插入图片描述
虽然引入了Invalidate Queues 后之后,能够很快得到其他CPU的响应(相当于在原来异步的基础上又做了一次异步)加快了Store Buffers的处理效率。但是依旧是会存在问题的,还是之前那段代码为例

int a=0;
int b=0;
void cpu0(){
    a=1;
    b=1;
}
void cpu1(){
  while(b==1){
    assert(a==1);
  }
}

还是假设 a,b初始为0,a在CPU0、CPU1均为Shared(共享)状态,b在CPU0 是 Exclusive(独占)状态。CPU0执行cpu0()方法,CPU1执行cpu1()方法。assert(a==1) 还是false。我们继续来推导一下过程,哪里会出现问题

  1. 当 CPU0执行 a=1 直接将a=1放入Store Buffers中。然后发送一个消息给CPU1,同时CPU1 读取b,但b不在CPU1的缓存行中,所以CPU1要加载b并发送一个消息给CPU0

  2. CPU1收到了CPU0的消息,把消息放入Store Buffers中直接返回ack给CPU0(这时我们可以感受到CPU0发送消息到收到应答明显加快了,减少了等待应答的时间)a=1写入缓存行中

  3. 此时CPU0执行b=1 又因为b=0是独占状态所以b=1变为修改状态存在缓存行中,这时候CPU0收到了CPU1读取b的消息,又把b变成了共享状态

  4. CPU1执行 while(b == 1) ,条件为true 。关键点来了,在CPU1中的a=0,因为a=1的消息存入到了Invalidate Queues 当中,最后才去处理a=1的操作,所以 assert(a==1) 断言失败

    失败的原因就是最后才处理 Invalidate Queues 中的消息,CPU做了这么多工作仍然存在问题,就很疑惑。问题解决不掉吗,这时候我们暂停下思绪,不妨来捋一下,

    1. 不产生指令重排序的方法不就是按顺序执行,加volatile关键字就能解决问题,

    2. 那不就证明使用volatile能按顺序执行,具体是怎么做的呢?

由此我们引出了内存屏障的概念

内存屏障

什么是内存屏障?先来一波书面解释

内存屏障(Memory Barrier)是计算机系统中用来控制内存访问和指令执行顺序的一种机制。它会在特定位置插入一个障碍,保证了在障碍之前的所有指令执行完成后,才能执行障碍后的指令。

内存屏障可以分为两类:读屏障(Read Barrier)和写屏障(Write Barrier)。

读屏障用于保证在读取操作之前,之前的写入操作已经完成。它可以防止读取过时(stale)的数据,保证内存的一致性。

写屏障用于保证在写入操作之后,之后的读取操作会看到已经完成的写入操作。它可以避免写入操作被乱序执行,保证数据的可见性和一致性。

内存屏障在多线程和多核处理器系统中起到了重要的作用。它可以确保多个线程之间对共享变量的读写操作的顺序执行,避免了数据的不一致和出现竞态条件(Race Condition)。此外,它也可以优化性能,例如允许乱序执行多个非相关指令,提高指令级并行度。 总而言之,内存屏障是用来控制内存访问和指令执行顺序的一种机制,它可以保证多线程操作的顺序性和一致性。

简单来说就是 CPU在性能优化道路上导致的顺序一致性问题,在CPU层面无法被解决,原因是CPU只是一个运算工 具,它只接收指令并且执行指令,并不清楚当前执行的整个逻辑中是否存在能不能优化的问题,也就是说 硬件层面也无法优化这种顺序一致性带来的可见性问题。对于开发人员来说我们并不需要关心内存屏障,我们更多的是直接利用。不同的CPU类型,内存屏障的指令也不同。如何通过volatile关键是来使用到内存屏障,这里作者水平有限,又涉及到更多的底层知识就不作展开解释了。但是有个重要的点需要知道。在上半篇中在研究可见性问题时,我们看到是在汇编指令层面有个#lock指令来解决可见性问题,同样内存屏障的核心也是这个#lock指令。

JMM模型

JMM即Java Memory Model,是一种抽象概念,并非真实存在。它描述的是一种规则或规范,通过这组规范定义了程序中各个变量(比如类的成员变量、静态属性等等)的访问方式。屏蔽了各种硬件和操作系统的访问差异, 保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范

Happens-Before 模型

并不是所有的程序指令都会存在可见性或者指令重排序问题。那有没有哪些情况是,不需要通过增加volatile关键字,也能保证在多线程环境下的可见性和有序性,所以我们引出 Happens-Before规则

概念

Happens-Before是一种依赖传递关系,定义为hb(A, B),表示在Java内存模型中,A操作的结果可以被后续操作B获取。Happens-Before规则用于定义一些禁止编译优化的场景,保证并发编程的正确性。它有以下几种常见的规则

程序顺序型规则

不管程序如何进行重排序,单线程的情况下,执行结果一定不会发生变化,举个例子

int a=1;
int b=2;
int c = a*b;

有的小伙伴可能有疑惑,a和b没有依赖关系,那这是可以进行重排序的,会存在先执行b=2再执行a=1 。那要先执行c呢,不可能先执行c,编译就通过不了先执行c就报错了。a,b重排序对结果没有任何影响。所以这里要说的是程序顺序型规则不是说必须要按照顺序去执行,只是不用去考虑重排序造成的影响。

传递性规则

传递性规则指的是

假设 A happens-before B, B happens-before C。 那么 A happens-before C成立

这个很好理解,假设A在B之前,B在C之前,那么A肯定在C之前。这表示不管在单线程还是多线程的环境中,传递性规则都能提供可见性保障。

volatile 变量规则

volatile变量规则通过内存屏障来保障一个volatile修饰的变量的写操作一定happens-before 于其读操作。关于volatile变量规则有这么一个表可以方便理解
在这里插入图片描述
举个例子

public class VolatileExample {
    int a=0;
    volatile boolean flag=false;
    public void writer(){
        a=1; //1
        flag=true; //2
    }
    public void reader(){
        if(flag){ //3
        int i=a; //4
        }
    }
}

假设两个线程 线程A和线程B 分别执行 writer() 和 reader() 方法,那么i的值始终为1。那有的小伙伴也会有疑问,在单线程的环境下,两个指令不存在依赖关系,会发生指令重排序,那writer() 方法中a=1 和 flag=true会重排序吗?其实我们可从从上面的那张表中得出结论。第一个操作a=1(普通读/写),第二个操作 flag=true(volatile写 这里因为flag是volatile修饰的)所以不允许重排序

监视器锁规则

一个线程对于一个锁的释放锁操作,一定happens-before与后续线程对这个锁的加锁操作。这个很好理解假设一个线程A抢到了锁,并修改了变量y的值且释放了锁,那么线程B获取到锁后,能够读取到线程A对变量y修改后的值。

start 规则

在happens-before规则中,如果一个线程的启动操作(即调用start()方法)发生在另一个线程的执行之前,那么这两个线程的操作(事件)就存在happens-before关系。这意味着第一个线程的启动操作的结果需要对第二个线程的执行可见。这个看着很拗口,其实也很好理解,用代码解释就能一目了然

public class StartTest {
    static int a = 0;
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            System.out.println(a);
        });
        a = 1;
        t1.start();
    }
}

这里说的是main线程的操作在t1线程启动之前,那么a=1对t1线程可见。其实跟我们正常写代码的逻辑是一样的

join 规则

在happens-before规则中,如果一个线程的join操作(即调用另一个线程的join()方法)发生在另一个线程的执行之前,那么这两个线程的操作(事件)就存在happens-before关系。这意味着第一个线程的join操作的结果需要对第二个线程的执行可见。这个还是用代码来解释更直接

public class StartTest {
    public static void main(String[] args) {
        try {
            Thread t1 = new Thread(()->{
                System.out.println("t1线程执行");
            });
            t1.start();
            t1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("main 线程启动");
    }
}

当注释掉 t1.join()时,因为是个异步执行,所以main线程结果先输出,当释放 t1.join()注释后,则必须等到t1线程执行完成之后main线程才会执行。

总结

本篇文章主要讲述了volatile关键字的解决可见性和有序性问题的本质。其中,可见性问题是因为CPU高速缓存引起的。为了保证缓存一致性,引入了MESI协议来保证缓存一致性。经过一系列猜想与论证,我们知道了volatile关键字解决可见性问题的核心是在汇编指令层面有个lock指令控制的。一致性是保证了可速率降低了,可不能让CPU闲下来,于是乎CPU又经过一系列的 Store Buffers、Store Forwarding以及 Invalidate Queues 的优化,然而这些优化手段又带了可见性问题,为了解决可见性问题,我们得知了内存屏障的概念,通过内存屏障来解决可见性问题。总的来说volatile解决的是CPU不断优化所带来的问题。本篇的完成度欠佳,尤其是涉及内存屏障这块内容时,知识匮乏无法说的更细致,后续会修改和补充。

  • 56
    点赞
  • 49
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值