0.背景-缓存一致性
依据维基百科的定义:
在一个共享内存多处理器零碎中,每个处理器都有一个独自的缓存,能够有很多共享数据正本:一个在主内存中,一个在每个申请它的处理器的本地缓存中。 当一个数据正本被更改时,其余正本必须反映该更改。 缓存一致性是确保共享操作数(数据)值的更改及时在整个零碎中流传的学科。
上面图1是缓存不统一的示例图,图2是缓存统一的示例图
其实Java的volatile某种意义上也是来解决这种缓存不统一的状况。
更多缓存一致性的常识,能够参看维基百科的词条,也能够看medium上的这篇文章
1.JMM提供的volatile域的语义
1.1 可见性
依据JSR-133 FAQ中的阐明,volatile字段是用于在线程之间传递状态的非凡字段。 每次读取volatile时,都会看到任意一个线程对该volatile的最初一次写入。 实际上,程序员将volatile字段指定为不能承受因为缓存或重排序而导致的“过期”值的字段。 禁止编译器和运行时在寄存器中调配它们。 它们还必须确保在写入后将其从高速缓存(cache)中刷新到主存(memory),以便它们能够立刻对其余线程可见。 同样,在读取volatile字段之前,必须使高速缓存有效,以便能够看到主内存中的值而不是本地处理器高速缓存中的值。
也就是说每次读取volatile都是从主存读取,写入也会刷新到主存,因此保障了不同线程拿到的都是最新值,即保障了共享资源对各个CPU上的线程的可见性,这其实就是保障了缓存一致性。
1.2. 重排序限度
在旧的内存模型下(Java1.5之前),对volatile变量的访问不能互相重排序,但能够与nonvolatile变量拜访一起重排序。 这毁坏了volatile字段作为从一个线程到另一线程发信号告诉状态的一种伎俩。
在新的内存模型下(Java1.5及之后),volatile变量不能互相从新排序依然为true。区别在于,当初对它们四周的失常字段拜访进行重排序不再那么容易了。
写入一个volatile 字段具备与monitor开释雷同的存储成果,而从一个volatile 字段中读取具备与monitor获取雷同的存储成果。
实际上,因为新的内存模型对volatile 字段拜访与其余字段拜访(无论是否为易失性)的从新排序施加了更严格的束缚,因而当线程A写入volatile 字段f时,对线程A可见的任何内容,这些内容在线程B读取f时都可见。
这是一个如何应用易失性字段的简略示例:
class VolatileExample {
int x = 0;
volatile boolean v = false;
public void writer() {
x = 42;
v = true;
}
public void reader() {
if (v == true) {
//uses x - guaranteed to see 42.
}
}
}
假设一个线程在调用writer办法,而另一个在调用reader办法。在writer办法中对v的写操作,会将对x的写操作也更新到主存中,而对v的读操作则从主存中获取该值。
因而,如果reader办法看到v的值为true,那么也保障能够看到在它之前产生的对42的写入。
在旧的内存模型下,状况并非如此。如果v不是volatile,则编译器能够对writer办法中的写入进行重排序,而reader办法对x的读取可能会看到0。对于重排序的示例,能够参见这篇文章。
无效地,volatile的语义已失去实质性加强,简直达到了同步(synchronization)的程度。出于可见性目标,对volatile 字段的每次读取或写入都像 “half” a synchronization (半同步)一样。
重要阐明:请留神,两个线程拜访雷同的volatile变量很重要,以便正确设置 happens-before 关系。在线程A写入volatile字段f时,对线程A的可见的所有,并不一定对读取volatile字段 g之后的线程B可见。
开释和获取必须“匹配”(即在雷同的volatile 字段上执行)以具备正确的语义。
1.3.如果x为volatile域,那么x++ 是原子操作吗?
首先先解释一下什么是原子操作:
An atomic operation is an operation that will always be executed without any other process being able to read or change state that is read or changed during the operation
原子操作是这样一个操作,该操作执行期间读取或扭转的状态不会被任何其余过程读取或扭转。
1.3.1 与预期不符
如果咱们有上面的代码:
package volatileTest;
import juc.CountDownLatch;
/**
* * @Author: cuixin
* * @Date: 2020/8/5 19:25
*/
public class VolatileAdder {
private volatile int x;
public void add(){
//不是原子操作
x++;
}
public int get(){
return x;
}
public static void main(String[] args) throws Exception
{
VolatileAdder instance = new VolatileAdder();
int taskNum = 2;
CountDownLatch countDownLatch = new CountDownLatch(taskNum);
for(int i=0; i<taskNum; i++){
new Thread(new Task(instance, countDownLatch)).start();
}
countDownLatch.await();
System.out.println(instance.get());
}
private static class Task implements Runnable{
private VolatileAdder adder;
private CountDownLatch latch;
Task(VolatileAdder adder, CountDownLatch latch){
this.adder = adder;
this.latch = latch;
}
@Override
public void run() {
for (int i = 0; i < 100000; i++)
{
adder.add();
}
latch.countDown();
}
}
}
(注:这里的应用CountDownLatch只是为了确保,两个线程运行完工作后,主线程才会调用instance.get(),输入x的值。)
咱们运行下面的程序,发现后果并不是料想的200000,要比这个值小一些(如果在你的机器上不是,你能够适当调大run办法中的循环次数)。
1.3.2 jvm指令层面看看x++
上面咱们先从jvm指令层面看看x++是不是原子的。
执行
javac volatileTest/VolatileAdder.java
javap -v volatileTest/VolatileAdder > volatileTest/VolatileAdder.disasm
拿到jvm层面反汇编代码,查看volatileTest/VolatileAdder.disasm
文件,能够发现 add 办法外面的一行 x++,用的四行 jvm 指令实现的。如下图:
对下面标红四条JVM指令阐明一下:
getfield
获取字段x的值并放入操作数栈顶,
iconst_1
将1放入操作数栈栈顶;
iadd
从操作数栈顶取出两个元素相加并将后果放回到栈顶;
putfield
从操作数栈顶拿到下面的相加后果,并赋值给字段x。
因为一个 ++ 操作须要四条 JVM 指令,那么就可能存在上面这种执行序列,此时相当于少做了一次++操作。
线程A | 线程B |
---|---|
getfield | |
getfield | |
iconst_1 | |
iadd | |
putfield | |
iconst_1 | |
iadd | |
putfield |
因为线程A执行 ++x操作期间,混杂着线程B 执行++x操作,所以说这不是原子操作。
那么如何解决呢,如果多线程下须要++操作,无妨应用Atomic相干类代替(预报,前面文章会介绍应用及原理)。
如果你还不释怀,认为下面的jvm对应的机器指令不肯定也有这么多。
1.3.3 从机器指令看x++
首先尝试运行上面的命令,将字节码文件转换老本地机器指令文件。
java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly volatileTest/VolatileAdder> volatileTest/VolatileAdder.native
这时候在我的机器上报了一个Could not load hsdis-amd64.dll; library not loadable; PrintAssembly is disabled
的谬误。
这个依据不同的操作系统和 cpu 下面的报错会有所不同,你能够依照这个地址本人编译来解决下面的问题,也能够本人搜搜看有没有现成的(比方,我用的就是他人弄好的文件),而后放到了JAVA_HOME/bin门路下,再执行就不报错了。
在 VolatileAdder.native
中搜寻 'add'
,能够看到 x++
,也是由四条机器指令实现的,同样的情理再一次阐明了x++不是原子操作。
2.内存屏障 memory barrier
2.1 概念
上面的这几段介绍来自维基百科:
A memory barrier, also known as a membar, memory fence or fence instruction, is a type of barrier instruction that causes a central processing unit (CPU) or compiler to enforce an ordering constraint on memory operations issued before and after the barrier instruction. This typically means that operations issued prior to the barrier are guaranteed to be performed before operations issued after the barrier.
内存屏障,也称为 membar,,memory fence或 fence instruction,是一种屏障指令,它使地方处理单元(CPU)或编译器对于在屏障指令之前和之后收回的存储器操作执行一种排序束缚。
这通常意味着能够保障在屏障之前公布的操作能够在屏障之后公布的操作之前执行。
Memory barriers are necessary because most modern CPUs employ performance optimizations that can result in out-of-order execution. This reordering of memory operations (loads and stores) normally goes unnoticed within a single thread of execution, but can cause unpredictable behaviour in concurrent programs and device drivers unless carefully controlled. The exact nature of an ordering constraint is hardware dependent and defined by the architecture’s memory ordering model. Some architectures provide multiple barriers for enforcing different ordering constraints.
内存屏障是必须的,因为大多数古代CPU都采纳了性能优化,这些性能优化可能会导致乱序执行。
通常在单个执行线程中不会留神到这种内存操作(load和store)的从新排序,然而除非认真管制,否则可能在并发程序和设施驱动程序中引起不可预测的行为。
排序束缚的确切性质取决于硬件,并由体系结构的内存排序模型定义。某些体系结构为执行不同的排序束缚提供了多个内存屏障。
Memory barriers are typically used when implementing low-level machine code that operates on memory shared by multiple devices. Such code includes synchronization primitives and lock-free data structures on multiprocessor systems, and device drivers that communicate with computer hardware.
当实现在多个设施共享的内存上运行的低级机器代码时,通常应用内存屏障。此类代码包含多处理器零碎上的同步原语和无锁数据结构,以及与计算机硬件进行通信的设施驱动程序。
2.2 Intel 64的内存屏障指令及内存排序限度
2.2.1 内存屏障指令
下面次要是说了Java 内存模型提供的 volatile 语义,那么这些语义是如何实现的呢?
其实下面 VolatileAdder.native
文件曾经给出了答案,要害就在lock add
l后面的lock
前缀
通过查看英特尔®64和IA-32架构软件开发人员手册卷2A, 能够找到 lock
的阐明,上面是节选:
使处理器的LOCK#信号在执行随同的指令的过程中被申明(将指令转换为原子指令)。在多处理器环境中,LOCK#信号可确保在断言该信号时,该处理器领有对任何共享内存的独占应用。
也就是下面在 addl
增加前缀 lock
,这会导致该处理器执行addl时领有对任何共享内存的独占应用。
其实x86-64中相似的内存屏障还有很多,比方mfence
,lfence
, cpuid
等。
比方上面是Intel 64中mfence的节选阐明:
Performs a serializing operation on all load-from-memory and store-to-memory instructions that were issued prior the MFENCE instrunction.
This serializing operation guarantees that every load and store instruction that preceds the MFENCE instruction in program order becomes globally visible before any load or store instruction that follows the MFENCE instruction.
对在MFENCE指令之前收回的所有 load-from-memory 和 store-to-memory 执行序列化操作。此序列化操作可确保,依照程序程序在 MFENCE 指令之前的每个 load 和 store 指令,对于 MFENCE 指令之后的任何 load 或store指令都是全局可见的。
2.2.2 内存排序限度
这里有个文件是对于Intel® 64内存排序的阐明,大家也能够看下。
2.3 Java内存模型
下面这只是对于Intel® 64相干的内存屏障指令和内存排序的阐明,每个CPU架构都不同呢?是不是有点失望。。。嗯,还好有大神
上面是Doug Lea整顿的对于不同处理器相干的内存屏障指令和原子指令。
大家肯定要去看看Doug Lea写的这篇“The JSR-133 Cookbook for Compiler Writers”。
看了之后JVM会确保生成的机器指令会在volatile字段四周插入适合的内存屏障指令,从而实现JSR-133定义的volatile语义。 下面给出的示例VolatileExample就会在如下地位插入内存屏障指令StoreStore和LoadLoad。
class VolatileExample {
int x = 0;
volatile boolean v = false;
public void writer() {
x = 42;
//在这之间插入StoreStore屏障, 等价于在v的值true刷到主存之前,先将x的值42刷到主存。
v = true;
}
public void reader() {
//在获取v的值之后插入LoadLoad屏障,等价于先从主存加载v的值,如果v的值为true,再从主存加载x的值。
if (v == true) {
//uses x - guaranteed to see 42.
}
}
}
读完这篇文章能够发现,能够看到不同CPU架构提供不同的内存屏障指令(次要由硬件工程师实现)和内存排序限度;为了对下层暗藏各种CPU架构的不同,Doug Lea基于此又提出了JVM层面的LoadLoad,StoreStore等内存屏障(由JVM实现者实现);而后JVM实现者则提供对立的Java内存模型(Java语言标准 第八版 17章);而后咱们这些一般的Java开发者就在这对立的Java内存模型上写跨平台的利用。
这里是不是有点像搭积木一样,一层层落下来,一层层地形象下来。尽管按理说一般的Java开发者只须要相熟Java内存模型即可编写并发程序,然而为了更好地了解如何应用Java内存模型提供的语义,为了更好地将本人的了解迁徙到其余编程语言,了解这些底层的机制非常有必要。
4.总结
这篇文章首先是举荐的缓存一致性的文章,给大家一个背景。而后次要是对volatile的语义进行了介绍,并设计示例VolatileAdder从JVM指令和机器指令两个层面来阐明volatile域++操作不是原子操作。
上面有针对示例VolatileAdder的机器代码中的lock addl指令进行了阐明,进而引出Intel64内存屏障指令和内存排序限度,而后JVM对不同CPU架构进行封装形象提供了对立的Java内存模型给一般开发者。
是不是没有想到,一个看起来简简单单的volatile,前面居然暗藏了那么多机密。
5.参考
https://en.wikipedia.org/wiki…
https://docs.oracle.com/javas…
http://gee.cs.oswego.edu/dl/j…
https://www.cs.umd.edu/~pugh/…
https://en.wikipedia.org/wiki…
https://docs.oracle.com/javas…
https://wiki.openjdk.java.net…
https://jpbempel.github.io/20…
https://www.infoq.com/article…
https://jpbempel.github.io/20…
发表回复