Skip to content

Latest commit

 

History

History
334 lines (228 loc) · 12.3 KB

File metadata and controls

334 lines (228 loc) · 12.3 KB

多线程原理

初步认识Volatile

一段代码引发的思考

public class App {

    public volatile static boolean stop=false;
    
    public static void main( String[] args ) throws InterruptedException {
        Thread t1=new Thread(()->{
            int i=0;
            while(!stop){ //condition 不满足
                i++;
            }
        });
        t1.start();
        Thread.sleep(1000);
        stop=true; //true
    }
}

volatile的作用

保证可见性

volatile如何保证可用性

-server -Xcomp -XX:+UnlockDiagnosticVMOptions - XX:+PrintAssembly - XX:CompileCommand=compileonly,*App.*

然后在输出的结果中,查找下 lock 指令,会发现,在修改 带有 volatile 修饰的成员变量时,会多一个 lock 指令。lock 是一种控制指令,在多处理器环境下,lock 汇编指令可以 基于总线锁或者缓存锁的机制来达到可见性的一个效果。

从硬件层面了解可见性本质

image-20220227234503478

什么叫缓存一致性呢?

有了高速缓存的存在以后,每个 CPU 的处理过程是,先将计算需要用到的数据缓存在 CPU 高速缓存中,在 CPU进行计算时,直接从高速缓存中读取数据并且在计算完成之后写入到缓存中。在整个运算过程完成后,再把缓存中的数据同步到主内存。

由于在多 CPU 种,每个线程可能会运行在不同的 CPU 内,并且每个线程拥有自己的高速缓存。同一份数据可能会被缓存到多个 CPU 中,如果在不同 CPU 中运行的不同线程看到同一份内存的缓存值不一样就会存在缓存不一致的问题

为了解决缓存不一致的问题,在 CPU 层面做了很多事情, 主要提供了两种解决办法

  1. 总线锁
  2. 缓存锁

总线锁和缓存锁

多 cpu 下,当其中一个处理器要对共享内存进行操作的时候,在总线上发出一个 LOCK#信号,这个信号使得其他处理器无法通过总线来访问到共享内存中的数据,总线锁定把 CPU 和内存之间的通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大,这种机制显然是不合适的

如何优化呢?最好的方法就是控制锁的保护粒度,我们只需要保证对于被多个 CPU 缓存的同一份数据是一致的就行。所以引入了缓存锁,它核心机制是基于缓存一致性协议来实现的。

缓存一致性协议

为了达到数据访问的一致,需要各个处理器在访问缓存时 遵循一些协议,在读写时根据协议来操作,常见的协议有 MSI,MESI,MOSI 等。最常见的就是 MESI 协议。接下来 给大家简单讲解一下 MESI

MESI 表示缓存行的四种状态,分别是

1.M(Modify) 表示共享数据只缓存在当前 CPU 缓存中, 并且是被修改状态,也就是缓存的数据和主内存中的数 据不一致

2.E(Exclusive) 表示缓存的独占状态,数据只缓存在当前 CPU 缓存中,并且没有被修改

3.S(Shared) 表示数据可能被多个 CPU 缓存,并且各个缓 存中的数据和主内存数据一致

4.I(Invalid) 表示缓存已经失效

在MESI 协议中,每个缓存的缓存控制器不仅知道自己的 读写操作,而且也监听(snoop)其它 Cache 的读写操作

image-20220227235144303

image-20220227235158463

对于 MESI 协议,从 CPU 读写角度来说会遵循以下原则:

CPU 读请求:缓存处于 M、E、S 状态都可以被读取,I 状 态 CPU 只能从主存中读取数据

**CPU 写请求:缓存处于 M、E 状态才可以被写。**对于 S 状 态的写,需要将其他 CPU 中缓存行置为无效才可写 使用总线锁和缓存锁机制之后,CPU 对于内存的操作大概 可以抽象成下面这样的结构。从而达到缓存一致性效果

image-20220227235313403

MESI 优化带来的可见性问题

通信问题!

CPU 缓存行的状态是通过消息传递来进行的。如果 CPU0 要对一个在缓存中共享的变量进行写入,首先需 要发送一个失效的消息给到其他缓存了该数据的 CPU。并 且要等到他们的确认回执。CPU0 在这段时间内都会处于 阻塞状态。为了避免阻塞带来的资源浪费。在 cpu 中引入 了 Store Bufferes

image-20220227235435704

CPU0 只需要在写入共享数据时,直接把数据写入到 store bufferes 中,同时发送 invalidate 消息,然后继续去处理其 他指令。

当收到其他所有 CPU 发送了 invalidate acknowledge 消息 时,再将 store bufferes 中的数据数据存储至 cache line 中。最后再从缓存行同步到主内存。

image-20220227235502074

1.数据什么时候提交是不确定的,因为需要等待其他 cpu 给回复才会进行数据同步。这里其实是一个异步操作

2.引入了 storebufferes 后,处理器会先尝试从 storebuffer 中读取值,如果 storebuffer 中有数据,则直接从 storebuffer 中读取,否则就再从缓存行中读取

image-20220228000510154

CPU的乱序执行->重排序->可见性问题

image-20220228000747247

CPU层面提供指令->内存屏障

所以在 CPU 层面提供了 memory barrier(内存屏障)的指 令,从硬件层面来看这个 memroy barrier 就是 CPU flush store bufferes 中的指令。软件层面可以决定在适当的地方来插入内存屏障。

CPU内存屏障

内存屏障是用来解决可见性问题的

内存屏障就是将 store bufferes 中的指令写入到内存,从 而使得其他访问同一共享内存的线程的可见性。

X86 的 memory barrier 指令包括 lfence(读屏障) sfence(写 屏障) mfence(全屏障)

写屏障:告诉处理器在写屏障之前 的所有已经存储在存储缓存(store bufferes)中的数据同步 到主内存,简单来说就是使得写屏障之前的指令的结果对 屏障之后的读或者写是可见的

读屏障:**处理器在读屏障之后的读 操作,都在读屏障之后执行。**配合写屏障,使得写屏障之前 的内存更新对于读屏障之后的读操作是可见的

全屏障:确保屏障前的内存读写操作 的结果提交到内存之后,再执行屏障后的读写操作

image-20220228001849685

volatile->lock->内存屏障->可见性

什么是JMM内存模型?

什么是JMM

导致可见性的根本原因是高速缓存、重排序

JMM最核心的价值是解决了有序性、可见性,JMM实际上提供了合理的禁用缓存以及禁止重排序的方法。

JMM定义了共享内存中多线程程序读写操作 的行为规范:在虚拟机中把共享变量存储到内存以及从内 存中取出共享变量的底层实现细节

通过这些规则来规范对内存的读写操作从而保证指令的正 确性,它解决了 CPU 多级缓存、处理器优化、指令重排序 导致的内存访问问题,保证了并发场景下的可见性。

JMM如何解决可见性有序性问题

JMM 提供了一些禁用缓存以及进制重排序的方 法,来解决可见性和有序性问题。这些方法大家都很熟悉: volatile、synchronized、final、happens-before

JMM如何解决顺序一致性问题

重排序问题

image-20220228203431250

2 和 3 属于处理器重排序。这些重排序可能会导致可见性 问题。

编译器的重排序,JMM 提供了禁止特定类型的编译器重排序。

处理器重排序,JMM 会要求编译器生成指令时,会插入内 存屏障来禁止处理器重排序

数据依赖,int a = 1; int b = a;,存在数据依赖不能做重排序。

as-if-serial,不管你怎么重排序,对单个线程执行的结果不能变。

JMM层面的内存屏障

语言级别:

CPU级别:

image-20220228204013606

Happens-Before

可见性保障,volatile以外,还提供了其他方法

JMM 中有哪些方法建立 happen-before 规则

1.程序顺序规则

一个线程中的每个操作,happens-before 于该线程中的 任意后续操作; 可以简单认为是 as-if-serial。

单个线程 中的代码顺序不管怎么变,对于结果来说是不变的 顺序规则表示 1 happenns-before 2; 3 happens-before 4

public class Demo {
    int a=0;
    volatile  boolean flag=false;

    public void writer(){ //线程A
        a=1;             //1
        flag=true;       //2
        // 1 happens before 2
    }
    public void reader(){
        // 3 happens before 4
        if(flag){  //3
            int x=a; //4
        }
    }
}

2.volatile规则

对于 volatile 修饰的变量的写的操作, 一定 happen-before 后续对于 volatile 变量的读操作; 根据 volatile 规则,2 happens before 3

// 2 happens before 3
public class Demo {
    int a=0;
    volatile  boolean flag=false;

    public void writer(){ //线程A
        a=1;             //1
        flag=true;       //2
        // 1 happens before 2
    }
    public void reader(){
        // 3 happens before 4
        if(flag){  //3
            int x=a; //4
        }
    }
}

3.传递性规则

1 happens-before 2; 3 happens-before 4 => 1 happens-before 4

// 1 happens before 4
public class Demo {
    int a=0;
    volatile  boolean flag=false;

    public void writer(){ //线程A
        a=1;             //1
        flag=true;       //2
        // 1 happens before 2
    }
    public void reader(){
        // 3 happens before 4
        if(flag){  //3
            int x=a; //4
        }
    }
}

4.start规则

public class StartRule {
    static int x=0;
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            //use x=10
        });

        x=10;

        t1.start();
    }
}

5.join规则

如果线程 A 执行操作 ThreadB.join()并成功返 回,那么线程 B 中的任意操作 happens-before 于线程 A 从 ThreadB.join()操作成功返回。

public class JoinRule {
    static int x=0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(()->{
            x=100;
        });
        t1.start();
        t1.join();
        System.out.println(x);
    }
}
public class JoinRule {
    static int x=0;
    public static void main(String[] args) throws InterruptedException {
        /*Thread t1=new Thread(()->{
            x=100;
        });
        t1.start();
        t1.join();
        System.out.println(x);*/

        Thread t1=new Thread(()->{
            System.out.println("t1");
            //执行的结果对于主线程可见
        });
        Thread t2=new Thread(()->{
            System.out.println("t2");
        });
        Thread t3=new Thread(()->{
            System.out.println("t3");
        });
        t1.start();
        t1.join(); //阻塞主线程 wait/notify
        //等到阻塞释放
        //获取到t1线程的执行结果.
        t2.start();
        t2.join(); // 建立一个happens-bebefore规则

        t3.start();
    }
}

6.监视器锁的规则,对一个锁的解锁,happens-before 于 随后对这个锁的加锁

public class SyncDemo {
    private App app =new App();
    public App getApp(){
        return app;
    }
    public void demo() {
        synchronized (this) {//ThreadA / ThreadB
        }
    }
}

多线程并发