并行与并发

并行:多个指令在多个处理器上同时执行 互补干扰
image.png
并发:同一时刻只能有一个指令执行 存在干扰
image.png

并发三大特性

可见性 (本节课重点)

一个线程改变了共享变量,其他线程能看到变化,
如何保证可见性

  • 通过 volatile 关键字保证可见性。
  • 通过 内存屏障保证可见性。
  • 通过 synchronized 关键字保证可见性。
  • 通过 Lock保证可见性。
  • 通过 final 关键字保证可见性

    有序性

    程序的执行顺序是按照代码的先后顺序执行,JVM存在指令重拍
    如何保证有序性

  • 通过 volatile 关键字保证可见性。

  • 通过 内存屏障保证可见性。
  • 通过 synchronized关键字保证有序性。
  • 通过 Lock保证有序性。

    原子性

    一个或多个操作要么全部执行,要么全部不执行
    基本数据类型变量的读取和赋值是原子性(64位处理器),自增并不一定
    32位处理long double 的操作
    Oracle官方解释:https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.7

    1. 非原子处理doublelong
    2. 出于 Java 编程语言内存模型的目的,对非易失性long double值的单次写入被视为两次单独的写入:每个 32 位一半。这可能导致线程从一次写入中看到 64 位值的前 32 位,而从另一次写入中看到后 32 位。
    3. longvolatilevalues 的写入和读取 double始终是原子的。
    4. 对引用的写入和读取始终是原子的,无论它们是作为 32 位还是 64 位值实现的。
    5. long一些实现可能会发现将 64 位或值的单个写入操作double分成对相邻 32 位值的两个写入操作很方便。为了效率起见,这种行为是特定于实现的;Java 虚拟机的实现可以自由地以原子方式或分两部分 执行写入long和值。double
    6. 鼓励 Java 虚拟机的实现尽可能避免拆分 64 位值。鼓励程序员将共享的 64 位值声明为volatile或正确同步他们的程序以避免可能的并发症

    如何保证原子性

  • 通过 synchronized 关键字保证原子性。

  • 通过 Lock保证原子性。
  • 通过 CAS保证原子性。

Java内存模型JMM

定义

Java Memory Model
JMM规范了:JVM与计算机内存如何协调工作
规定:一个线程如何,何时看到修改后的共享变量,以及如何同步访问的共享变量

JMM与硬件的架构关系

image.png

内存交互的操作流程

image.png
动作解释

  1. lock:主内存变量,标识为一个线程独占
  2. unlock:作用于主内存变量,把处于锁定的变量释放出来,
  3. read:作用于主内存变量:把变量值从内存传输到工作线程的内存中
  4. load:作用于工作内存变量,把工作线程内存的中值,放在工作内存的副本中
  5. use:作用于工作内心变量,把工作内存变量传给执行引擎,
  6. assign:工作内心变量,把执行引擎接收的值付给内存工作变量
  7. store:把工作内存的变量传到主内存
  8. write:把store操作从总过内存的变量,传到主内存的变量中

java 的其他规定

  1. 变量从主内存到工作内存,需按顺序执行read,load操作;从工作内存到主内存,需顺序执行store,write
    但不保证不必须是连续的执行的,就是不保证不被打断
  2. 不允许read 和 load、store、write之一单独出现
  3. 不允许线程没有发生assign操作吧数据从工作线程到主内存
  4. 新变量只能在主内存诞生,不允许工作内存中直接使用一个未被初始化的变量,use和store前,必须assign和load
  5. 变量同一时刻只允许一条线程lock,但是可以多次lock,lock与unlock是成对的
  6. lock时,会清空内存中此变量的值,在执行引擎使用这个变量是需要重新load
  7. 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量
  8. 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作

JMM的内存可见性保证

按程序类型,Java程序的内存可见性保证可以分为下列3类:

  1. 单线程程序
  2. 正确同步多线程程序
  3. 未同步/位正确同步的多线程程序

单例解释

  1. **
  2. * @author Fox
  3. * hsdis-amd64.dll
  4. * 查看汇编指令
  5. * -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp
  6. * DCL为什么要使用volatile
  7. */
  8. public class SingletonFactory {
  9. //这里要加volatile修饰
  10. private static SingletonFactory myInstance;
  11. public static SingletonFactory getMyInstance() {
  12. if (myInstance == null) {
  13. synchronized (SingletonFactory.class) {
  14. if (myInstance == null) {
  15. // 这个过程不是原子的,需要让对象内存可见
  16. // 1. 开辟一片内存空间
  17. // 3. myInstance指向内存空间的地址
  18. // 2. 对象初始化
  19. myInstance = new SingletonFactory();
  20. }
  21. }
  22. }
  23. return myInstance;
  24. }
  25. public static void main(String[] args) {
  26. SingletonFactory.getMyInstance();
  27. }
  28. }

volatile的内存语义

  1. 可见性:其他线程能看到这个变量的写入
  2. 原子性:对单个比那里的读写是原子的,
    64位的long型和double型变量,只要它是volatile变量,对该变量的读/写就具有原子性。
  3. 有序性:读写操作前后加上各种特定的内存屏障来禁止指令重排序来保障有序性

读-写 的内存语义:
写:JMM会把本地内存中的共享变量刷到住内存
读:会把本地内存的值置位无效,从主内存中读

Lock指令

内存屏障
LoadLoad屏障:(指令Load1; LoadLoad; Load2),在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
LoadStore屏障:(指令Load1; LoadStore; Store2),在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
StoreStore屏障:(指令Store1; StoreStore; Store2),在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
StoreLoad屏障:(指令Store1; StoreLoad; Load2),在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能 这个比较常用

CPU缓存架构剖析

缓存一致性(Cache coherence)

总线窥探(Bus Snooping)

工作原理:数据被多个缓存共享时,处理器修改共享值时,更改必须传播到其他所有该数据的副本的缓存中。
数据的变更同时通过总线窥探完成,
窥探协议类型:
write-invalidate: 只是通知其他线程的缓存无线 MESI协议
write-update: 更新是会直接通过总线都更新

MESI协议

状态:
M: modified 已修改
E: Exclusive 独占
S:shared 共享
I: invalid 无效

避免伪共享方案

volatile 语义可以造成 :
原因:valtail会把两个临近的变量一起加载,但是各自更新是因为volatie会通知另一个线程已经改变数值了,就会更新变量,造成实际耗时增加

  1. class Pointer {
  2. volatile long x;
  3. //避免伪共享: 缓存行填充
  4. volatile long y;
  5. }
  6. public class FalseSharingTest {
  7. public static void main(String[] args) throws InterruptedException {
  8. testPointer(new Pointer());
  9. }
  10. private static void testPointer(Pointer pointer) throws InterruptedException {
  11. long start = System.currentTimeMillis();
  12. Thread t1 = new Thread(() -> {
  13. for (int i = 0; i < 100000000; i++) {
  14. pointer.x++;
  15. }
  16. });
  17. Thread t2 = new Thread(() -> {
  18. for (int i = 0; i < 100000000; i++) {
  19. pointer.y++;
  20. }
  21. });
  22. t1.start();
  23. t2.start();
  24. t1.join();
  25. t2.join();
  26. System.out.println(pointer.x+","+pointer.y);
  27. System.out.println(System.currentTimeMillis() - start);
  28. }
  29. }

image.png
解决方案:

  1. 避免伪共享: @Contended + jvm参数:-XX:-RestrictContended jdk8支持

    总线锁定

    锁总线,相当于变成的串行,成本高

    缓存锁定

    总结:

    可见性:

    怎么做到各个线程对于共享内存的可见性
    刷缓存的实际:
    本地内存什么时候会删除?跟时间片有关
    本地内存没有了才会从主内存中加载

本地内存没有了才会从主内存中加载