如何学: 分理解并发的三大特性,JMM工作内存和主内存关系,知道多线程之间如何通信的,掌握volatile能保证可见性和有序性,CAS就可以了
并发和并行
目标都是最大化CPU的使用率
并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。所以无论从微观还是 从宏观来看,二者都是一起执行的。
并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执 行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间 分成若干段,使多个进程快速交替的执行。
并发三大特性
并发编程Bug的源头:可见性、原子性和有序性问题
- 可见性
当一个线程修改了共享变量的值,其他线程能够看到修改的值。Java 内存模型是通过在变量 修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介 的方法来实现可见性的。
如何保证可见性
- 通过 volatile 关键字保证可见性。
- 通过 内存屏障保证可见性。
- 通过 synchronized 关键字保证可见性。
- 通过 Lock保证可见性。
- 通过 final 关键字保证可见性
- 有序性
即程序执行的顺序按照代码的先后顺序执行。JVM 存在指令重排,所以存在有序性问题。
如何保证有序性
- 通过 volatile 关键字保证可见性。
- 通过 内存屏障保证可见性。
- 通过 synchronized关键字保证有序性。
- 通过 Lock保证有序性。
- 原子性
一个或多个操作,要么全部执行且在执行过程中不被任何因素打断,要么全部不执行。在 Java 中,对基本数据类型的变量的读取和赋值操作是原子性操作(64位处理器)。**不采取任 何的原子性保障措施的自增操作并不是原子性的。 **
如何保证原子性
- 通过 synchronized 关键字保证原子性。
- 通过 Lock保证原子性。
- 通过 CAS保证原子性。
JMM :JAVA多线程通信模型——共享 内存模型
内存交互操作
关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、 如何从工作内存同步到主内存之间的实现细节,Java内存模型定义了以下八种操作来完成:
- lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
- unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放 后的变量才可以被其他线程锁定。
- read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存 中,以便随后的load动作使用
- load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放 入工作内存的变量副本中。
- use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引 擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给 工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主 内存中,以便随后的write的操作。
- write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值 传送到主内存的变量中。
volatile的内存语义
volatile的特性
- 可见性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最 后的写入。
- 有序性:对volatile修饰的变量的读写操作前后加上各种特定的内存屏障来禁止指 令重排序来保障有序性。
volatile写-读的内存语义
- 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到 主内存。
- 当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程接下来 将从主内存中读取共享变量
volatile可见性实现原理
JMM内存交互层面实现
volatile修饰的变量的read、load、use操作和assign、store、write必须是连续的,即修改后必须立即同步回主内存,使用时必须从主内存刷新,由此保证volatile变量操作对多线程 的可见性。
硬件层面实现
通过lock前缀指令,会锁定变量缓存行区域并写回主内存,这个操作称为“缓存锁定”, 缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据。**一个处理器的缓存回写到内存会导致其他处理器的缓存无效。 **
总结: JAVA中可见性如何保证
- storeLoad内存屏障 ==> (汇编层面指令)方面使用Lock指令“ lock; addl $0,0(%%rsp) ”
例如volatile,锁机制
当前线程对当前共享变量的操作会存在读不到,或者不能立刻感知另一个线程对共享变量的写操作
- 通过上下文切换,例如Thread.yield();
volatile有序性实现原理
在JVM层面中,在volatile写和volatile读之间,会添加内存屏障storeLoad , 禁止指令重排,从而保证有序性
private volatile static int i;
private static a;
new Thread(() -> {
// volatile 写
i = 2;
// 会添加内存屏障storeLoad , 禁止指令重排,从而保证有序性
// volatile 读
a = i;
},"thread1")
指令重排序
即只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序
as-if-serial
as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),(单线
程)程序的执行结果不能被改变。
volatile重排序规则
1. 第二个操作是volatile写,不管第一个操作是什么都不会重排序
2. 第一个操作是volatile读,不管第二个操作是什么都不会重排序
3. 第一个操作是volatile写,第二个操作是volatile读,也不会发生重排序
JMM内存屏障插入策略
JVM层面的内存屏障
在JSR规范中定义了4种内存屏障:
- LoadLoad屏障:(指令Load1; LoadLoad; Load2),在Load2及后续读取操作要读取的数 据被访问前,保证Load1要读取的数据被读取完毕。
- LoadStore屏障:(指令Load1; LoadStore; Store2),在Store2及后续写入操作被刷出前, 保证Load1要读取的数据被读取完毕。
- StoreStore屏障:(指令Store1; StoreStore; Store2),在Store2及后续写入操作执行前, 保证Store1的写入操作对其它处理器可见。
- StoreLoad屏障:(指令Store1; StoreLoad; Load2),在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的 实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能
由于x86只有store load可能会重排序,所以只有JSR的StoreLoad屏障对应它的mfence或 lock前缀指令,其他屏障对应空操作
CPU高速缓存架构
CPU缓存即高速缓冲存储器,是位于CPU与主内存间的一种容量较小但速度很高的存储器。由于CPU的速度远高于主内存,CPU直接从内存中存取数据要等待一定时间周期,Cache中保存着CPU刚用过或循环使用的一部分数据,当CPU再次使用该部分数据时可从Cache中直接调用,减少CPU的等待时间,提高了系统的效率。
缓存一致性(Cache coherence)
计算机体系结构中,缓存一致性是共享资源数据的一致性,这些数据最终存储在多个本地缓 存中。当系统中的客户机维护公共内存资源的缓存时,可能会出现数据不一致的问题,这在多处 理系统中的cpu中尤其如此。
总线窥探(Bus Snooping)
总线窥探(Bus snooping)是缓存中的一致性控制器(snoopy cache)监视或窥探总线事务的一种方案,其目标是在分布式共享内存系统中维护缓存一致性
工作原理
当特定数据被多个缓存共享时,处理器修改了共享数据的值,更改必须传播到所有其他具有 该数据副本的缓存中。这种更改传播可以防止系统违反缓存一致性。
窥探协议类型
根据管理写操作的本地副本的方式,有两种窥探协议:
Write-invalidate
当处理器写入一个共享缓存块时,其他缓存中的所有共享副本都会通过总线窥探失效。这种 方法确保处理器只能读写一个数据的一个副本。其他缓存中的所有其他副本都无效。这是最常用 的窥探协议。MSI、MESI、MOSI、MOESI和MESIF协议属于该类型。
Write-update
当处理器写入一个共享缓存块时,其他缓存的所有共享副本都会通过总线窥探更新。这个方
法将写数据广播到总线上的所有缓存中。它比write-invalidate协议引起更大的总线流量。这就 是为什么这种方法不常见。Dragon和firefly协议属于此类别。
MESI协议
MESI协议是一个基于写失效的缓存一致性协议,是支持回写(write-back)缓存的最常用
协议。
状态缓存行有4种不同的状态:
- 已修改Modified (M)
缓存行是脏的(dirty),与主存的值不同。如果别的CPU内核要读主存这块数据,该缓存行必 须回写到主存,状态变为共享(S).
- 独占Exclusive (E)
缓存行只在当前缓存中,但是干净的–缓存数据同于主存数据。当别的缓存读取它时,状态变为 共享;当前写数据时,变为已修改状态。
- 共享Shared (S)
缓存行也存在于其它缓存中且是未修改的。缓存行可以在任意时刻抛弃。
- 无效Invalid (I)
缓存行是无效的