并发编程的实现原理-volatile-笔记

JMM怎么解决原子性、可见性、有序性的问题?java

  • 在Java中提供了一系列和并发处理相关的关键字,
  • 好比volatile、Synchronized、final、juc(java.util.concurrent)等,
  • 这些就是Java内存模型封装了底层的实现后提供给开发人员使用的关键字,
  • 在开发多线程代码的时候,咱们能够直接使用synchronized等关键词来控制并发,
  • 使得咱们不须要关心底层的编译器优化、缓存一致性的问题了,
  • 因此在Java内存模型中,除了定义了一套规范,还提供了开放的指令在底层进行封装后,提供给开发人员使用。

原子性保障缓存

  • 在java中提供了两个高级的字节码指令monitorentermonitorexit
  • 在Java中对应的Synchronized来保证代码块内的操做是原子的

可见性安全

  • Java中的volatile关键字提供了一个功能,
  • 那就是被其修饰的变量在被修改后能够当即同步到主内存,
  • 被其修饰的变量在每次是用以前都从主内存刷新。
  • 所以,可使用volatile来保证多线程操做时变量的可见性。
  • 除了volatile,Java中的synchronizedfinal两个关键字也能够实现可见性

有序性多线程

  • 在Java中,可使用synchronized和volatile来保证多线程之间操做的有序性。
  • 实现方式有所区别:
    • volatile关键字会禁止指令重排。
    • synchronized关键字保证同一时刻只容许一条线程操做。

volatile 如何保证可见性架构

  • volatile变量修饰的共享变量,
    • 在进行写操做的时候会多出一个lock前缀的汇编指令
    • 这个指令在前面咱们讲解CPU高速缓存的时候提到过,
    • 会触发总线锁或者缓存锁,经过缓存一致性协议来解决可见性问题
  • 对于声明了volatile的变量进行写操做,
    • JVM就会向处理器发送一条Lock前缀的指令,
    • 把这个变量所在的缓存行的数据写回到系统内存,
    • 再根据咱们前面提到过的MESI的缓存一致性协议,
    • 来保证多CPU下的各个高速缓存中的数据的一致性。

volatile 防止重排序并发

  • 指令重排的目的是为了最大化的提升CPU利用率以及性能,
  • CPU的乱序执行优化在单核时代并不影响正确性,
  • 可是在多核时代的多线程可以在不一样的核心上实现真正的并行,
    • 一旦线程之间共享数据,就可能会出现一些不可预料的问题
  • 指令重排序必需要遵循的原则是,
    • 不影响代码执行的最终结果,
    • 编译器和处理器不会改变存在数据依赖关系的两个操做的执行顺序,
      • 这里所说的数据依赖性仅仅是针对单个处理器中执行的指令和单个线程中执行的操做.
  • 这个语义,实际上就是as-if-serial语义,
    • 无论怎么重排序,单线程程序的执行结果不会改变,
    • 编译器、处理器都必须遵照as-if-serial语义

多核心多线程下的指令重排影响性能

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=1,y=0;
  • x=1,y=1
  • 由于多是前后执行t1/t2,也多是反过来,还多是t1/t2交替执行,
  • 可是这段代码的执行结果也有多是x=0,y=0。
    • 这就是在乱序执行的状况下会致使的一种结果,
    • 由于线程t1内部的两行代码之间不存在数据依赖,
    • 所以能够把x=b乱序到a=1以前;
    • 同时线程t2中的y=a也能够早于t1中的a=1执行,那么他们的执行顺序多是
      • l t1: x=b
        l t2:b=1
        l t2:y=a
        l t1:a=1
  • 因此从上面的例子来看,重排序会致使可见性问题。
    • 可是重排序带来的问题的严重性远远大于可见性,
    • 由于并非全部指令都是简单的读或写,好比DCL的部分初始化问题。
    • 因此单纯的解决可见性问题还不够,还须要解决处理器重排序问题

内存屏障3d

  • 内存屏障须要解决咱们前面提到的两个问题,
  • 一个是编译器的优化乱序和CPU的执行乱序,
  • 咱们能够分别使用优化屏障内存屏障这两个机制来解决

从CPU层面来了解一下什么是内存屏障

  • CPU的乱序执行,
    • 本质仍是,因为在多CPU的机器上,每一个CPU都存在cache,
    • 当一个特定数据第一次被特定一个CPU获取时,
      • 因为在该CPU缓存中不存在,就会从内存中去获取,
      • 被加载到CPU高速缓存中后就能从缓存中快速访问。
    • 当某个CPU进行写操做时,
      • 它必须确保其余的CPU已经将这个数据从他们的缓存中移除,
      • 这样才能让其余CPU安全的修改数据。
    • 显然,存在多个cache时,咱们必须经过一个cache一致性协议来避免数据不一致的问题,
    • 而这个通信的过程就可能致使乱序访问的问题,也就是运行时的内存乱序访问。
    • 如今的CPU架构都提供了内存屏障功能,
      • 在x86的cpu中,实现了相应的内存屏障
        • 写屏障(store barrier)、
        • 读屏障(load barrier)
        • 全屏障(Full Barrier),
      • 主要的做用是
        • Ø 防止指令之间的重排序
        • Ø 保证数据的可见性
    • store barrier称为写屏障
      • 至关于storestore barrier,
      • 强制全部在storestore内存屏障以前的全部操做,
        • 都要在该内存屏障以前执行,并发送缓存失效的信号。
      • 全部在storestore barrier指令以后的store指令,
        • 都必须在storestore barrier屏障以前的指令执行完后再被执行。
      • 也就是进制了写屏障先后的指令进行重排序,
        • 是的全部store barrier以前发生的内存更新都是可见的
          • 这里的可见指的是修改值可见以及操做结果可见
    • load barrier称为读屏障,
      • 至关于loadload barrier,
      • 强制全部在load barrier读屏障以后的load指令
        • 都在loadbarrier屏障以后执行。
        • 也就是进制对load barrier读屏障先后的load指令进行重排序,
        • 配合store barrier,使得全部store barrier以前发生的内存更新,
          • 对load barrier以后的load操做是可见的
    • full barrier成为全屏障,
      • 至关于storeload,是一个全能型的屏障,
      • 由于它同时具有前面两种屏障的效果。
      • 强制了全部在storeload barrier以前的store/load指令,都在该屏障以前被执行,
      • 全部在该屏障以后的的store/load指令,都在该屏障以后被执行。
      • 禁止对storeload屏障先后的指令进行重排序。
  • 总结:
    • 内存屏障只是解决顺序一致性问题,
    • 不解决缓存一致性问题,
    • 缓存一致性是由cpu的缓存锁以及MESI协议来完成的。
    • 而缓存一致性协议只关心缓存一致性,不关心顺序一致性。
    • 因此这是两个问题

编译器层面如何解决指令重排序问题

  • 在编译器层面,经过volatile关键字,取消编译器层面的缓存和重排序。
    • 保证编译程序时在优化屏障以前的指令不会在优化屏障以后执行。
    • 这就保证了编译时期的优化不会影响到实际代码逻辑顺序。
  • 若是硬件架构自己已经保证了内存可见性,
    • 那么volatile就是一个空标记,不会插入相关语义的内存屏障
  • 若是硬件架构自己不进行处理器重排序,有更强的重排序语义,
    • 那么volatile就是一个空标记,不会插入相关语义的内存屏障。

在JMM中把内存屏障指令分为4类,

  • 经过在不一样的语义下使用不一样的内存屏障来进制特定类型的处理器重排序,从而来保证内存的可见性
    • LoadLoad Barriers, load1 ; LoadLoad; load2 ,
      • 确保load1数据的装载优先于load2及全部后续装载指令的装载
    • StoreStore Barriers,store1; storestore;store2 ,
      • 确保store1数据对其余处理器可见优先于store2及全部后续存储指令的存储
    • LoadStore Barries, load1;loadstore;store2,
      • 确保load1数据装载优先于store2以及后续的存储指令刷新到内存
    • StoreLoad Barries, store1; storeload;load2,
      • 确保store1数据对其余处理器变得可见, 优先于load2及全部后续装载指令的装载;
      • 这条内存屏障指令是一个全能型的屏障,
        • 在前面讲cpu层面的内存屏障的时候有提到。
        • 它同时具备其余3条屏障的效果

volatile为何不能保证原子性

public class Demo {
        volatile int i;
        public void incr(){
            i++;
        }
        public static void main(String[] args) {
            new Demo().incr();
        }
    }
  • 对一个原子递增的操做,会分为三个步骤:
    • 1.读取volatile变量的值到local;
    • 2.增长变量的值;
    • 3.把local的值写回让其余线程可见
相关文章
相关标签/搜索