乐趣区

关于java:高效并发下的高速缓存和指令重排

1. 前言

    对于计算机系统处理器资源的高效应用,计算机系统设计就引入 高速缓存 以解决 CPU 运算速度与主内存存储速度之间的速度不匹配问题;引入 指令重排 来晋升 CPU 外部运算单元的执行利用效率。

    晋升计算机处理器的运算能力,最简略、最无效的伎俩是让计算机反对多任务处理,能够充分利用处理器的运算能力。当然计算机操作系统的运算能力不单单取决于处理器,还需思考零碎中并行化与串行化的比重,磁盘 I / O 读写速度,网络通信,数据库交互等。

2. 高速缓存

2.1 高速缓存与缓存一致性

2.1.1 高速缓存

    计算机处理器运算速度远远超出计算机存储设备的读写速度。肯定水平上存储设备的读写速度限制了计算机系统的运算能力,引入高速缓存作为处理器和存储设备之间的一层缓冲。高速缓存的存储速度靠近处理器的运算速度,处理器无需期待主内存迟缓的读写操作,使得处理器高效的工作。

2.1.2 缓存一致性

  • 缓存一致性问题

引入高速缓存很好的解决了主内存读写速度与处理器运算速度相差几个数量级的问题。
但多处理器计算机系统下,存在某个时刻下,主内存中某个数据在不同处理器高速缓存中的数据不统一的状况。

  • 解决计划

(1)处理器都是通过总线来和主存储器 (主内存) 进行交互的,所以能够通过给总线加锁,解决缓存一致性问题;

(2)能够通过引入缓存一致性协定,来解决缓存一致性问题。

    总线,总线英文标识为 Bus,公共汽车,总线是连贯多个设施或者接入点的数据传输通路,处理器所有传出的数据都要通过总线交互主存储器。

    缓存一致性协定,要求处理器要遵循这些协定,这些协定规定了读写操作的标准来保障缓存一致性。

    Inter 处理器个别采纳的是 MESI 协定。MESI(Modified Exclusive Shared Or Invalid)(也称为伊利诺斯协定,是因为该协定由伊利诺斯州立大学提出)是一种宽泛应用的反对写回策略的缓存一致性协定,该协定被利用在 Intel 奔流系列的 CPU 中。

2.2 工作内存与主内存

    了解了高速缓存,工作内存类似的,高速缓存是从处理器角度登程,工作内存是从线程角度登程。

    所有的变量存储在主内存中,每条线程有本人的工作内存。此处主内存仅是虚拟机内存的一部分,与 Java 内存模型(程序计数器、Java 堆、非堆、虚拟机栈、本地办法栈) 没有关联。

  • 工作内存中保留了以后线程应用到变量的主内存拷贝,
  • 线程对变量所有的操作都在工作内存中进行。
  • 不同线程之间无奈间接拜访对方工作内存的变量
  • 线程间变量值的传递均需通过主内存来实现,工作内存交互主内存。

2.3 线程间工作内存交互主内存

    每个线程都对应本人的工作内存,批改共享变量的值后,从当前工作内存保留并写入到主内存。同样的,共享变量被其余线程批改后的新值,以后线程须要从主内存读取并载入到当前工作内存,能力进行应用。

    Java 内存模型定义了以下八种原子操作来作用于线程工作内存与主内存的交互。

操作 名称 作用内存 操作阐明
lock 锁定 主内存 标识某个变量为线程独占
unlock 解锁 主内存 开释某个被线程独占的变量
read 读取 主内存 变量的值从主内存传输到工作内存
load 载入 工作内存 把读取到的值放入工作内存的变量正本
use 应用 工作内存 把变量值传给执行引擎
assign 赋值 工作内存 把执行引擎接管到的值赋给变量
store 存储 工作内存 把工作内存变量的值传输到主内存
write 写入 主内存 变量值放入到主内存的变量中

3. 原子性、可见性、有序性

3.1.1 性质

  • 原子性

家喻户晓,原子操作是不可再拆分的操作,即原子性操作是并发平安的;

    原子操作蕴含 read、load、assign、use、store、write。

    lock 和 unlock 操作反对咱们对一个更大范畴操作提供原子性保障。直观来说,synchronized 关键字,被该关键字润饰的代码块具备原子性,应用该关键字能保障代码块的线程平安。

    synchronized 反映到字节码指令,蕴含 monitorenter 和 monitorexit 指令,这两个指令隐式调用了 lock、unlock 操作。

  • 有序性

在某个线程中所有的操作都是有序的。Java 程序中在另一个线程察看以后线程的操作,都是无序的。

    volatile 和 synchronized 都可保障线程之间操作的有序性。

    volatile 具备禁止指令重排序的能力。

    synchronized 具备 lock、unlock 能力,反对一个变量在同一时刻只许一个线程对其进行 lock 锁定操作。

  • 可见性

可见性体现在多线程之间,一个线程批改了某个共享变量 (线程间共享变量) 的值,其余线程能够立刻失去这个批改,即新值对其余线程是实时可见的。

    volatile 关键字润饰的共享变量,线程写入新值,线程间是可见的。

    volatile 变量与一般变量的区别,在于 volatile 变量的新值会立刻 store 存储到主内存中,在应用 volatile 变量时会先从主内存 read 读取新值 load 载入到当前工作内存。而一般变量应用时不会立刻从主内存刷新,当前工作内存若存在,则间接应用工作内存中变量的值。

3.1.2 可见性演示实例

    对于可见性,郭婶 (郭霖) 举了一个栗子,有助了解,这边就间接拿来了。

/**
 * @className: VisibilityDemo 
 * @description: 可见性演示实例
 **/
public class VisibilityDemo {
    private static volatile boolean flag = true;

    public static void main(String... args) {Thread thread1 = new Thread(() -> {while (true) {if (flag) {
                    flag = false;
                    System.out.println("Thread1 set flag to false");
                }
            }
        }, "Thread-01");
        Thread thread2 = new Thread(() -> {while (true) {if (!flag) {
                    flag = true;
                    System.out.println("Thread2 set flag to true");
                }
            }
        }, "Thread-02");
        // 别离启动两个线程
        thread1.start();
        thread2.start();}
}
  • 当共享变量 flag 为一般变量 private static boolean flag 时,程序中两线程会交替打印信息到控制台,一段时间后,两线程外部分支条件不再满足,将不再打印信息到控制台;
...
Thread2 set flag to true
Thread1 set flag to false
Thread1 set flag to false
Thread1 set flag to false
Thread2 set flag to true
Thread2 set flag to true
  • 当共享变量由 volatile 润饰时 private static volatile boolean flag,程序中两线程会继续交替打印信息到控制台;
...
Thread2 set flag to true
Thread1 set flag to false
Thread1 set flag to false
Thread1 set flag to false
Thread2 set flag to true
Thread2 set flag to true
...

3.1.3 可见性演示实例问题剖析

    因为线程工作内存与主内存存在缓存延时问题

    一个一般的线程共享变量private static boolean flag,在上例中存在,随着程序的运行,在某个时刻线程 Thread-01 的 flag 为 false,线程 Thread-02 的 flag 为 true,此时两者都不会进入分支构造体,不再执行赋值操作,不再刷新工作内存数据到主内存。两个线程都会进行输入信息到控制台。

    申明为 volatile 变量private static volatile boolean flag,会保障共享变量每次赋值都会即时存储到主内存,每次应用共享变量时,会从主内存读取并载入到以后线程工作内存再应用。应用关键字后的程序,两线程会继续交替输入信息到控制台。

4. 指令重排

4.1 就你 TMD 叫指令重排啊

    在以后线程察看 Java 程序,所有操作是有序的,但在其余线程察看以后线程的操作是无序的。即线程内体现为串行的语义,多线程间存在工作内存与主内存同步延时及指令重排序景象。

4.2 指令重排的线程平安问题

  • 多线程下指令重排的线程平安问题

咱们晓得处理器在指令集层面,会做肯定的指令排序优化,来晋升处理器运算速度。在单线程中能够保障对应高级语言的程序执行后果是正确的,即单线程下保障程序执行的有序性(及程序正确性);多线程状况下,在某个线程中察看其余线程的操作是无序的(存在线程共享内存时,则无奈保障程序正确性),这就是多线程下指令重排的线程平安问题。

4.2.1 指令重排演示实例

import lombok.SneakyThrows;

/**
 * @description: 指令重排:线程内体现为串行语义
 * @author: niaonao
 **/
public class OrderRearrangeDemo {
    static boolean initFlag;
    public static void main(String... args) {Runnable customRunnable = new CustomRunnable();
        new Thread(customRunnable, "Thread-01").start();
        new Thread(customRunnable, "Thread-02").start();}

    static class CustomRunnable implements Runnable {
        // @SneakyThrows 是 lombok 包下的注解
        // 继承了 Throwable 用于捕捉异样
        @SneakyThrows
        @Override
        public void run() {
            initFlag = false;
            Integer number = null;
            number = 1;
            initFlag = true;
            // 期待初始化实现
            while (!initFlag) { }
            System.out.println("name:" + Thread.currentThread().getName() + ", number:" + number);
        }
    }
}

    下面这个例子,在理论并发场景中很少呈现线程平安问题,但存在指令重排引起线程平安问题的危险。

  • 个别状况下执行后果为
name: Thread-01, number: 1
name: Thread-02, number: 1

Process finished with exit code 0
  • 指令重排存在的危险后果可能为
name: Thread-01, number: 1
name: Thread-02, number: null

Process finished with exit code 0

4.2.2 指令重排演示实例问题剖析

    线程内保障程序的有序性,多线程下处理器指令重排优化存在的状况如下(这里从高级语言来疾速了解,其实指令我也做不到啊),上面并没有列出所有状况。

    // 状况 -01
    initFlag = false;
    Integer number = null;
    number = 1;
    initFlag = true;
    while (!initFlag) { }
    System.out.println("name:" + Thread.currentThread().getName() + ", number:" + number);

    // 状况 -02
    initFlag = false;
    Integer number = null;
    initFlag = true;
    number = 1;
    while (!initFlag) { }
    System.out.println("name:" + Thread.currentThread().getName() + ", number:" + number);

    // 状况 -03
    initFlag = false;
    initFlag = true;
    Integer number = null;
    number = 1;
    while (!initFlag) { }
    System.out.println("name:" + Thread.currentThread().getName() + ", number:" + number);
    
    // 状况 -04
    Integer number = null;
    initFlag = false;
    number = 1;
    initFlag = true;
    while (!initFlag) { }
    System.out.println("name:" + Thread.currentThread().getName() + ", number:" + number);
    
    // 状况 -05
    Integer number = null;
    initFlag = false;
    initFlag = true;
    number = 1;
    while (!initFlag) { }
    System.out.println("name:" + Thread.currentThread().getName() + ", number:" + number);

    // 状况 -06
    Integer number = null;
    number = 1;
    initFlag = false;
    initFlag = true;
    while (!initFlag) { }
    System.out.println("name:" + Thread.currentThread().getName() + ", number:" + number);
    
    // 状况 -07
    Integer number = null;
    initFlag = false;
    initFlag = true;
    while (!initFlag) { }
    number = 1;
    System.out.println("name:" + Thread.currentThread().getName() + ", number:" + number);
    
    // 状况 -07
    Integer number = null;
    initFlag = false;
    initFlag = true;
    while (!initFlag) { }
    number = 1;
    System.out.println("name:" + Thread.currentThread().getName() + ", number:" + number);

    线程共享变量 initFlag 在线程 Thread-01 中曾经执行 initFlag = true 操作后,在线程 Thread-02 中读取到 initFlag 为 true,就会跳出 while 循环,此时因为指令重排,number 可能还没有赋值为 1,程序打印到控制台的信息会是name: Thread-02, number: null

4.3 禁止指令重排序

    指令重排有线程平安危险,怎么防止呢?

    欸,问得好 niaonao 同学,请坐。Java 提供 volatile 关键字具备两个个性,一是可见性,一是禁止指令重排。如 4.2.1 指令重排演示实例,就用 volatile 润饰共享变量 static boolean initFlag 即可。

    可见性就不再赘述了。对于禁止指令重排的原理是通过 volatile 润饰的共享变量,会增加一个内存屏障,处理器在做重排序优化时,无奈将内存屏障前面的指令放在内存屏障后面。

Powered By niaonao

退出移动版