JMM(Java Memory Model) Java内存模型
定义了一组规范,当多线程访问一个主内存中的共享变量时,并不是直接操作主内存中的共享变量,而是将主内存中的变量拷贝到各自线程的工作内存中,在各自的工作内存中进行数据的操作,操作完成之后再写回给主内存。
多线程下的三大特性
- 原子性
指一段操作,是不可中断的,一旦开始,不能被其他线程打断
- 有序性(禁止指令重排)
指在单线程的情况下,同一段代码的执行结果是相同的
- 可见性
JMM对上述三个特性的支持
原子性:
当多个线程同时操作同一数据时,首先需要将主内存的数据拷贝至各自线程的工作空间中,在各自的工作空间中进行数据的改变,改变完成之后将数据写回給主内存,但由于多线程的调度问题,在多线程操作时,存在着同一操作重复写的情况,(例如++操作,一个线程正准备往主内存写回数据时,被挂起,另一个线程写回数据后,此线程由于内存的可见性,本应该获取新的数据再次++,但却未如此,而是直接将数据写回给主内存,导致了数据重复写,丢失数据的情况)。使用atomic各种类可以使volatile保证原子性
有序性:
编译器/优化器会将源代码的执行顺序按照自己认为最优的方式进行重排(前提为重排之后的命令没有数据依赖关系 例如int x=5, int y=6, x= x +5, y= xx,不可能将y=xx排到第一条语句中),但是如此进行指令重排的话,在多线程环境下,操作同一个数据,如果发生指令重排,将会导致多线程运行结果的不确定性( int a,b,x,y=0, 1线程 x=a, b=1 2线程y=b, a=2,不发生指令重排,结果为x=0,y=0, 发生指令重排,可能导致1线程变为b=1,x=a,2线程a=2,y=b,在多线程调度的情况下,发生指令重排会导致运行结果变成x=2,y=1,导致了多线程运行结果的不确定),所以可以使用volatile进行禁止指令重排,导致运行结果的一致性
volatile 实现有序性,是通过添加内存屏障来实现的,什么是内存屏障,参照附录一:https://www.yuque.com/zhanyifan-rkxpe/grf7g5/gi2zb4#pMh4h
可见性:
当一个线程修改了自己工作控件的变量,并写回给主内存,要及时通知给其他线程,这种特性叫做jmm的第一个特性, 内存可见性
单例模式-双端检索机制下的问题
// 但这种方法并不能完全解决多线程的单例模式问题,
// 有几率存在着错误,因为存在着指令重排的问题,
// 会导致在new 实例的时候发生指令重排,在new实例中分为三步,
// 1. 分配空间,2. 调用构造函数,3. 将实例指向刚分配的空间,有可能2,3发生重排,
// 导致instance在实例没初始化结束(未调用构造函数结束)时,另一个线程进入判断,结果不为空。
// 所以最终的方式应该采用双端检测机制+在需要单例模式的变量前加上volatile关键字,
// 禁止指令重排,从而解决问题
if(instance == null){
synchronized (Singleton.class){
if(instance== null){
instance = new Singleton()
}
}
}
CAS(Compare And Swap) 比较并交换
CAS 是什么
CAS的全称是Compare-And-Swap,它是一条CPU并发原语。
它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。
CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中CAS方法,JVM会帮我们实现CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现原子小左。再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干指令组成的,用语完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致性问题。
比较当前工作内存中的值和主内存的值是否相同,如果相同执行操作,否则继续比较直到主内存和工作内存的值一致为止
CAS的实现原理
CAS举例(AtomicInteger)
初始化unsafe 以及 value值的偏移量
获取并自增
- 调用unsafe,将初始化计算出的value属性的偏移量(offset)传入
- 根据传入的偏移量(offset)获取出该内存地址上的变量值
底层调用native方法,直接去比较内存地址上的变量值,是否与传入的期望值相同,如果相同,将新值(var5+var4)设置到该内存地址中,结束循环,设置成功。否则,重新循环2-3步,直到设置成功
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
// 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;
}
// native method
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
详细流程解释
假设有两个线程(threadA,threadB)同时执行getAndIncrement操作
AtomicInteger中的value值,假设为2,即主内存中的AtomicInteger中的value为2,根据JMM内存模型,threadA和threadB各自拷贝了一份变量副本(value = 2)到各自的工作内存中。
- threadA执行getIntVolatile(var1,var2) 拿到value的值为2,假设此时threadA被挂起
- threadB此时也通过getIntVolatile(var1,var2)拿到value的值为2,此时threadB还有时间片,接着执行 compareAndSwapInt()方法,比较内存地址中的值也是2,成功修改内存中的值为3,。threadB执行完毕。
- 此时threadA,执行compareAndSwapInt(),但是此时内存地址中的值是3,和threadA工作内存中的值2不相等,说明这个变量已经被其他线程先一步修改过了,compareAndSwapInt返回false,只能在此循环,重新getIntVolatile获取主内存中新的变量值
threadA重新获取value的值,因为value的值被volatile修改,所以对其他线程是可见的,所以threadA循环4-5,直到成功。
CAS的缺点
采用自旋锁,循环时间长,开销大
- 只能保证一个共享变量的原子性
-
ABA问题描述与解决方案
何为ABA问题
当多线程CAS在进行compareAndSet方法的时候(例如
AtomicInteger
),仅判断了原有值与当前工作空间的值是否相同,那么,假设A线程操作需要10秒中,B线程操作需要2秒中,在A还没将数据写回给主内存的情况下,B线程可以对主内存的数据进行多次操作,即可以这样说,主内存的值为100,A,B各自的工作空间中都为100初始值,B线程第一次将修改为10,第二次修改为20,第三次修改成100,并写回主内存。那么对于A线程来说,在使用compareAndSet(CAS)方法来判断主内存的值和当前工作区的值的话,那么,此次在A线程的修改就能够成功,因为A线程并不知道B线程将值又修改回了主内存的原有值,但并不代表这种过程是没有问题的。也就产生了狸猫换太子的情况问题代码演示
public void testABA() {
AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
new Thread(() -> {
atomicReference.compareAndSet(100,101);
atomicReference.compareAndSet(101,100);
},"threadA").start();
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean b = atomicReference.compareAndSet(100, 2020);
System.out.println("compare result: " + b);
Integer integer = atomicReference.get();
System.out.println("atomic value:" + integer);
}, "threadB").start();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 输出结果
compare result: true
atomic value:2020
ABA问题解决:
在每次修改成功的时候加上Stamp版本号,修改之前比较当前的stamp版本号是否正确,符合则进行修改,否则修改失败,使用
AtomicStampedReference
添加版本号进行控制public void testABASolution() {
AtomicStampedReference<Integer> atomicReference = new AtomicStampedReference<>(100,1);
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\n 第一次的版本号:" + atomicReference.getStamp());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicReference.compareAndSet(100,101, atomicReference.getStamp(), atomicReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "\n 第二次的版本号:" + atomicReference.getStamp());
atomicReference.compareAndSet(101,100, atomicReference.getStamp(), atomicReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "\n 第三次的版本号:" + atomicReference.getStamp());
},"threadA").start();
new Thread(() -> {
int stamp = atomicReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\n 第一次的版本号:" + stamp);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean b = atomicReference.compareAndSet(100, 2020, stamp, atomicReference.getStamp() + 1);
System.out.println("compare result: " + b);
System.out.println("当前最新的版本:" + atomicReference.getStamp());
}, "threadB").start();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 输出结果
threadA
第一次的版本号:1
threadB
第一次的版本号:1
threadA
第二次的版本号:2
threadA
第三次的版本号:3
compare result: false
当前最新的版本:3
可以看出,使用上述的
AtomicStampedReference
上即可解决ABA问题,因为每次的stamp版本号已经变化,需要重新获取新的版本号之后再进行修改。附录
附录一:volatile内存屏障(Memory Barrier)
在Intel的硬件中,提供了如下的几种内存屏障
读屏障
- 写屏障
- 全能屏障(读写屏障)
- Lock 总线加锁
在Java中,有以下的几种内存屏障
屏障类型 | 指令示例 | 说明 |
---|---|---|
LoadLoad | Load1; LoadLoad; Load2 | 保证load1的读取操作在load2及后续读取操作之前执行 |
StoreStore | Store1; StoreStore; Store2 | 在store2及其后的写操作执行前,保证store1的写操作已刷新到主内存 |
LoadStore | Load1; LoadStore; Store2 | 在stroe2及其后的写操作执行前,保证load1的读操作已读取结束 |
StoreLoad | Store1; StoreLoad; Load2 | 保证store1的写操作已刷新到主内存之后,load2及其后的读操作才能执行 |
ps: 内存屏障,除了可以通过volatile来生成之外,还可以通过 Unsafe来手动的添加内存屏障
如下在Unsafe中的几个API,可以手动的添加读屏障,写屏障,以及全能屏障(读写屏障)
而且,通过Unsafe也可以实现synchronized的效果(synchronized底层通过monitor对象来实现互斥的)