多线程之内存模型|8月更文挑战

233 阅读6分钟

内存模型基本概念

在并发编程中,需要处理两个关键问题:线程之间如何通信及线程之间如何同步

线程之间的通信机制有两种:共享内存和消息传递。

​ 共享内存:线程之间共享程序的公共状态,通过写-读内存的公共状态进行隐式通信。

​ 消息传递: 线程之间没有公共状态,线程之间必须通过发送消息来进行通信。(类似消息队列)

同步是指程序中用于控制不同线程间操作发生相对顺序的机制。

并发编程中有3大重要特性

原子性 - 保证指令不会受线程上下文切换的影响

可见性 - 保证指令不受cpu缓存的影响

有序性 - 保证指令不会受cpu指令并行优化的影响

Java虚拟机规范中定义了一种Java内存 模型(Java Memory Model,即JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果。Java内存模型的主要目标就是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的细节

1673435-20190822095001999-1852846254.png

从上图来看,线程A与线程B之间如要通信的话,必须要经历下面2个步骤:

  1. 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。

  2. 然后,线程B到主内存中去读取线程A之前已更新过的共享变量。

可见性

退不出的循环

/**
 * @Author blackcat
 * @version: 1.0
 * @description:可见性(用不停止的  可以加volatile 从主存中获取变量)
 *              volatile适用仅用在一个写线程,多个读线程的情况
 *synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是
 *synchronized 是属于重量级操作,性能相对更低
 */
@Slf4j
public class Threadvisibility {

    private static  boolean flag = true;

    public static void main(String[] agrs) {
        new Thread(() -> {
            log.info("start");
            while (flag) {

            }
            log.info("end");
        }, "server").start();

        try {
            Thread.sleep(1 * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.info("修改flag");
        flag = false;
    }
}

volatile 可以用来修饰成员变量和静态成员变量,变量具有以下特性

可见性::对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。

原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile i++这种复合操作不具有原子性。

volatile适用仅用在一个写线程,多个读线程的情况。

volatile 原理

volatile 的底层实现原理是内存屏障,Memory Barrier(Memory Fence)

对 volatile 变量的写指令后会加入写屏障

对 volatile 变量的读指令前会加入读屏障

如何保证可见性

写屏障(Store Barrier),在写指令之后插入写屏障,能让写入缓存的最新数据写回到主内存,以保证写入的数据立刻对其他线程可见

读屏障(Load Barrier),在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主内存加载数据,以保证读取的是最新的数据。

import lombok.extern.slf4j.Slf4j;

/**
 * @Author blackcat
 * @create 2021/8/1 20:38
 * @version: 1.0
 * @description:
 */
@Slf4j
public class VolatileTest {

    public static void main(String[] args) {
        VolatileExample volatileExample = new VolatileExample();
        new Thread(() -> {
            volatileExample.writer();
        }, "write").start();

        new Thread(() -> {
            volatileExample.reader();
        }, "read").start();
    }
}
@Slf4j
class VolatileExample {
    int a = 0;
    volatile boolean flag = false;

    public VolatileExample() {

    }

    public void writer() {
        log.info("writer start");
        a = 1;
        flag = true;
        log.info("writer end");
    }

    public void reader() {
        log.info("before{}",a);
        if (flag) {
            log.info("after{}",a);
        }
    }
}

如何保证有序性

写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后。

读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前。

有序性

定义

指令重排是指在程序执行过程中, 为了性能考虑, 编译器和CPU可能会对指令重新排序.

CPU 支持多级指令流水线,例如支持同时执行 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回 的处理器,就可以称之为五级指令流水线。这时 CPU 可以在一个时钟周期内,同时运行五条指令的不同阶段(相当于一条执行时间最长的复杂指令),IPC = 1,本质上,流水线技术并不能缩短单条指令的执行时间,但它变相地提高了指令地吞吐率。

a = b + c;
d = e - f ;

先加载b、c(注意,即有可能先加载b,也有可能先加载c),但是在执行add(b,c)的时候,需要等待b、c装载结束才能继续执行,也就是增加了停顿,那么后面的指令也会依次有停顿,这降低了计算机的执行效率。

为了减少这个停顿,我们可以先加载e和f,然后再去加载add(b,c),这样做对程序(串行)是没有影响的,但却减少了停顿。既然add(b,c)需要停顿,那还不如去做一些有意义的事情。

综上所述,指令重排对于提高CPU处理性能十分必要。虽然由此带来了乱序的问题,但是这点牺牲是值得的。

指令重排可以保证串行语义一致,但是没有义务保证多线程间的语义也一致

//执行次数比较多,指令重排  (0,0)    
public class Disorder {

    private static int x = 0, y = 0;
    private static int a = 0, b = 0;

    public static void main(String[] args) throws InterruptedException {

        while (true) {
            reSort();
        }

    }
    static void reSort() throws InterruptedException {
        Thread one = new Thread(new Runnable() {
            public void run() {
                a = 1; //操作1
                x = b; //操作2
            }
        });
        Thread other = new Thread(new Runnable() {
            public void run() {
                b = 1; //操作3
                y = a;  //操作4
            }
        });
        one.start();
        other.start();
        one.join();
        other.join();
        if (x == 0 && y == 0) {
            System.out.println("(" + x + "," + y + ")");
        }
    }
}

DoubleCheckedLocking问题

/**
 * @Author blackcat
 * @version: 1.0
 * @description:双重检查
 */
public class DoubleCheckedLocking { //1
    
    //加入volitale
    private static Instance instance; //2

    /**
     *   instance = new Instance();
     *   可分解为3行伪代码
     *   1、分配对象的内存空间
     *   2、初始化对象
     *   3、设置instance指向刚分配的内存地址
     *    
     *   2与3的操作存在重排序
     *   当多线程时候会判断成instance == null  直接返回了对象但是对象还没有初始化
     */
    public static Instance getInstance() { //3
        if (instance == null) { //4:第一次检查
            synchronized (DoubleCheckedLocking.class) { //5:加锁
                if (instance == null) //6:第二次检查
                    instance = new Instance(); //7:问题的根源出在这里
            } //8
        } //9
        return instance; //10
    } //11

    static class Instance {
    }
}

happen-before 规则

  1. 程序顺序规则:一个线程中的每一个操作,happens-before于该线程中的任意后续操作。
  2. 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
  3. volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
  4. 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
  5. start规则:如果线程A执行操作ThreadB.start()启动线程B,那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作、
  6. join规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。