缓存
为了提升性能,JVM 做了 2 件事情。
缓存+重排序
为什么会出现线程可见性问题
要想解释为什么会出现线程可见性问题,需要从计算机处理器结构谈起。
我们都知道计算机运算任务需要CPU和内存相互配合共同完成,其中CPU负责逻辑计算,内存负责数据存储。
CPU要与内存进行交互,如读取运算数据、存储运算结果等。
由于内存和CPU的计算速度有几个数量级的差距,为了提高CPU的利用率,现代处理器结构都加入了一层读写速度尽可能接近CPU运算速度的高速缓存来作为内存与CPU之间的缓冲:
将运算需要使用的数据复制到缓存中,让CPU运算可以快速进行,计算结束后再将计算结果从缓存同步到主内存中,这样处理器就无须等待缓慢的内存读写了。
高速缓存的引入解决了CPU和内存之间速度的矛盾,但是在多CPU系统中也带来了新的问题:缓存一致性。
在多CPU系统中,每个CPU都有自己的高速缓存,所有的CPU又共享同一个主内存。
如果多个CPU的运算任务都涉及到主内存中同一个变量时,那同步回主内存时以哪个CPU的缓存数据为准呢?这就需要各个CPU在数据读写时都遵循同一个协议进行操作。
处理器=》高速缓存=》缓存一致性协议=》主内存 处理器=》高速缓存=》缓存一致性协议=》主内存
参考上图,假设有两个线程A、B分别在两个不同的CPU上运行,它们共享同一个变量X。
如果线程A对X进行修改后,并没有将 X 更新后的结果同步到主内存,则变量X的修改对B线程是不可见的。
所以CPU与内存之间的高速缓存就是导致线程可见性问题的一个原因。
另一个原因就是重排序。
重排序
目的
现在的CPU一般采用流水线来执行指令。
一个指令的执行被分成:取指、译码、访存、执行、写回、等若干个阶段。然后,多条指令可以同时存在于流水线中,同时被执行。
指令流水线并不是串行的,并不会因为一个耗时很长的指令在“执行”阶段呆很长时间,而导致后续的指令都卡在“执行”之前的阶段上。
重排序的目的是为了性能。
Example
理想情况下:
过程A:cpu0—写入1—> bank0; 过程B:cpu0—写入2—> bank1;
如果bank0状态为busy, 则A过程需要等待
如果进行重排序,则直接可以先执行B过程。
类别
在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。重排序分三种类型:
编译器优化的重排序
编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
指令级并行的重排序
现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。
如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
内存系统的重排序
由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
执行过程
从java源代码到最终实际执行的指令序列,会分别经历下面三种重排序:
{源代码 -> 编译器优化重排序(1) -> 指令级并行重排序(2) -> 内存系统重排序(3) -> 最终执行的指令顺序}
上述的1属于编译器重排序,2和3属于处理器重排序。这些重排序都可能会导致多线程程序出现内存可见性问题。
对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。
对于处理器重排序,JMM的处理器重排序规则会要求java编译器在生成指令序列时,插入特定类型的内存屏障(memory barriers,intel称之为memory fence)指令,
通过内存屏障
指令来禁止特定类型的处理器重排序(不是所有的处理器重排序都要禁止)。
JMM 属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。
不进行重排序的场景
数据依赖性
如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。
| 名称 | 示例 | 说明 |
| 写后读 | a = 1; b = a; | 写一个变量后再读这个位置 |
| 写后写 | a = 1; a = 2; | 写一个变量后再写这个变量 |
| 读后写 | a = b; b = 1; | 读一个变量后再写这个变量 |
上面三种情况,只要重排序两个操作的执行顺序,程序的执行结果将会被改变。
所以有数据依赖性的语句不能进行重排序。
as-if-serial
概念
as-if-serial语义就是: 不管怎么重排序(编译器和处理器为了提高并行度), 单线程程序的执行结果不能被改变。所以编译器和cpu进行指令重排序时候回遵守as-if-serial语义。
栗子
public int add() { int x = 1; //1 int y = 1; //2 int ans = x + y; //3 return ans }
上面三条指令, 指令1和指令2没有数据依赖关系, 指令3依赖指令1和指令2。
根据上面我们讲的重排序不会改变我们的数据依赖关系, 依据这个结论, 我们可以确信指令3是不会重排序于指令1和指令2的前面。
我们看一下上面上条指令编译成字节码文件之后
public int add(); Code: 0: iconst_1 // 将int型数值1入操作数栈 1: istore_1 // 将操作数栈顶数值写到局部变量表的第2个变量(因为非静态方法会传入this, this就是第一个变量) 2: iconst_1 // 将int型数值1入操作数栈 3: istore_2 // 将将操作数栈顶数值写到局部变量表的第3个变量 4: iload_1 // 将第2个变量的值入操作数栈 5: iload_2 // 将第三个变量的值入操作数栈 6: iadd // 操作数栈顶元素和栈顶下一个元素做int型add操作, 并将结果压入栈 7: istore_3 // 将栈顶的数值存入第四个变量 8: iload_3 // 将第四个变量入栈 9: ireturn // 返回
作者:叶止水
链接:https://www.jianshu.com/p/ff016da0262d