java基础巩固-宇宙第一AiYWM:为了维持生计,多高(多线程与高并发)_Part5~整起(JMM,java内存模型:(是一种约定,不是存在的实物)、内存模型三大特性、volatile)

这个应该分类在JVM那里,不过无所谓咯,这不是重点。
为啥要用多线程呢?为啥要用高并发呢?~见线程篇
在这里插入图片描述
换句话说,引入缓存为了平衡CPU和存储设备的运算速度的差异,但是也引入了新的问题:缓存一致性

  • 其实 除了增加高速缓存外处理器还会对输入的代码进行乱序优化(指令重排序)来使得处理器内部的运算单元能尽量被充分利用
    • 所以不仅仅引入了缓存一致性问题还有指令重排序问题(因为CPU你丫想利用指令重排序优化执行效率呀)
      在这里插入图片描述
      在这里插入图片描述
  • 在多处理器系统中每个处理器都有自己的高速缓存,并且多个处理器共享同一个主内存,当多个处理器的运算任务都涉及同一块主内存时将可能导致各自的缓存数据不一致的情况(各自为王,多个处理器各自的缓存的数据同步刷回到主内存时以谁的缓存数据为王(准)呢?)

在大白话说一下,为什么有这个内存模型呢,我感觉主要的原因就是,举个例子,打手们(线程们)去打擂台,倘若现在是一个全国大赛,打手们都用同一个擂台(线程们共同操纵或者访问的同一个变量)。此时就发现问题了…

  • 如果第一个打手来说,这个擂台太高了太硬了太宽了或者旁边没放他爱嚼的草莓味的牙套(这些都可以看作线程们共同操纵或者访问的同一个变量)的时候,举办方说,好吧,那就把擂台(线程们共同操纵或者访问的同一个变量)按照第一个打手的要求长宽高重构一下,同时把牙套重新换成草莓味的
  • 第二个打手来了,又说这个擂台太高了太硬了太宽了或者旁边没放他爱嚼的苹果味的牙套的时候,举办方说,再换…
  • 第三个打手…
  • 第四个打手来了…

此时,出来的好办法就是,官方要求(官方出台了一项约定~(java内存模型是一种约定指定了一组排序规则(各个变量的访问规则,也就是在虚拟机中将变量存储到内存和从内存中取出变量(这里的变量不是咱们Java中的变量,这个变量包含了实例字段、静态字段和构成数组对象的元素,但是不包括局部变量与方法参数(为啥不包含这俩,因为这俩是线程私有的根本就不会被共享,有啥竞争问题?))的相关规则),来保证多线程对共享内存修改后彼此之间的可见性,屏蔽掉各种硬件和OS的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果,不是存在的实物),就是 在底层处理器内存模型的基础上,定义自己的多线程语义。它明确指定了一组排序规则【这一组规则被称为 Happens-Before】,来保证线程间的可见性。)
在这里插入图片描述

  • 总结一下,JMM 存在的两个原因:
    • 编程语言也可以直接复用操作系统层面的内存模型,但是不同的操作系统内存模型不同,如果直接复用操作系统层面的内存模型,就可能会导致同样一套代码换了一个操作系统就无法执行了Java 语言是跨平台的,它需要自己提供一套内存模型以屏蔽系统差异
    • 另外一个原因就是,可以把 JMM 看作是 Java 定义的并发编程相关的一组规范,除了抽象了线程和主内存之间的关系之外,其还规定了从 Java 源代码到 CPU 可执行指令的这个转化过程要遵守哪些和并发相关的原则和规范,其主要目的是为了简化多线程编程,增强程序可移植性的
      • 为什么要遵守这些并发相关的原则和规范呢? 这是因为并发编程下,像 CPU 多级缓存和指令重排这类设计可能会导致程序运行出现一些问题。就比如上面提到的指令重排序就可能会让多线程程序的执行出现问题,为此,JMM 抽象了 happens-before 原则来解决这个指令重排序问题
  • JMM体现在下面三个方面:【并发编程三个重要特性】
    • 原子性-保证指令不会受到线程上下文切换的影响
      • 一次操作或者多次操作,要么所有的操作全部都得到执行并且不会受到任何因素的干扰而中断,要么都不执行。在 Java 中,可以借助synchronized 、各种 Lock 【synchronized 和各种 Lock 可以保证任一时刻只有一个线程访问该代码块,因此可以保障原子性】以及各种原子类实现原子性【各种原子类是利用 CAS (compare and swap) 操作(可能也会用到 volatile或者final关键字)来保证原子操作】。
    • 可见性-保证指令不会受cpu缓存的影响
      • 当一个线程对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。在 Java 中,可以借助synchronized 、volatile 【如果我们将变量声明为 volatile ,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取】以及各种 Lock 实现可见性
    • 有序性-保证指令不会受cpu指令并行优化的影响
      • 由于指令重排序问题,代码的执行顺序未必就是编写代码时候的顺序。指令重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致 ,所以在多线程下,指令重排序可能会导致一些问题。在 Java 中,volatile 关键字可以禁止指令进行重排序优化
  • 之前人家物理硬件和OS也是有内存模型的,但是你要用人家物理硬件的和OS的,当程序在这一套平台上跑的好好的换一套平台并发访问有可能就出错了,所以Java得有自己的,用平台的不方便。
    在这里插入图片描述
  • 不同架构的物理机器可以拥有不一样的内存模型哦
    在这里插入图片描述
  • 你前面这个打手提出要求对擂台(线程们共同操纵或者访问的同一个变量)和牙套(线程们共同操纵或者访问的同一个变量)进行修改了之后,必须通知我们官方,然后由我们官方提前通知后面的打手,后面的打手可以对照一下自己的口味和喜好,决定要不要进行某些更改。
    在这里插入图片描述
    ,这时,再看看线程的工作内存:
    在这里插入图片描述
  • 从上图也可以看出:在多处理器系统中:
    • 每个处理器都有自己的高速缓存
    • 并且多个处理器共享同一个主内存
  • CPU 缓存模型:为了解决计算机系统中主内存与 CPU 之间运行速度差问题,会在 CPU 与主内存之间 添加一级或者多级高速缓冲存储器( Cache )。这个 Cache 一般是被集成到 CPU 内部的,所以也叫 CPU Cache ,比如图中的缓存L1。
    • 区分:
      • CPU Cache 缓存的是内存数据用于解决 CPU 处理速度和内存不匹配的问题
      • 内存缓存的是硬盘数据用于解决硬盘访问速度过慢的问题
    • Cache内部是按行存储的,其中每一行称为 Cache 行。(Cache行是Cache与主内存进行数据交换的单位,Cache行的大小一般为2的幂次数字节)
      在这里插入图片描述
      在这里插入图片描述
    • CPU Cache 的工作方式:
      • 复制一份数据到 CPU Cache 中,当 CPU 需要用到的时候就可以直接从 CPU Cache 中读取数据,当运算完成后,再将运算得到的数据写回 Main Memory 中。但是,这样存在 内存缓存不一致性的问题 !比如我执行一个 i++ 操作的话,如果两个线程同时执行的话,假设两个线程从 CPU Cache 中读取的 i=1,两个线程做了 1++ 运算完之后再写回 Main Memory 之后 i=2,而正确结果应该是 i=3。
    • 当CPU访问或者修改某个变量时,首先会去看 CPU Cache 内是否有该变量,如果有则直接从CPU Cache中获取进行访问或者修改,否则就去主内存里面获取该变量。然后把该变量所在内存区域的一个 Cache 行大小的内存一次性都复制到 Cache 。由于存放到 Cache 行的**是内存块而不是单个变量**,所以可能会把多个变量存放到 Cache 行中(可能被多个线程访问或者说写入到的多个变量被放入了一个缓存行中(别扭,这样说,在多线程下访问同一个缓存行中的多个变量时才会出现伪共享)。这也是伪共享产生的原因)。当多个线程同时想修改一个缓存行里面的多个变量时就出现问题了(因为同时只能有一个线程操作缓存行),所以相比将每个变量放到一个缓存行,性能会有所下降,这就是 伪共享
      • 伪共享:一个缓存行加入了多个cell(累加单元)对象
        在这里插入图片描述
      • 变量a和b同时被放到了 CPU一级和二级缓存中。当线程1使用CPU1对变量a进行更新时 首先会修改 CPU1的一级缓存变量a所在的缓存行,这时候在 缓存一致性协议下 CPU2中变量a对应的一级缓存中的缓存行失效。那么线程2在写入变量a时就只能去二级缓存里查找,这就破坏了一级缓存。(但是一级缓存比二级缓存快呀,所以相当于捡了芝麻丢了西瓜)。这不就说明多个线程不可能同时去修改自己所使用的CPU中相同的缓存行中的变量(因为有缓存一致性协议存在,所以不能乱来)
        在这里插入图片描述
        • CPU 为了解决内存缓存不一致性问题可以通过制定缓存一致协议(比如 MESI 协议)或者其他手段来解决。 这个 缓存缓存一致性协议指的是在 CPU 高速缓存与主内存交互的时候需要准守的原则和规范。不同的 CPU 中,使用的缓存一致性协议通常也会有所不同。
          在这里插入图片描述
      • 上面也说了伪共享产生的原因,那为什么被多个线程写入的多个变量会被放入到一个缓存行中呢?–原因就是 缓存与内存交换数据的基本单位就是缓存行而不是一个单独的变量(当CPU 要访问的变量没有在缓存中找到时,根据程序运行的局部性原理就会把该变量所在内存中大小为缓存行的内存放入缓存行。)
        • 地址连续的多个变量才有可能会被放到一个缓存行中。比如当创建数组时,数组里面的多个元素就会被放入同一个缓存行。
          • 说了半天,那到底一次性把地址连续的多个变量按照一个整块的形式都拿到缓存行中是好是坏呀,不是咱们之前总会拓展思维很好吗(狗头)。—再好也好不过批判性思维:
            • 在单个线程下顺序修改一个缓存行中的多个变量会充分利用程序运行的局部性原则,从而加速了程序的运行
            • 在多线程下并发修改一个缓存行中的多个变量时就会竟争缓存行(在多线程下访问同一个缓存行中的多个变量时才会出现伪共享),从而降低程序运行性能
        • JDK 之前一般都是通过 字节填充的方式来避免该问题,也就是创建一个变量时使用填充字段填充该变量所在的缓存行(JDK 提供了sun.misc.Contended 注解,用来解 共享问题),这样就避免了将多个变量存放在同一个缓存行中
          在这里插入图片描述
          在这里插入图片描述
          买一赠一:
          在这里插入图片描述

从图中可以看出,JMM有如下规定:也就是线程操作共享变量时的步骤:

  • JMM固定将所有变量都存放在主内存中
    • (这里的变量不是咱们Java中的变量,这个变量包含了实例字段、静态字段和构成数组对象的元素,但是不包括局部变量与方法参数(为啥不包含这俩,因为这俩是线程私有的根本就不会被共享,有啥竞争问题?))
      • 主内存 :所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的本地变量(也称局部变量)
    • 在 JDK1.2 之前,Java 的内存模型实现总是从 主存 (即共享内存)读取变量,是不需要进行特别的注意的。而在当前的 Java 内存模型下,线程可以把变量保存 本地内存 (比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致
  • 每条线程还有自己的私有工作内存。当线程使用主内存中的变量时,线程会先 把主内存中的变量复制一个副本到线程自己私有的工作空间中或者工作内存 中,
    • 线程读写变量,都是针对自己私有的工作内存中的变量副本进行的,换句换说也就是线程不能直接读写主内存中的变量
      • 本地内存 :每个线程都有一个私有的本地内存来存储共享变量的副本,并且,每个线程只能访问自己的本地内存,无法访问其他线程的本地内存本地内存是 JMM 抽象出来的一个概念存储了主内存中的共享变量副本
    • 处理完后将自己私有工作空间中的变量值更新回主内存。所以如果时机没把握对就会存在与主内存的变量一致不一致问题。
      • 线程之间变量值的传递均需要通过主内存来完成。
  • 不同的线程之间也无法直接访问对方私有的工作内存中的变量
    • 线程之间变量值的传递均需要通过主内存来完成。
      在这里插入图片描述

然后呢,官方害怕出现这些消息通知的中途被打断的情况:

  • 前一个打手改了擂台和牙套这些变量之后,骑着马哼哧哼哧去找官方的路中,被什么事打断了,官方没收到前一个打手的更改这个消息。
  • 或者,打手为了去通知后一个打手擂台尺寸和牙套味道已被更改,让后面的打手自己考虑一下要不要再改,官方信使骑着马哼哧哼哧去找下一个的路中,被什么事打断了,下一个打手没收到前一个打手的更改这个消息。
    • 其实也是**由于Cache的存在而导致了共享变量的内存不可见问题**(也就是线程1写入的值对线程2不可见),看书中的例子:

既然害怕出现上面的中途被打断的情况们,大家肯定有所疑问,java内存模型是怎样实现或者说维护打手->官方->打手之间的消息传递的具体过程不被打断的呢,接着赏图。(Java内存模型是围绕着在并发过程中如何处理原子性、可见性、有序性这3个特征来建立的
在这里插入图片描述

  • 原子性
    • 保证多个操作的原子性最简单的方法就是使用 synchronized 关键字进行同步
      在这里插入图片描述
      在这里插入图片描述
      JMM中要求了以下操作必须是原子性的,不能被打断的,就算拿着金牌或者尚方宝剑也不能打断。(Java内存模型中定义了下面8种操作来保证主内存和工作内存之间的具体的交互协议,或者说 Java内存模型中定义了一个变量如何从主内存拷贝到工作内存、以及如何从工作内存同步回主内存之类的实现细节,为此Java 内存模型定义来以下八种同步操作)~基本上基本数据类型的访问读写都是具备原子性的
      在这里插入图片描述
      • lock(锁定):作用于主内存的变量,用来把一个变量标识为一条线程独占的状态。
      • unlock:作用于主内存的变量,用来把一个处于锁定状态的变量释放出来,释放出来的变量才可以被其他线程锁定
      • read(读取):作用于主内存的变量,用来把一个变量的值从主内存传输到线程的私有工作内存中,一边随后的load操作使用
      • load(载入):作用于线程私有的工作内存的变量,把read操作从主内存中得到的变量值放入工作内存的变量副本
        • 如果要把一个变量从主内存复制到工作内存那就要顺序执行read和load操作,可以不用连续执行,按顺序执行就行
      • use:作用于线程私有的工作内存的变量。用来把工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作
      • assign(赋值):作用于工作内存的变量,用来把一个从执行引擎接收到的值赋值给线程私有的工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个assign操作
      • store(存储):作用于线程私有的工作内存的变量,他把工作内存中一个变量的值传递到主内存中以便随后的write操作使用
      • write(写入):作用于主内存的变量,把store操作从线程私有的工作内存中得到的变量值放入主内存的变量中
        • 如果要把一个变量从工作内存同步刷回到主内存那就要顺序执行store和write操作,可以不用连续执行,按顺序执行就行
          在这里插入图片描述
          在这里插入图片描述
          在这里插入图片描述
          在这里插入图片描述
        • 从OS角度来看这个指令重排序,
          在这里插入图片描述
  • 可见性:指的是这个线程改动了主内存中的值,其他线程可不可见,你改了人家不知道人家就会用一个错的主内存中的值呀,你真是个坏人。(Java内存模型是通过在变量修改后将新值同步回主内存,然后在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的。编译器在遇到下面这三个关键字时会 插入相应的内存屏障来禁止指令重排序,或者说下面三个关键字volatile、synchronizatized、final能实现可见性,保证语义的正确性)
    • volatile(弱形式的同步)不仅 保证可见性还有有序性但不保证操作的原子性~synchronized 和 volatile 的区别是什么以及、什么叫进入或推出Synchronized、什么叫进入或退出volatile
      • volatile 保证可见性还有有序性但不保证操作的原子性,就像之前两个线程一个i++一个i–,只能保证看到最新值,不能解决指令交错【写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证读跑到它前面去,而有序性的保证也只是保证了本线程内相关代码不被重排序】,不就验证了这一点嘛
        • 在 Java 中如果我们将变量声明为 volatile ,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。volatile 关键字其实并非是 Java 语言特有的,在 C 语言里也有,volatile它最原始的意义就是禁用 CPU 缓存。如果我们将一个变量使用 volatile 修饰,这就指示 编译器,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。比如
          在这里插入图片描述
        • 要想保证原子性,利用 synchronized 、Lock或者AtomicInteger都可以
          在这里插入图片描述
      • Java 提供了几种语言结构,包括 volatile, final 和 synchronized, 它们旨在帮助程序员向编译器描述程序的并发要求:编译器在遇到这三个关键字时,会插入相应的内存屏障,保证语义的正确性
        • volatile:保证可见性和有序性
        • synchronized:保证可见性和有序性,并且通过管程(Monitor)保证一组动作的原子性
          • 有一点需要注意的是,synchronized 不保证同步块内的代码禁止重排序,因为它通过锁保证同一时刻只有一个线程访问同步块(或临界区),也就是说同步块的代码只需满足 as-if-serial 语义 - 只要单线程的执行结果不改变,可以进行重排序
        • final:通过禁止在构造函数初始化和给 final 字段赋值这两个动作的重排序,保证可见性(如果 this 引用逃逸就不好说可见性了)
      • volatile的底层实现原理是 内存屏障,Memory Barrier (Memory Fence)
        • volatile保证可见性
          • 对volatile变量的写指令后会加入写屏障
            在这里插入图片描述
          • 对volatile变量的读指令前会加入读屏障
            在这里插入图片描述
        • volatile保证有序性
          在这里插入图片描述
      • volatile关键字可以确保对一个变量的更新对其他线程马上可见。当一个变量被声明为 volatile时,线程在写入变量时 不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存 ,当其他线程读取该共享变量时 会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值。
        在这里插入图片描述
      • 一般遇见下面两种情况才会用volatile关键字:
        • 写入变量值不依赖变量的当前值时。(因为如果依赖当前值,将是获取->计算->写入三步操作,这三步操作不是原子性的,而 volatile刚好不保证原子性,所以不能用)
        • 读写变量值时没有加锁(因为 加锁本身已经保证了内存可见性,这时候不需要把变
          量声明为volatile)
    • synchronized
      • 可以同时保证可见性、原子性和有序性;
        • 通过管程(Monitor)\保证一组动作的原子性synchronized 不保证同步块内的代码重排序,但是被synchronized完全保护的变量就是不会发生因重排序而产生的可见性问题,就是这么霸气,因为它通过锁保证同一时刻只有一个线程访问同步块(或临界区),也就是说同步块的代码只需满足 as-if-serial 语义 ,所以只要单线程的执行结果不改变,synchronized允许可以进行重排序,但是被synchronized完全保护的变量就是不会发生因重排序而产生的可见性问题,就是这么霸气。)
          在这里插入图片描述
        • synchronized这个同步块的可见性是由“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)这条规则获得的”
      • 同时synchronized属于重量级操作,性能更低
    • final
      • 通过禁止在构造函数初始化和给 final 字段赋值这两个动作的重排序,保证可见性(如果 this 引用逃逸就不好说可见性了)
      • final关键字的可见性是指被final修饰的字段在构造器中一旦完成初始化,并且构造器没有把this的引用传递出去(this引用逃逸很危险,其他线程很有可能通过这个引用访问到初始化一半的对象),那在其他线程中就能看见final字段的值
        在这里插入图片描述
  • 有序性:如果在本线程内观察则所有的操作都是有序的(线程内表现为串行);如果在一个线程中观察另一个线程则所有的操作都是无序的(指令重排序现象【在多线程中指令重排序很伤。在不改变程序结果的前提下,这些指令的各个阶段可以通过重排序和组合来实现指令级并行,指令重排序技术在80’s中叶到90’s中叶占据了计算架构的重要地位。分阶段,分工是提升效率的关键!但是指令重排的前提是,重排指令不能影响结果】和工作内粗与主内存同步延迟现象)。
    • Java提供了两个关键字,volatile和synchronized来保证线程之间操作的有序性
      • synchronized是由一个变量在同一个时刻只允许一条线程对这个变量进行lock操作,这就决定了持有同一个锁的两个同步块只能串行的进入
      • volatile关键字可以禁止指令重排序,从而保证代码有序性
    • Java内存模型允许编译器和处理器对指令重排序以提高运行性能,并且 只会对不存在数据依赖性的指令重排序。(在单线程下重排序可以保证最终执行的结果与程序顺序执行的结果一致,但是在多线程下就会存在问题)
      • 常见的指令重排序有下面 2 种情况:【Java 源代码会经历 编译器优化重排 —> 指令并行重排 —> 内存系统重排 的过程,最终才变成操作系统可执行的指令序列
        • 编译器优化重排 :编译器(包括 JVM、JIT 编译器等)在不改变单线程程序语义的前提下,重新安排语句的执行顺序
        • 指令并行重排 :现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序
          • 除了上面两种情况内存系统也会有“重排序”,但是不是真正意义上的重排序。内存系统的重排序指的是在 JMM 里表现为主存和本地内存的内容可能不一致,进而导致程序在多线程下执行可能出现问题
      • 指令重排序可以保证串行语义一致,但是指令重排序没有义务保证多线程间的语义也一致 ,所以在多线程下,指令重排序可能会导致一些问题
        • 编译器和处理器的指令重排序的处理方式不一样。对于编译器,通过禁止特定类型的编译器的当时来禁止重排序对于处理器,通过插入内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)的方式来禁止特定类型的处理器重排序。指令并行重排和内存系统重排都属于是 处理器级别的指令重排序
      • int c = a + b;则可以说变量c依赖于a和b的值
      • 通过把变量声明为volatile的本身就可以避免指令重排序
        • 写volatile变量时,可以确保 volatile写之前的操作不会被编译器重排序到 volatile写之后
        • 读volatile变量时,可以确保 volatile读之后的操作不会被编译器重排序到volatile读之前
    • 有序性带来的影响:著名的double-checked locking单例模式
      • 先看看单check(单层if)
        在这里插入图片描述
      • double-check(双层if)
        在这里插入图片描述
        • 还得改进一下,加个volatile防止重排序:
          在这里插入图片描述
  • 前面说了Java 内存模型(JMM)指定了8种操作的访问规则,来保证线程间的并发访问安全。这八种内存访问操作还有一个等效判断原则称为 Happens-Before:Java内存模型中定义的两项操作之间的偏序关系(意思就是: 要想保证 B 操作能够看到 A 操作的结果,也就是满足可见性(无论它们是否在同一个线程),那么 A 和 B 之间必须满足 Happens-Before 关系)。换句话说操作A先行发生于操作B其实就是说在发生操作B之前,操作A产生的影响能被操作B观察到
    • Java 内存模型(下文简称 JMM)就是在底层处理器内存模型的基础上,定义自己的多线程语义。Java 内存模型明确指定了一组排序规则【这一组规则被称为 Happens-Before, JMM 规定,要想保证 B 操作能够看到 A 操作的结果(无论它们是否在同一个线程),那么 A 和 B 之间必须满足 Happens-Before 关系】,来保证线程间的可见性
      在这里插入图片描述
    • happens-before规定了对共享变量的写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结,【也就是如果抛开以下happens-before规则,JMM并不一定能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见】
      • 为什么需要 happens-before 原则? happens-before 原则的诞生是为了程序员和编译器、处理器之间的平衡。程序员追求的是易于理解和编程的强内存模型,遵守既定规则编码即可。编译器和处理器追求的是较少约束的弱内存模型,让它们尽己所能地去优化性能,让性能最大化
      • happens-before 原则的设计思想:
        • 为了对编译器和处理器的约束尽可能少,只要不改变程序的执行结果(单线程程序和正确执行的多线程程序),编译器和处理器怎么进行重排序优化都行
        • 对于会改变程序执行结果的重排序,JMM 要求编译器和处理器必须禁止这种重排序
      • JSR-133 对 happens-before 原则的定义:
        • 如果一个操作 happens-before 另一个操作,那么第一个操作的执行结果将对第二个操作可见,并且第一个操作的执行顺序排在第二个操作之前。更准确地来说,happens-before更想表达的意义是前一个操作的结果对于后一个操作是可见的,无论这两个操作是否在同一个线程里【操作 1 happens-before 操作 2,即使操作 1 和操作 2 不在同一个线程内,JMM 也会保证操作 1 的结果对操作 2 是可见的】
        • 两个操作之间存在 happens-before 关系,并不意味着 Java 平台的具体实现必须要按照 happens-before 关系指定的顺序来执行。如果重排序之后的执行结果,与按 happens-before 关系来执行的结果一致,那么 JMM 也允许这样的重排序
    • happens-before 和 JMM的关系:
      在这里插入图片描述
    • (下面这些先行发生关系不需要任何同步器协助就已经存在,如果两个操作之间的关系不在此列,并且无法从下列规则中推导出来的话,那么这两个操作就没有顺序行保障,虚拟机就可以对这两个操作随意的进行重排序
      在这里插入图片描述
      • 单线程规则:一个线程中的每个动作都 happens-before 该线程中后续的每个动作
        • 程序次序规则(Program Order Rule)在一个线程内, 按照程序代码顺序,写在前面的操作先行发生于写在后面的操作。准确地说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。
        • 线程解锁m之前对变量的写,对于接下来对m加锁的其它线程对该变量的读可见
      • 监视器锁定规则:监听器的解锁动作 happens-before 后续对这个监听器的锁定动作(举个例子,线程(不管是不是同一个)的解锁动作发生在锁定之前?这明显不对。happens-before 也是为了保证可见性,比如那个解锁和加锁的动作,可以这样理解,线程1释放锁退出同步块,线程2加锁进入同步块,那么线程2就能看见线程1对共享对象修改的结果)
        • 管程锁定规则:( Monitor Lock Rule):一个unlock操作先行发生于后面对同二个锁的lock操作。这里必须强调的是同一个锁;而“后面”是指时间上的先后顺序。
        • 解锁规则 :解锁 happens-before 于加锁
      • volatile 变量规则:对 volatile 字段的写入动作先行发生于( happens-before) 后续对这个字段的每个读取动作【volatile就是—>写到主存中,没个啥】,说白了就是 对 volatile 变量的写操作的结果对于发生于其后的任何操作都是可见的
        在这里插入图片描述
      • 线程 start 规则线程 start() 方法的执行先行发生于 happens-before 一个启动线程内的任意动作
        • 线程start前对变量的写,对该线程开始后对该变量的读可见
      • 线程 join 规则:一个线程内的所有动作 happens-before 任意其他线程在该线程 join() 成功返回之前
        • 线程终止规则Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束,Thread.isAlive()的返回值等手段检测到线程已经终止执行。
        • 线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用t1.isAlive()或t1join0等待它结束)
      • 线程中断规则(Thread Interruption Rule)1 .对线程interrupt(方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread. interrupted0方法检测到是否有中断发生。
        • 线程tl打断t2 (interrupt) 前对变量的写,对于其他线程得知t2 被打断后对变量的读可见(通过t2.interrupted或t2.isInterrupted)
      • 对象终结规则(Finalizer Rule):个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。
        • 对变量默认值(0, false, nul) 的写,对其它线程对该变量的读可见
      • 传递性:如果 A happens-before B, 且 B happens-before C, 那么 A happens-before C
        • 具有传递性,如果x hb-> y并且y hb-> z 那么有x hb-> z,配合volatile的防指令重排,
          在这里插入图片描述
          在这里插入图片描述
          在这里插入图片描述

老规矩,买一赠一,送个🕴关键字volatile的使用及其原理:

  • volatile的两层语义
    在这里插入图片描述
    • volatile保证变量对所有线程的可见性:当volatile变量被一个线程修改,新值对所有线程会立即更新。或者理解为多线程环境下使用volatile修饰的变量的值一定是最新的。(不用volatile修饰的普通值或者说普通变量的值必须通过主内存才能在线程间来回传递,从而实现可见性
    • jdk1.5以后volatile完全避免了指令重排优化实现了有序性
      • Java里面的运算并非原子操作导致了volatile变量的运算在并发下一样是不安全的,也就是volatile不能保证原子性,咱们还是得通过加锁(使用synchronized或者java.util.concurrent中的原子类)来保证原子性
  • volatile的原理:
    • 获取JIT(即时Java编译器,把字节码解释为机器语言发送给处理器)的汇编代码,发现volatile多加了lock addl指令,这个操作相当于一个内存屏障,使得lock指令后的指令不能重排序到内存屏障前的位置。这也是为什么JDK1.5以后可以使用双锁检测实现单例模式。load addi $0x0, (%esp)指令把修改同步到内存时意味着所有之前的操作都已经执行完成,这样就形成了“指令重排序无法越过内存屏障”的效果。【内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)是一种 CPU 指令,用来禁止处理器指令发生重排序(像屏障一样),从而保障指令执行的有序性。另外,为了达到屏障的效果,它也会使处理器写入、读取值之前,将主内存的值写入高速缓存,清空无效队列,从而保障变量的可见性。】
      • 只有一个CPU访问内存时并不需要内存屏障;但是如果有两个或者两个以上访问同一块内存时,而且其中有一个在观测另一个,就需要内存屏障来保障一致性了。
        在这里插入图片描述
      • 在 Java 中,Unsafe 类提供了三个开箱即用的内存屏障相关的方法,屏蔽了操作系统底层的差异:public native void loadFence();、public native void storeFence();、public native void fullFence();
    • lock前缀的另一层意义是使得本线程工作内存中的volatile变量值立即写入到主内存中,并且使得其他线程共享的该volatile变量无效化,这样其他线程必须重新从主内存中读取变量值。
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述
  • Java 内存区域和 JMM 的区别
    • JVM 内存结构和 Java 虚拟机的运行时区域相关,定义了 JVM 在运行时如何分区存储程序数据,就比如说堆主要用于存放对象实例
    • Java 内存模型和 Java 的并发编程相关,抽象了线程和主内存之间的关系就比如说线程之间的共享变量必须存储在主内存中,规定了从 Java 源代码到 CPU 可执行指令的这个转化过程要遵守哪些和并发相关的原则和规范,其主要目的是为了简化多线程编程,增强程序可移植性的

收工叠饭~

巨人的肩膀:
狂神说
Java并发编程之美
深入理解Java虚拟机

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值