目录
前言
- 多线程内存的划分分为:java线程、工作内存、save和load操作、主内存
- 编译器和执行器都会对指令进行重排。处理器通过内存屏障限制指令重排,编译器可以禁止特定类型的编译器进行指令重排。
- 指令重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致
CPU缓存模型
JMM
1、JMM的作用
定义一些规范,处理多线程间数据的通信;解决多线程环境下指令重排序导致的问题。
- 用于屏蔽不同操作系统间的内存差异,线程和主内存之间的关系。(8种原子性语义操作)
- 定义Java 源代码到 CPU 可执行指令的这个转化过程要遵守哪些和并发相关的原则和规范。(8种原子性语义操作)
- 定义了一些并发编程相关的规范来解决一些问题,开发者可以利用这些规范更方便地开发多线程程序。(如:抽象了 happens-before 原则来解决这个指令重排序问题)
2、谈JMM前的说明
- JMM只是一个抽象内存模型。
- JMM和物理机内存模型不是一个范畴。
- JMM和Java运行时数据区没有直接对应关系。
3、JMM示意图
JMM有8种原子内存语义操作
1、8种原子内存语义的作用
用来描述线程本地内存和主动内存的交互协议,java线程之间的通信由JMM控制。
2、8种原子语义有哪些
1、lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
2、unlock(解锁):作用于主内存的变量,它吧一个处于锁定状态的变量释放出来,释放后的变量才能被其它线程锁定。
3、read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load作用。
4、load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入到工作内存的变量副本中。
5、assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量。
6、store(存储):作用于工作内存的变量,把工作内存的变量传到主内存中,以便随后write操作。
7、write(写入):作用于主内存的变量,把store操作从工作内存中的变量放到主内存中。
3、JMM的8中基本操作必须满足的规则
1、不允许read和load、store和write操作之一单独出现
2、发生过assign操作,必须把变量在工作内存改变后必须同步会主内存(同步的时间点不一定)
3、没有发生过assign操作,不允许把数据从工作内存同步回主内存
4、新变量只能从主内存诞生
5、一个变量同一时刻只允许一个线程对其进行lock操作,但可被同一线程多次lock,lock几次就要unlock几次(synchronized同步代码块原理实现方式)
6、一个变量执行lock操作会清空工作内存中该变量的值
7、变量没有执行lock操作则不能执行unlock操作
8、对变量执行unlock前,必须把此变量同步回主内存(synchronized同步块可见性实现原理)
注意:synchronized对线程原子性、可见行、有序性的保障,其实底层是通过8种基本操作实现的
先行先发原则
1、先行先发原则的作用
- 为了程序员和编译器、处理器之间的平衡,只要不改变程序的执行结果,编译器和处理器怎么进行重排序优化都行。
- 会改变程序执行结果的重排序,JMM 要求编译器和处理器必须禁止这种重排序
- 表达的意义其实并不是一个操作发生在另外一个操作的前面,虽然这从程序员的角度上来说也并无大碍。更准确地来说,它更想表达的意义是前一个操作的结果对于后一个操作是可见的,无论这两个操作是否在同一个线程里。阐述操作之间的内存可见性。
- 判断数据是否存在竞争,线程是否安全的依据。
- JMM中的顺序性不能仅仅依靠volatile和synchronized.
- 先行发生原则和时间的先后顺序没什么关系
2、先行先发原则的规则
A操作先行发生于B操作,那A操作的影响能被B操作观察到。(影响包括内存共享变量值的改变、发送消息、调用方法等)
1、程序次序:一个线程内,按照程序代码的顺序,写在前面的操作先行发生于后面的操作。
2、synchronized相关:unlock先于后面的lock
一个unlock操作先行发生于后面对同一个锁的lock操作。(后面只时间的先后)
3、volatile相关:写先于读
对volatile的写操作先行发生于后面对这个变量的读操作。(后面只时间的先后)
线程相关
4、start():Thread的start()方法先行发生于此线程的每一个动作。(start()先于所有动作)
5、join()或isAlive():线程中的所有操作先行发生于对此线程的终止检验(所有动作先于Thread.join方法结束、Thread.isAlive等)
6、interrupt():线程中断先行发生于对中断检验代码(interrupt()先于Thread.interrupted())
7、finalize():初始化方法的完成先于finalize()的开始
8、传递性:A操作先于B操作,B操作先于C操作,那A操作先于C操作。
3、happen before和JMM的关系
线程的三大特性
原子性
1、基本数据类型的访问读写是具备原子性的
2、lock和unlock操作没有开放个用户直接使用,但是提供了更高级别的字节码指令monitorenter和monitorexit,也就是java中的synchronized快
可见性
一个线程修改了某一个共享变量的值,其他线程是否能够立刻知道这个修改。
1、synchronized和final关键字也具有可见性
2、synchronized可见性是因为,对一个变量执行unlock前,必须先把变量同步回主内存中(执行store和write操作)这条获得的。
3、final可见性,被final修饰的字段在构造器中一旦初始化完成,其它线程就能看见该字段的值。
有序性
由于指令重排序问题,代码的执行顺序未必就是编写代码时候的顺序。
synchronized的有序性是通过 一个变量在同一时刻只允许一个线程对其进行lock操作 获得的。
Jmm对线程三大特性的保障
volatile介绍
1、volatile内部原理
volatile变量赋值后,汇编代码会多一个lock操作,相当于内存屏障,作用是使本CPU的缓存写入内存(相当于store和write操作),该写入动作会使其他cpu的缓存无效,所以valatile变量的修改对其它cpu立即可见。
2、volatile的变量新增的内存语义
1、工作内存中,每次使用使用对象前必须先从主内存刷新最新的值。(其它cpu能看到变量最新的值)
2、工作内存中,每次修改变量后必须立刻同步会主内存中。(其它线程可以看到本线程对变量的修改)
3、volatile修饰的变量不会被指令从排序优化。(a肯定在b之前执行)
volatile boolean a;
int i=3;
int j=5;
volatile boolean b;
3、volatile作用
1、只能保证变量对所有线程的可见性,不能保证原子性和有序性。
2、每次使用前,都要先刷新,执行引擎看不到不一致的情况,因此可以认为不存在一致性的问题。
3、使用Volatile变量会禁止指令重排序优化,其实就是内存屏障的作用,(volatile前的代码不会在volatile变量之后执行,volatile变量之后的代码也不会在volatile变量之前执行),保证代码的执行顺序与程序的顺序相同。
4、每次使用变量前必须从主内存刷新最新的值,每次修改变量后必须立刻同步回主内存中。
5、目前商用虚拟机几乎都把64位的数据读写操作作为原子操作来对待。
4、内存屏障
- 内存屏障(memory barrier)是一个CPU指令。该指令确保一些特定操作执行的顺序,或者影响一些数据的可见性。
- 编译器和CPU可以在保证输出结果一样的情况下对指令重排序,使性能得到优化。插入一个内存屏障,相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。
- volatile通过插入内存屏障(Memory Barrier),在内存屏障前后禁止重排序优化,以此实现有序性。内存屏障有两个作用:一是保证特定操作的执行顺序,二是保证某些变量的内存可见性。