JMM
JMM(Java内存模型-Java Memory Model)本身是一种抽象的概念,并不真实存在。它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问状态。CPU的运行并不是直接操作内存而是先把内存里边的数据读到缓存,而内存的读和写操作的时候就会造成不一致的问题。Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,简称JMM) 来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。
JMM要解决的问题
- 通过JMM来实现线程和主内存之间的抽象关系。
- 屏蔽各个硬件平台和操作系统的内存访问差异以实现让Java程序在各种平台下都能达到一致的内存访问效果。
问题:如果线程A对共享变量X进行了修改,但是线程A没有及时把更新后的值刷新到主存中,而此时线程B从主内存中读取共享变量的值。所以X的值是原始值,那么我们就说对于线程B来讲,共享变量X的更改对线程B是不可见的。
解决:
- Synchronized,JMM关于同步的规定
- 线程解锁前,必须把共享变量的值刷新回主内存
- 线程加锁前,必须读取主内存的最新值到自己的工作内存中
- 加锁解锁是同一把锁
volatile关键字
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存,工作内存是每个线程的私有数据区域。而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作必须在工作内存中进行。所以 首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量。各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信必须通过主内存来完成。当多个线程进行操作共享数据时,可以保证内存中的数据可见。本质是将缓存中的数据时时刻刻刷新到主存中,也可以理解为被volatile修饰的属性都在主存中操作。效率较低,但是比同步锁效率要高
JMM之可见性
各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存进行操作后再写回到主内存中的。 这就可能存在一个线程AAA修改了共享变量X的值,但是未写回到主内存时,另一个线程B又对主内存中同一个共享变量X进行操作,但此时A线程工作内存中共享变量X对线程B来说并不可见,这种工作内存中主内存同步延迟现象就造成了可见性问题。
Java中普通的共享变量不保证可见性,因为数据修改被写入内存的时机是不确定的,多线程并发下很可能出现”脏读”,所以每个线程都有自己的工作内存,线程自己的工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取,赋值等)都必需在线程自己的工作内存中进行,而不能够直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。如果没有可见性保证会导致线程脏读。
JMM之原子性
// number++在多线程下是非线程安全的,因为++操作实际是执行了3个指令,getField、iadd、putField
class MyData {
volatile int number = 0;
public void addTo60() {
this.number = 60;
}
public void addPlusPlus() {
this.number++;
}
AtomicInteger atomicInteger = new AtomicInteger();
public void addAtomic() {
atomicInteger.getAndIncrement();
}
}
public class VolatileDemo {
public static void main(String[] args) {
// 验证volatile可见性
// seeByVolatile();
// 验证volatile不保证原子性,如果要保证原子性,使用Atomic原子类
atomic();
}
public static void atomic() {
MyData myData = new MyData();
for (int i = 1; i <= 20; i++) {
new Thread(() -> {
for (int j = 1; j <= 1000; j++) {
myData.addPlusPlus();
myData.addAtomic();
}
}).start();
}
//等待上面20个线程全部计算结束
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + "int finally number is " + myData.number);
System.out.println(Thread.currentThread().getName() + "AtomicInteger finally number is " + myData.atomicInteger);
}
public static void seeByVolatile() {
MyData myData = new MyData();
//第一个线程
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " come in");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
myData.addTo60();
System.out.println(Thread.currentThread().getName() + " update number to " + myData.number);
}).start();
//第二个线程 main
while (myData.number == 0) {
}
System.out.println(Thread.currentThread().getName() + "mission is over");
}
}
多线程环境下,”数据计算”和”数据赋值”操作可能多次出现,即操作非原子 。若数据在加载之后,若主内存count变量发生修改之后,由于线程工作内存中的值在此前已经加载,从而不会对变更操作做出相应变化,即私有内存和公共内存中变量不同步,进而导致数据不一致。对于volatile变量,JVM只是保证从主内存加载到线程工作内存的值是最新的,也就是数据加载时是最新的。由此可见volatile解决的是变量读时的可见性问题,但无法保证原子性,对于多线程修改共享变量的场景必须使用加锁同步。
- 要use(使用)一个变量的时候必需load(载入),要load(载入)的时候必需从主内存read(读取)这样就解决了读的可见性。
- 写操作是把assign和store做了关联(在assign(赋值)后必需store(存储))。store(存储)后write(写入)。也就是做到了给一个变量赋值的时候一串关联指令直接把变量值写到主内存。
JMM之有序性(禁止指令重排序)
对于一个线程的执行代码而言,我们总是习惯性认为代码的执行总是从上到下,有序执行。 但为了提供性能,编译器和处理器通常会对指令序列进行重新排序。指令重排可以保证串行语义一致,但没有义务保证多线程间的语义也一致,即可能产生"脏读",简单说,两行以上不相干的代码在执行的时候有可能先执行的不是第一条,执行顺序会被优化。单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。处理器在进行重排序时必须要考虑指令之间的数据依赖性。多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。<br /><br />volatile实现了进制指令重排序优化,从而避免多线程环境下程序出现乱序执行的现象。内存屏障又称内存栅栏,是一个CPU指令,它的作用有两个:
- 保证特定操作的顺序执行
保证某些变量的内存可见性
由于编译器和处理器都能执行指令重排序优化。如果在指令间插入一条Memory Barrier,则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障的另一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本<br />
屏障类型 | 指令示例 | 说明 |
---|---|---|
LoadLoad | Load1;LoadLoad;Load2 | 保证load1的读取操作在load2及后续读取操作之前执行 |
LoadStore | Load1;LoadStore;Store2 | 在store2及其后的写操作执行前,保证load1的读操作已经读取结束 |
StoreLoad | Store1;StoreLoad;Load2 | 保证store1的写操作已经刷新到主内存之后,load2及其后的读操作才能执行 |
StoreStore | Store1;StoreStore;Store2 | 在store2及其后的写操作执行前,保证store1的写操作已经刷新到主内存 |
内存屏障(也称内存栅栏,内存栅障,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作),避免代码重排序。内存屏障其实就是一种JVM指令,Java内存模型的重排规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令,通过这些内存屏障指令,volatile实现了Java内存模型中的可见性和有序性,但volatile无法保证原子性。内存屏障之前的所有写操作都要回写到主内存,内存屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果(实现了可见性)。因此重排序时,不允许把内存屏障之后的指令重排序到内存屏障之前。对一个 volatile修饰的字段的写操作, happens-before 于任意后续对这个 volatile 字段的读操作,也叫写后读。
线程和主内存
JMM定义了线程和主内存之间的抽象关系
- 线程之间的共享变量存储在主内存中(从硬件角度来说就是内存条)
- 每个线程都有一个私有的本地工作内存,本地工作内存中存储了该线程用来读/写共享变量的副本(从硬件角度来说就是CPU的缓存,比如寄存器、L1、L2、L3缓存等)
happens-before
在JMM中,如果一个操作执行的结果需要对另一个操作可见性或者代码重排序,那么这两个操作之间必须存在happens-before关系(先行发生原则)
- 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。
happens-before之8条
次序规则:一个线程内,按照代码顺序,写在前面的操作先行发生于写在后面的操作
- 锁定规则:一个unLock操作先行发生于后面((这里的“后面”是指时间上的先后))对同一个锁的lock操作
- volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作,前面的写对后面的读是可见的,这里的”后面”同样是指时间上的先后。
- 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
- 线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作
- 线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。可以通过Thread.interrupted()检测到是否发生中断
- 线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread::join()方法是否结束、Thread::isAlive()的返回值等手段检测线程是否已经终止执行。
- 对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始
Happens-before之volatile变量规则
第一个操作 | 第二个操作:普通写 | 第二个操作:volatile读 | 第二个操作:volatile写 |
---|---|---|---|
普通读写 | 可以重排 | 可以重排 | 不可以重排 |
volatile读 | 不可以重排 | 不可以重排 | 不可以重排 |
volatile写 | 可以重排 | 不可以重排 | 不可以重排 |
- 当第一个操作为volatile读时,不论第二个操作是什么,都不能重排序。这个操作保证了volatile读之后的操作不会被重排到volatile读之前。
- 当第二个操作为volatile写时,不论第一个操作是什么,都不能重排序。这个操作保证了volatile写之前的操作不会被重排到volatile写之后。
- 当第一个操作为volatile写时,第二个操作为volatile读时,不能重排。
多线程对变量的读写过程
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在 主内存。主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到的线程自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:
- 我们定义的所有共享变量都储存在物理主内存中
- 每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝)
- 线程对共享变量所有的操作都必须先在线程自己的工作内存中进行后写回主内存,不能直接从主内存中读写(不能越级)
- 不同线程之间也无法直接访问其他线程的工作内存中的变量,线程间变量值的传递需要通过主内存来进行(同级不能相互访问)
8种原子操作
Java内存模型中定义的8种工作内存与主内存之间的原子read(读取) -> load(加载) -> use(使用) -> assign(赋值) -> store(存储) -> write(写入) -> lock(锁定) -> unlock(解锁)
- read: 作用于主内存,将变量的值从主内存传输到工作内存,主内存到工作内存
- load: 作用于工作内存,将read从主内存传输的变量值放入工作内存变量副本中,即数据加载
- use: 作用于工作内存,将工作内存变量副本的值传递给执行引擎,每当JVM遇到需要该变量的字节码指令时会执行该操作
- assign: 作用于工作内存,将从执行引擎接收到的值赋值给工作内存变量,每当JVM遇到一个给变量赋值字节码指令时会执行该操作
- store: 作用于工作内存,将赋值完毕的工作变量的值写回给主内存
- write: 作用于主内存,将store传输过来的变量值赋值给主内存中的变量
由于上述只能保证单条指令的原子性,针对多条指令的组合性原子保证,没有大面积加锁,所以,JVM提供了另外两个原子指令:
- lock: 作用于主内存,将一个变量标记为一个线程独占的状态,只是写时候加锁,就只是锁了写变量的过程。
- unlock: 作用于主内存,把一个处于锁定状态的变量释放,然后才能被其他线程占用
lock和unlock只保证了主内存和共享内存在数据同步时的原子性,但是在进行数据计算时可能存在多个步骤,这个过程实际上还是在执行引擎中完成的。所以数据计算的过程依然是非线程安全,volatile依然无法保证原子性
单例模式Volatile的应用
// DLC双端检查加锁
public class SingletonDemo {
private static volatile SingletonDemo instance = null; // volatile保证禁止指令重排序
private SingletonDemo() {
System.out.println(Thread.currentThread().getName() + "构造方法");
}
public static SingletonDemo getInstance() {
if (instance == null) {
synchronized (SingletonDemo.class) {
if (instance == null) {
instance = new SingletonDemo();
}
}
}
return instance;
}
}
public class SingletonDemo{
private SingletonDemo() { }
private static class {
private static SingletonDemo instance = new SingletonDemo();
}
public static SingletonDemo {
return SingletonDemoHandler.instance;
}
}
上述写法如果不加Volatile在多线程条件下可能由于指令重排序的原因导致出错。当某一个线程执行到第一次检测,读取到instance不为null时,instance的引用对象可能没有完成初始化操作。
instance = new SingletonDemo()可以分为以下3步完成
- memory = allocate(); // 1.分配对象内存空间
- instance(memory); // 2.初始化对象
- instance = memory; // 3.设置instance指向刚分配的内存地址,此时instance != null
步骤2和步骤3不存在数据依赖关系,而且无论重排序前还是重排序后程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。
- memory = allocate(); // 1.分配对象内存空间
- instance(memory); // 3.设置instance指向刚分配的内存地址,此时instance != null,但是对象还没有初始化完成
- instance = memory; // 2.初始化对象
但是指令重排只会保证串行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性。所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题
一个单例模式中volatile关键字引发的思考
CAS
JDK1.5后Java.util.concurrent.atomic包下提供了常用的原子变量
- volatile保证内存可见性
- CAS(Compare And swap) 算法保证数据的原子性,CAS算法是硬件对于并发操作共享数据的支持,包含三个操作数。内存值V/预估值A/更新值B —> 当且仅当V == A时,V = B. 否则将不做任何操作
// CASDemo public class CASDemo { public static void main(String[] args) { AtomicInteger atomicInteger = new AtomicInteger(5); System.out.println(atomicInteger.compareAndSet(5,2019)); System.out.println(atomicInteger.compareAndSet(5,2020)); } }
CAS底层原理
public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); }
- this:当前对象
- valueOffset:内存偏移量(内存地址)
Unsafe类能解决i++多线程下不安全的问题
Unsafe
- Unsafe是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java CAS操作的执行依赖于Unsafe类的方法。Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都是直接调用操作系统底层资源执行相应任务。
- 变量ValueOffset,表示该变量值在内存中的偏移地址,因为Unsafe句式根据内存偏移地址获取数据的 ```java /**
- Atomically decrements by one the current value. *
- @return the previous value */ public final int getAndDecrement() { return unsafe.getAndAddInt(this, valueOffset, -1); } ```
- 变量Value用volatile修饰,保证了多线程之间的内存可见性
CAS真实含义
- CAS的全程为Compare-And-Swap, 它是一条CPU并发原语。它的功能是判断你内存在某个位置的值是否是预期值,如果是则更改为新的值,这个过程是原子的。
CAS并发原语体现在Java语言中就是sum.misc.Unsafe类中的各个方法。调用Unsafe类中的CAS方法,JVM会帮我们实现出 CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。由于CAS是一种系统原语,原语属于操作系统用于范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断。也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致的问题
public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); } 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; }
var1:AtomicInteger对象本身
- var2:该对象值的引用地址
- var4:需要变动的数量
- var5:var1通过var2(地址值)找出的主内存中的真实的值,用该对象当前值与var5比较
- 如果相同,更新var4 + var5并且返回true
- 如果不同,继续取值然后再比较,知道更新完成
假设线程A和线程B两个线程同时执行getAndAddInt操作
- AtomicInteger里面的value原始值为3,即主内存中AtomicInteger的value的3,根据JMM模型,线程A和线程各自持有一份值为3的value的副本分别到各自的工作内存
- 线程A通过getIntVolatile(var1,var2)拿到value值为3,这时线程A被挂起
- 线程B也通过getIntVolatile(var1,var2)方法获取到value值3,此时刚好线程B没有被挂起并执行compareAndSwapInt方法比较内存值也为3,成功修改内存值为4,线程B执行完成
- 这时线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的值3和主内存的值4不一致,说明该值已经被其他线程抢先一步修改过了,那线程A本次修改失败,只能重新读取重新来一遍
线程A重新获取value值,因为变量value被volatile修饰(如果是getAndIncrement()方法,不需要使用value变量,直接使用地址偏移值访问),所以其他线程对它的修改,线程A总是能看到,线程A继续执行compareAndSwapInt进行比较替换,直到成功
public class JUC27 { public static void main(String[] args) { AtomicInteger a = new AtomicInteger(3); new Thread(() -> { System.out.println(Thread.currentThread().getName() + "\t 值为: " + a.getAndIncrement()); },"线程A").start(); new Thread(() -> { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "\t 值为: " + a.getAndIncrement()); },"线程B").start(); } }
CAS缺点
执行getAndAddInt方法执行时,会进行自旋锁操作。如果CAS失败会一直进行尝试。如果CAS长时间不成功,可能会带来CPU很大的开销
- 只能保证一个共享变量的原子操作。当对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性
- ABA问题
ABA问题及解决方案
举例:
小明在提款机,提取了50元,因为提款机问题,有两个线程,同时把余额从100变为50
- 线程1(提款机):获取当前值100,期望更新为50,
- 线程2(提款机):获取当前值100,期望更新为50,
- 线程1成功执行,线程2某种原因block了,这时,某人给小明汇款50
- 线程3(默认):获取当前值50,期望更新为100
- 线程3成功执行,余额变为100,
- 线程2从Block中恢复,获取到的也是100,compare之后,继续更新余额为50
此时可以看到,实际余额应该为100(100-50+50),但是实际上变为了50(100-50+50-50)。这就是ABA问题带来的成功提交。
使用时间戳原子引用解决ABA问题(原子引用 + 时间戳(版本号,类似乐观锁))
/**
当前账户余额为100
线程1 获取锁成功后挂起,当前版本号为:1
线程2 获取锁成功后挂起,当前版本号为:1
线程1 被唤醒
线程1 扣减余额成功,剩余余额为: 50 当前版本号为: 2
main 汇入50元,汇入成功账号余额为: 100 当前版本号为: 3
线程2 被唤醒
线程2 扣减余额失败,剩余余额为: 100 当前版本号为: 1
*/
public class ABADemo2 {
private static AtomicStampedReference<Integer> balance = new AtomicStampedReference<>(100,1);
private static Integer _50 = 50;
public static void main(String[] args) throws InterruptedException {
System.out.println("当前账户余额为" + balance.getReference());
new Thread(() -> {
boolean isSuccess = balance.compareAndSet(balance.getReference(), balance.getReference() - _50, balance.getStamp(), balance.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "\t 扣减余额"+ (isSuccess ? "成功" : "失败") +",剩余余额为: " + balance.getReference() + "\t当前版本号为: " + balance.getStamp());
},"线程1").start();
new Thread(() -> {
int stamp = balance.getStamp();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean isSuccess = balance.compareAndSet(balance.getReference(), balance.getReference() - _50, stamp, stamp + 1);
System.out.println(Thread.currentThread().getName() + "\t 扣减余额"+ (isSuccess ? "成功" : "失败") + ",剩余余额为: " + balance.getReference() + "\t当前版本号为: " + stamp);
},"线程2").start();
TimeUnit.SECONDS.sleep(1);
boolean isSuccess = balance.compareAndSet(balance.getReference(), balance.getReference() + _50, balance.getStamp(), balance.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "\t 汇入50元,汇入" + (isSuccess ? "成功" : "失败") + "账号余额为: " + balance.getReference() + "\t当前版本号为: " + balance.getStamp());
}
}
// ===================> 打印
当前账户余额为100
线程1 扣减余额成功,剩余余额为: 50 当前版本号为: 2
main 汇入50元,汇入成功账号余额为: 100 当前版本号为: 3
线程2 扣减余额失败,剩余余额为: 100 当前版本号为: 2