JUC上篇-理论
MESI-缓存一致性协议
- 为什么有MESI协议?
现在的处理器都是多核处理器,并且每个核都带有多个缓存(指令缓存和数据缓存,见下图)。为什么需要缓存呢,这是因为CPU访问内存的速度比较慢,所以在CPU和内存之间加了个缓存以提高访问速度。既然每个核都有缓存,那么假设两个核或者多个核同时访问同一个变量时这些缓存是如何进行同步的呢(缓存细分一个个缓存行),这就有了MESI协议
- 什么是MESI缓存一致性协议
MESI中每个缓存行都有四个状态,分别是E(exclusive)、M(modified)、S(shared)、I(invalid)。
M:代表该缓存行中的内容被修改了,并且该缓存行只被缓存在该cpu中。这个状态的缓存行中的数据和内存中的不一样,在未来的某个时刻它会被写入到内存中(当其他CPU要读取该缓存行的内容时。或者其他CPU要修改该缓存对应的内存中的内容时(个人理解CPU要修改该内存时先要读取到缓存中在进行修改),这样的话和读取缓存中的内容其实是一个道理)。
E:代表该缓存行对应内存中的内容只被该CPU缓存,其他CPU没有缓存该缓存对应内存行中的内容。这个状态的缓存行中的内容和内存中的内容一致。该缓存可以在任何其他CPU读取该缓存对应内存中的内容时变成S状态。或者本地处理器写该缓存就会变成M状态。
S:该状态意味着数据不止存在本地CPU缓存中,还存在别的CPU的缓存中。这个状态的数据和内存中的数据是一致的。当有一个CPU修改该缓存行对应的内存的内容时会使该缓存行变成I状态。
I:代表该缓存行中的内容时无效的。
load read 和 local write 分别代表本地CPU读写。remote read 和 remote write 分别代表其他CPU读写。
- MESI缓存一致性解释
在一个典型系统中,可能会有几个缓存(在多核系统中,每个核心都会有自己的缓存)共享主存总线,每个相应的cpu会发出读写请求,而缓存的目的是为了减少
CPU
读写共享主存的次数。
一个缓存除在Invalid
状态外都可以满足cpu的读请求,一个Invalid
的缓存行必须从主存中读取变成S
或者E
状态)来满足该CPU
的读请求。
一个写请求只有在该缓存行是M
或E
状态时才能被执行,如果缓存行处于S
状态,必须先将其它缓存中该缓存行变成Invalid
状态(也即是不允许 不同CPU
同时修改同一缓存行,即使修改该缓存行中不同位置的数据也不允许)。该操作经常作用广播的方式来完成,例如:RequestFor Ownership
(RFO
)
缓存可以随时将一个非M
状态的缓存行作废,或者变成Invalid
状态,而一个M
状态的缓存行必须先被写回主存。
一个处于M
状态的缓存行必须时刻监听所有试图读该缓存行相对就主存的操作,这种操作必须在缓存将该缓存行写回主存并将状态变成S
状态之前被延迟执行。
一个处于S
状态的缓存行也必须监听其他缓存使该缓存行无效或者独享该缓存行的请求,并将该缓存行变成无效(Invalid
)。
一个处于E
状态的缓存行也必须监听其他缓存读主存中该缓存行的操作,一旦有这种操作,该缓存行需要变成S
状态。
对于M
和E
状态而言总是精确的,他们在和该缓存行的真正状态是一致的。而S
状态可能是非一致的,如果一个缓存将处于S
状态的缓存行作废了,而另一个缓存实际上可能已经独享了该缓存行,但是该缓存却不会将该缓存行升迁为E
状态,这是因为其它缓存不会广播他们作废掉该缓存行的通知,同样由于缓存并没有保存该缓存行的copy
的数量,因此(即使有这种通知)也没有办法确定自己是否已经独享了该缓存行。
从上面的意义看来E状态是一种投机性的优化:如果一个CPU
想修改一个处于S
状态的缓存行,总线事务需要将所有该缓存行的copy
变成Invalid
状态,而修改E
状态的缓存不需要使用总线事务。
Java内存模型(JMM)
- Java内存模型(JMM)是什么?
Java虚拟机规范中定义了Java内存模型(Java Memory Model,JMM),用于屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的并发效果,JMM规范了Java虚拟机与计算机内存是如何协同工作的:规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。
- Java内存模型(JMM)定义了什么?
A: 线程之间的共享变量存储在主内存中(Main Memory)中
B: 每个线程都有一个私有的本地内存(Local Memory),本地内存是JMM的一个抽象概念,并不真实存在,它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。本地内存中存储了该线程以读/写共享变量的拷贝副本。
C: 从更低的层次来说,主内存就是硬件的内存,而为了获取更好的运行速度,虚拟机及硬件系统可能会让工作内存优先存储于寄存器和高速缓存中。
D: Java内存模型中的线程的工作内存(working memory)是cpu的寄存器和高速缓存的抽象描述。而JVM的静态内存储模型(JVM内存模型)只是一种对内存的物理划分而已,它只局限在内存,而且只局限在JVM的内存。
- JMM模型下的线程间通信?
线程间通信必须要经过主内存。如下,如果线程A与线程B之间要通信的话,必须要经历下面2个步骤:
1)线程A把本地内存A中更新过的共享变量刷新到主内存中去。
2)线程B到主内存中去读取线程A之前已更新过的共享变量。
关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间的实现细节,Java内存模型定义了以下八种操作来完成:
lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
unlock(解锁): 作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用。
load(载入): 作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
use(使用): 作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。
Java内存模型还规定了在执行上述八种基本操作时,必须满足如下规则:
A: 如果要把一个变量从主内存中复制到工作内存,就需要按顺寻地执行read和load操作, 如果把变量从工作内存中同步回主内存中,就要按顺序地执行store和write操作。但Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。
B: 不允许read和load、store和write操作之一单独出现。
C: 不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。
D: 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。
E: 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
F: 一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现
G: 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值。
H: 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
I: 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。
- Java内存模型解决的问题?
当对象和变量被存放在计算机中各种不同的内存区域中时,就可能会出现一些具体的问题。Java内存模型建立所围绕的问题:在多线程并发过程中,如何处理多线程读同步问题与可见性(多线程缓存与指令重排序)、多线程写同步问题与原子性(多线程竞争race condition)
1.什么是多线程读同步与可见性?
可见性(共享对象可见性):线程对共享变量修改的可见性。当一个线程修改了共享变量的值,其他线程能够立刻得知这个修改。
线程缓存导致的可见性问题:
如果两个或者更多的线程在没有正确的使用volatile声明或者同步的情况下共享一个对象,一个线程更新这个共享对象可能对其他线程来说是不可见的:共享对象被初始化在主存中。跑在CPU上的一个线程将这个共享对象读到CPU缓存中,然后修改了这个对象。只要CPU缓存没有被刷新到主存,对象修改后的版本对跑在其他CPU上的线程都是不可见的。这种方式可能导致每个线程拥有这个共享对象的私有拷贝,每个拷贝停留在不同的CPU缓存中。
下图示意了这种情形。跑在左边CPU的线程拷贝这个共享对象到它的CPU缓存中,然后将count变量值修改为2.这个修改对跑在右边的CPU上其他线程是不可见的,因为修改后的count的值还没有被刷新回主内存中去。
解决这个内存可见性问题你可以使用
a. Java中的volatile关键字:volatile关键字可以保证直接从主存中读取一个变量,如果这个变量被修改后,总是会被写回到主存中去。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的,无论是普通变量还是volatile变量都是如此,普通变量与volatile变量的区别是:volatile的特殊规则保证了新值能立即同步到主内存,以及每个线程在每次使用volatile变量前都立即从主内存刷新。因此我们可以说volatile保证了多线程操作时变量的可见性,而普通变量则不能保证这一点。
b. Java中的synchronized关键字:同步快的可见性是由“如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值”、“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store和write操作)”这两条规则获得的。
c.Java中的final关键字:final关键字的可见性是指,被final修饰的字段在构造器中一旦被初始化完成,并且构造器没有把“this”的引用传递出去(this引用逃逸是一件很危险的事情,其他线程有可能通过这个引用访问到“初始化了一半”的对象),那么在其他线程就能看见final字段的值(无须同步)。
重排序导致的可见性问题:
Java程序中天然的有序性可以总结为一句话:如果在本地线程内观察,所有操作都是有序的(“线程内表现为串行”(Within-Thread As-If-Serial Semantics));如果在一个线程中观察另一个线程,所有操作都是无序的(“指令重排序”现象和“线程工作内存与主内存同步延迟”现象)。
Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性。
volatile关键字本身就包含了禁止指令重排序的语义。
synchronized则是由“一个变量在同一个时刻只允许一条线程对其进行lock操作”这条规则获得的,这个规则决定了持有同一个锁的两个同步块只能串行地进入。
指令序列的重排序:
A: 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
B: 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-LevelParallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
C: 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
每个处理器上的写缓冲区,仅仅对它所在的处理器可见。这会导致处理器执行内存操作的顺序可能会与内存实际的操作执行顺序不一致。由于现代的处理器都会使用写缓冲区,因此现代的处理器都会允许对写-读操作进行重排序:
数据依赖:
编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。(这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑)。
指令重排序对内存可见性的影响:
d
当1和2之间没有数据依赖关系时,1和2之间就可能被重排序(3和4类似)。这样的结果就是:读线程B执行4时,不一定能看到写线程A在执行1时对共享变量的修改。
指令重排序改变多线程程序的执行结果例子:
flag变量是个标记,用来标识变量a是否已被写入。这里假设有两个线程A和B,A首先执行writer()方法,随后B线程接着执行reader()方法。线程B在执行操作4时,能否看到线程A在操作1对共享变量a的写入呢?
答案是:不一定能看到。
由于操作1和操作2没有数据依赖关系,编译器和处理器可以对这两个操作重排序;同样,操作3和操作4没有数据依赖关系,编译器和处理器也可以对这两个操作重排序。
as-if-serial语义:
不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。(编译器、runtime和处理器都必须遵守as-if-serial语义)
happens before:
从JDK 5开始,Java使用新的JSR-133内存模型,JSR-133使用happens-before的概念来阐述操作之间的内存可见性:在JMM中,如果一个操作执行的结果需要对另一个操作可见(两个操作既可以是在一个线程之内,也可以是在不同线程之间),那么这两个操作之间必须要存在happens-before关系:
A:程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
B: 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
C: volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
D: 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
一个happens-before规则对应于一个或多个编译器和处理器重排序规则。
内存屏障禁止特定类型的处理器重排序:
重排序可能会导致多线程程序出现内存可见性问题。对于处理器重排序,JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障(Memory Barriers,Intel称之为Memory Fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序。通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。
为了保证内存可见性,Java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。
StoreLoad Barriers是一个“全能型”的屏障,它同时具有其他3个屏障的效果。现代的多处理器大多支持该屏障(其他类型的屏障不一定被所有处理器支持)。执行该屏障开销会很昂贵,因为当前处理器通常要把写缓冲区中的数据全部刷新到内存中(Buffer Fully Flush)。
2. 什么是多线程写同步与原子性?
多线程竞争(Race Conditions)问题: 当读,写和检查共享变量时出现race conditions。
如果两个或者更多的线程共享一个对象,多个线程在这个共享对象上更新变量,就有可能发生race conditions。
想象一下,如果线程A读一个共享对象的变量count到它的CPU缓存中。再想象一下,线程B也做了同样的事情,但是往一个不同的CPU缓存中。现在线程A将count加1,线程B也做了同样的事情。现在count已经被增加了两次,每个CPU缓存中一次。如果这些增加操作被顺序的执行,变量count应该被增加两次,然后原值+2被写回到主存中去。然而,两次增加都是在没有适当的同步下并发执行的。无论是线程A还是线程B将count修改后的版本写回到主存中取,修改后的值仅会被原值大1,尽管增加了两次:
解决这个问题可以使用Java同步块。一个同步块可以保证在同一时刻仅有一个线程可以进入代码的临界区。同步块还可以保证代码块中所有被访问的变量将会从主存中读入,当线程退出同步代码块时,所有被更新的变量都会被刷新回主存中去,不管这个变量是否被声明为volatile。
使用原子性保证多线程写同步问题:
原子性: 指一个操作是按原子的方式执行的。要么该操作不被执行;要么以原子方式执行,即执行过程中不会被其它线程中断。
实现原子性:
A: 由Java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store、write,我们大致可以认为基本数据类型变量、引用类型变量、声明为volatile的任何类型变量的访问读写是具备原子性的(long和double的非原子性协定:对于64位的数据,如long和double,Java内存模型规范允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为两次32位的操作来进行,即允许虚拟机实现选择可以不保证64位数据类型的load、store、read和write这四个操作的原子性,即如果有多个线程共享一个并未声明为volatile的long或double类型的变量,并且同时对它们进行读取和修改操作,那么某些线程可能会读取到一个既非原值,也不是其他线程修改值的代表了“半个变量”的数值。但由于目前各种平台下的商用虚拟机几乎都选择把64位数据的读写操作作为原子操作来对待,因此在编写代码时一般也不需要将用到的long和double变量专门声明为volatile)。这些类型变量的读、写天然具有原子性,但类似于 “基本变量++” / “volatile++” 这种复合操作并没有原子性。
B: 如果应用场景需要一个更大范围的原子性保证,需要使用同步块技术。Java内存模型提供了lock和unlock操作来满足这种需求。虚拟机提供了字节码指令monitorenter和monitorexist来隐式地使用这两个操作,这两个字节码指令反映到Java代码中就是同步快——synchronized关键字。
JMM缓存不一致问题
- 总线加锁(性能太低)
cpu从主内存读取数据到高速缓存,会在总线对这个数据加锁,这样其他cpu没法去读或写这个数据,直到这个cpu使用完数据释放锁之后其它cpu才能读取该数据
- MESI缓存一致性协议
多个cpu从主内存读取同一个数据到各自的高速缓存,当其中某个cpu修改了缓存里的数据,该数据会马上同步回主内存,其它cpu通过总线嗅探机制可以感知到数据的变化从而将自己缓存里的数据失效。
- Volatile可见性底层实现原理
底层实现主要是通过汇编lock前缀指令,它会锁定这块内存区域的缓存(缓存行锁定)并写回主内存。
IA-32架构软件开发手册对lock指令的解释:
A: 会将当前处理器缓存行的数据立即
写回到系统内存。
B: 这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效(MESI协议)
分析如下代码,熟悉volatile关键字原理:
public class Main {
private static volatile boolean initFlag = false;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
System.out.printf("waiting data...");
while (!initFlag){
}
System.out.printf("===================success===========");
}).start();
Thread.sleep(2000);
new Thread(()->{
prepareData();
}).start();
}
public static void prepareData(){
System.out.printf("preparing data start .....");
initFlag = true;
System.out.printf("preparing data end....");
}
}
synchronized底层原理
synchronized 实现可见性 (额外的知识点)
synchronized能够实现:
1.原子性(同步)
2.可见性
JMM关于synchronized的两条规定:
1.线程解锁前,必须把共享变量的最新值刷新到主内存中
2.线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中
重新读取最新的值(注意:加锁与解锁需要时同一把锁)
注:线程解锁前对共享变量的修改能在下次加锁时对其他线程可见
synchronized底层原理可以从 java源码层级、字节码层级、JVM层级(Hotspot)、OS(操作系统层级)探究。
java源码层级:
比较简单就是定义一个对象通过 Object obj = new Object();
synchronized(obj);
字节码层级:
通过两个指令 monitorenter(加锁指令)、monitorexit(解锁指令)控制
JVM层级:
加锁就是锁定对象,在JVM层级synchronized底层实现是一个锁升级的过程 无锁状态 -> 偏向锁 -> 轻量级锁(自旋锁) -> 重量级锁
JDK较早的版本中是没有锁升级的过程的,是占用OS(操作系统的资源) 互斥量 由用户态 调用OS的内核态 (该过程调用了操作系统的资源非常重量级的锁,效率较低),JDK后期版本(jdk1.6之后)进行优化后才有的锁升级过程。
普通对象在内存中的布局:
普通对象在内存中布局分为四部分:对象头(markword)、类型指针(class pointer)、实例数据(instance data)、填充对齐(padding)。
对象头(markword)中都有些什么?synchronized在JVM层级的原理就是通过对象头内容来实现的。
对象头(markword)是会变化的,如下图所示:
加锁并且源对象未上锁 如下图:
当加上synchronized后会有后续的历程
以上图解释说明:
当使用synchronized时,JVM层面会读到monitorenter后,检查锁标志位(01或者00、10、11)如果是01表示要么是无锁态,要么是偏向锁,在检查是否是偏向锁发现是0确定了是无锁态,就可以上锁了,锁升级了偏向锁,在对象头上设置线程ID,并设置成为偏向锁,当下次再次竞争该资源时,发现当前线程的ID和对象中的线程ID一致,就继续重入进去了,无需上锁,提高了效率。
偏向锁:markword上记录当前线程指针,下次同一个线程加锁的时候,不需要争用,只需要判断线程指针是否同一个,所以,偏向锁,偏向的是第一个线程。hashcode备份在线程栈上。
轻量级锁:有争用锁升级为轻量级锁,每个线程有自己的LockRecord在自己的线程上,用CAS去争用markword的LR的指针,指针指向哪个线程的LR,哪个线程就拥有锁。
重量级锁:自旋超过10次,升级为重量级锁,如果太多线程自旋CPU消耗过大,不如升级为重量级锁,进入等待队列(不消耗CPU),由操作系统分配执行队列中等待的线程。
偏向锁由于有锁撤销的过程revoke,会消耗系统资源,所以,在锁竞争用特别激烈的时候,用偏向锁未必效率高,还不如直接使用轻量级锁。
Synchronized vs Lock(CAS):
在高可用 高耗时的环境下synchronized效率更高
在低可用 低耗时的环境下CAS效率更高
synchronized到重量级之后是等待队列(不消耗CPU)
CAS(等待期间消耗CPU)
一切以实测为主!
CAS介绍
- 什么是CAS?
在计算机科学中,比较和交换(Compare And Swap)是用于实现多线程同步的原子指令。 它将内存位置的内容与给定值进行比较,只有在相同的情况下,将该内存位置的内容修改为新的给定值。 这是作为单个原子操作完成的。 原子性保证新值基于最新信息计算; 如果该值在同一时间被另一个线程更新,则写入将失败。 操作结果必须说明是否进行替换; 这可以通过一个简单的布尔响应(这个变体通常称为比较和设置),或通过返回从内存位置读取的值来完成(摘自维基本科)
- java中CAS的操作
public class AtomicInteger extends Number implements java.io.Serializable {
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
public final int getAndSet(int newValue) {
return unsafe.getAndSetInt(this, valueOffset, newValue);
}
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
public final boolean weakCompareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
public final int getAndDecrement() {
return unsafe.getAndAddInt(this, valueOffset, -1);
}
public final int getAndAdd(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta);
}
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
public final int decrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, -1) - 1;
}
public final int addAndGet(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}
}
从上面可以看出JAVA中的CAS操作都是通过sun包下Unsafe类实现,而Unsafe类中的方法都是native方法,由JVM本地实现,用volatile修饰value字段,保证可见性。
- Unsafe类
Unsafe是CAS和核心类,Java无法直接访问底层操作系统,而是通过本地(native)方法来访问。不过尽管如此,JVM还是开了一个后门,JDK中有一个类Unsafe,它提供了硬件级别的原子操作。AtomicInteger中的valueOffset表示的是变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的原始值的,所以这样就能通过Unsafe来实现CAS操作了。
Unsafe类中的compareAndSwap*方法,方法中先想办法拿到变量Value在内存中的地址,通过Atomic::cmpxchg实现原子性的比较和替换,其中参数x是即将更新的值,参数e是原内存值。至此,最终完成了CAS操作的全过程。
- 以AtomicInteger为例,探究其增加(incrementAndGet())的方法
AtomicInteger i = new AtomicInteger();
i.incrementAndGet();
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
private volatile int value;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
//方法内部
//this代表当前对象
//valueOffset代表内存偏移量,变量ValueOffset,
//便是该变量在内存中的偏移地址 ,因为UnSafe就是根据内存偏移地址获取数据的
//1是常量每次自增1
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
}
//var1代表当前对象this
//var2代表内存偏移值
//var4是常量1
//var5 = this.getIntVolatile(var1, var2);调用类Unsafe类的一个本地方法getIntVolatile获取当前对象这个内存偏移量的值是多少,获取到后赋值给var5
//this.compareAndSwapInt(var1, var2, var5, var5 + var4)接着又调用Unsafe类的compareAndSwapInt本地方法。var代表当前对象,var2代表内存偏移量。方法作用就是当前对象的内存偏移量位置取到的值是否和我们先前取到的var5值相同,如果相同值就变成了var5+var4,也就是值加1。方法成功返回true取否为flase,从而退出循环!返回var5以前的值
//如果当前对象这个内存偏移量的值与我们先前取到的var5不相同,则不加1返回false取否变为true,再次循环,直到比较成功而更新值,返回以前的值!这就是自旋!
//var1 Atomic Integer 对象本身,var2 该对象值的引用地址,var4需要变得数量,var5是用var1,var2找出的主内存中真实的值。
public final class Unsafe {
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
}