目录
Java 内存模型
① JMM(Java Memory Model),定义了主存(共享)、工作内存(私有)抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、CPU 指令优化等
② JMM 体现在以下几个放面(并发安全也如此):
2.1 原子性:保证指令不受到线程上下文切换影响(指令交错)
2.2 可见性:保证指令不受 CPU 缓存影响
2.3 有序性:保证指令不受 CPU 指令并行优化影响(指令重排)
可见性
退不出的循环
static void cannotFinish() throws InterruptedException { new Thread(() -> { while(flag){ } log.debug("收到,结束!"); }, "t").start(); log.debug("t 启动!"); Thread.sleep(1000); log.debug("t 该结束了"); flag = false; }
线程 t 中循环并没有结束
原因分析
① 初始线程 t 从主存读取 flag 到工作内存,为 true 则继续循环
② 由于线程 t 反复从主存读取 flag,JIT 编译器会将 flag 的值缓存至线程 t 工作内存中的高速缓存中,减少对主存的访问,提高效率
③ 1s 后,主线程将 flag 值改为 false,并同步至主存;而线程 t 是从工作内存中的高速缓存中读取 flag 值,读到的永远是旧值
解决方案
synchronized
synchronized 可以保证 flag 可见性,它会使线程 t 只从主存中取 flag 的值(同步代码块中)
new Thread(() -> { while(true){ synchronized (VolatileTest.class){ if(!flag)break; } } log.debug("收到,结束!"); }, "t").start();
System.out.println()
private static boolean flag = true; while(flag){ System.out.println(flag); }
底层实现用到了 synchronized,会保证 flag 从主存中读取
volatile
① volatile 也可以保证 flag 的可见性,线程 t 每次都从主存中取 flag 值(无论在哪里)
② 可见性是对于共享资源的,当一个线程对该资源进行修改,其他线程能马上“看到”
③ 单线程或者局部变量,无需担心可见性问题
private static volatile boolean flag = true; // volatile 修饰 flag
volatile 保证原子性?
volatile 是不保证原子性的
private static int count = 0; public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for(int i = 0; i < 5000; i++){ count++; } }, "t1"); Thread t2 = new Thread(() -> { for(int i = 0; i < 5000; i++){ count--; } }, "t2"); t1.start(); t2.start(); t1.join(); t2.join(); log.debug("count: {}", count); }
① volatile 保证了 count 的可见性;即线程 t1、t2 每次都是从主存取值,保证每次取的都是最新值
② 但是无法避免上下文切换(指令交错)带来的影响
volatile 保证可见性原理
① 被 volatile 修饰的变量,变量⭐进行读操作⭐时,在变量前面有一个“读屏障”(lfence);“读屏障”保证它后面对共享变量的读取操作,都是从主存取
② 被 volatile 修饰的变量,变量⭐进行写操作⭐时,在变量后面有一个“写屏障”(sfence);“写屏障”保证它前面对共享变量的更新操作,都同步到主存
验证写屏障
private static int mark1 = 0; private static int mark2 = 0; private static volatile int mark3 = 0; static void fence() throws InterruptedException { new Thread(() -> { while(mark3 == 0){ } log.debug("mark1 = {}, mark2 = {}, mark3 = {}; 结束!", mark1, mark2, mark3); }, "t").start(); log.debug("开始!"); Thread.sleep(1000); mark1 = 1; mark2 = 2; mark3 = 3; // 验证写屏障 // --------------------------- 写屏障 ----------------------------------------------------- }
可以看到, mark3 之前的 mark2、mark1,更新之后的值都同步到了主存
验证读屏障
对 mark3 的判断,放在 mark1、mark2 之后都无法结束
while(mark1 == 0 || mark2 == 0 || mark3 == 0){ // mark3 放在最后 mark1 mark2 //------------------------------ 读屏障 --------------------------------------------------- mark3 } while(mark1 == 0 || mark3 == 0 || mark2 == 0){ // mark3 放在中间 mark1 //------------------------------ 读屏障 --------------------------------------------------- mark3 mark2 }
对 mark3 的判断,放在 mark1、mark2 之前可以结束
while(mark3 == 0 || mark1 == 0 || mark2 == 0){ //--------------------------------- 读屏障 ------------------------------------------------ mark3 mark1 mark2 }
有序性
想不到的结果
static void get(){ // 线程 t1 if(flag){ log.debug("count : {}", count); }else{ count = 1; log.debug("count : {}", count); } } static void put(){ // 线程 t2 count = 2; // 操作 1 flag = true; // 操作 2 }
线程 t1 执行方法 get,线程 t2 执行方法 put;求最终 count 打印的值?
按平常的分析来:
① 线程 t1 先执行,则此时 flag = false;打印 “count :1”
② 线程 t2 先执行完了操作 1,则此时 flag = false,count = 2;打印“count :1”
③ 线程 t3 先执行完了操作 1 和 2,则此时 flag = true,count = 2;打印“count :2”
然而还有一种打印结果:“count :0”
原因分析
① 因为当 flag = false 时, count = 1
② 所以得 flag = true,并且此时 count 还处于初始化状态
只有一种可能:线程 t2 中,操作 2 比 操作 1 先执行
指令级并行原理
名词
① 时钟周期时间(Clock Cycle Time) :CPU 能识别的最小时间单位,等于主频的到数(4G 主频,时钟周期时间:0.25 ns);生活中的钟表的时钟周期时间:1s
② CPI 指令平均时钟周期数(Cycles Per Instruction)
③ IPC 单位时钟周期运行指令数(Instruction Per Clock Cycle),CPI 的倒数
④ 程序 CPU 执行时间 = 指令数 * CPI * Clock Cycle Time
工厂加工零件
① 一台计器对零件进行五步加工,每个步骤需要 1 分钟,则加工完 1 个零件,需要 5 分钟
② 并不是下图这种流程;零件 2 并不需要等待 零件 1 加工完才能加工
③ 从图 ① 也可以得出下图;不同零件的不同加工步骤,是可以并行执行的(零件1 进行第 5 步加工时,零件 2 在进行第 4 步加工,零件 3 在进行第 3 步加工 ......);最好情况是,一分钟做五件事;现实生活中的流水线作坊,加工过程差不多是这样
支持流水线的处理器
① 现代处理器会设计为一个时钟周期完成一条执行时间最长的 CPU 指令
② 每条指令可以划分为 5 个步骤:
1. instruction fetch(IF) 取指令
2. instruction decode(ID) 指令译码
3. execute(EX) 指令执行
4. memory access(MEM) 内存访问
5. register write back(WB) 数据写回
③ 现代 CPU 支持多级指令流水线,能同时处理如上 5 个步骤的,称为五级指令流水线;单条指令执行时间不会改变,但是变相提高了指令吞吐量
指令重排序优化
① 为了达到不同指令不同阶段并行执行,可能会对指令进行重排序(第 2 条指令,可能被重排到第 1 条指令之前)
② 不改变程序结果的前提下,可以通过组合和重排序来实现指令各阶段指令级并行
③ 分工,分阶段是提升效率的关键
int a = 10; // 1 int b = 20; // 2 // 12,21 都没影响,所以可以重排序 int a = 10; // 1 int b = a + 20; // 2 // 12 可以,21 会报错; // b 需要使用 a,假如 b = a + 20 先执行,a 都还没定义,会报错;所以不能重排序
保证有序性
① 可以使用 volatile 保证有序性(不发生指令重排)
② 被 volatile 修饰的变量,在变量进行读操作时,会在变量前面加一个“读屏障” ;保证它后面的指令,不会重排到它前面;但是不保证,后面的一堆指令不重排
volatile int a = 0; int b = 0; int c = 0; int d = 0; void sfence(){ log.debug("b: {}", b); // 不受读屏障影响 //----------------------------------- 读屏障 ---------------------------------------------- log.debug("a: {}", a); log.debug("c: {}", c); log.debug("d: {}", d); }
③ 被 volatile 修饰的变量,在变量进行读操作时,会在变量后面加一个“写屏障”; 保证它前面的指令,不会重排到它后面;但是不保证,前面的一堆指令不重排
volatile int a = 0; int b = 0; int c = 0; int d = 0; void sfence(){ b = 1; a = 1; //----------------------------------- 写屏障 ---------------------------------------------- c = 1; // 不受写屏障影响 d = 1; // 不受写屏障影响 }