java内存模型jmm-爱代码爱编程
面试题
1.你知道什么是Java内存模型吗?
2.JMM与volatile二者之间的关系。
3.JMM有哪些特性orJMM的三大特性是什么?
4.为什么要有JMM,它为什么出现?作用和功能是什么?
5.happens-before现行发生原则你有了解过吗?
计算机硬件存储系统
Java内存模型:JMM(Java Memory Model)本身是一种抽象的概念,并不真实存在。它仅仅描述的是一组约定或规范,通过这组规范定义了程序中(尤其是多线程)各个变量的读写访问方式,并决定一个线程对于共享变量的写入何时以及如何变成对另一个线程可见,关键技术点都是围绕多线程的原子性、可见性和有序性展开。
原则:JMM键技术点都是围绕多线程的原子性、可见性和有序性展开。
能做什么?
1.通过JMM来实现线程和主内存之间的抽象关系。
2.屏蔽各个操作系统和硬件平台的内存访问差异性,以实现Java程序在任何平台下都能达到一致的内存访问效果。
JMM三大特性:
1.可见性
系统主内存共享变量数据修改被写入的时机是不确定的,多线程并发下很可能出现脏读,所以每个线程都有自己的工作内存,线程的工作内存中保存了线程需要使用的变量在主内存中的数据副本,线程对数据的所有操作(读、写)都需要在线程自己的工作内存中进行,不能直接读写主内存中的变量。不同线程之间也无法访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
线程脏读:
2.原子性:指一个操作是不可打断的,在多线程环境下,操作不能被其他线程干扰。
3.有序性:对于一个线程的代码而言,我们习惯性认为代码从上到下,有序执行。但是实际上,为了性能,编译器和处理器往往会对指令序列进行重新排序。Java规范规定JVM线程内部维持顺序化语义,即只要程序的最终结果和它的顺序话执行结果相等,那么指令的执行顺序可以与代码顺序不同,此过程叫做指令重排序。
优缺点:
显然不行,x值未定义,不能直接用。
volatile详解
volatile两大特性是可见性和有序性(禁止指令重排),为什么volatile可以保证可见性和有序性?
volatile靠内存屏障实现可见性和有序性,那什么是内存屏障?
内存屏障
内存屏障是什么?内存屏障(也成内存栅栏,屏障指令)是一类同步屏障指令,是CPU或编译器在对内存的随机访问的操作中的一个同步点,此点之前的所有读写指令都执行完后才能执行此点之后的操作,避免指令重排序。内存屏障其实就是一种JVM指令,Java内存模型的重排规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令,通过这些内存屏障指令,volatile实现了Java内存模型中的可见性和有序性(禁止指令重排),但是volatile无法保证原子性。
内存屏障之前的所有写操作都要将数据会写到主内存
内存屏障之后的所有读操作都可以读取到内存屏障之前的所有写操作的最新结果(实现了可见性)
写屏障(Store Memory Barrier):告诉处理器在写屏障之前将所有写操作的最新结果会写到主内存中。也就是说看到Store屏障指令,就需要把该指令之前的所有写操作指令全部执行完毕才能玩下执行。
读屏障(Load Memory Barrier):处理器在读屏障之后的指令,都需要在读屏障之后执行。也就是说在Load屏障指令之后,能保证后面的读指令读取的数据就一定是最新的。
因此在重排序的时候,不允许将内存屏障之后的指令重排序到内存屏障之前。一句话,对一个volatile变量的写,一定先行发生于后续对于这个volatile变量的读,也叫读后写。
内存屏障的分类
内存屏障粗分为两种:1.读屏障(Load Memory Barrier) 2.写屏障(Store Memory Barrier)
读屏障(Load Memory Barrier)
作用:在读指令之前插入读屏障,让线程的工作内存(CPU高速缓存)中缓存的共享变量数据失效,重新回到主内存中去获取共享变量的最新数据。
写屏障(Store Memory Barrier)
作用:在写指令之后插入写屏障,强制将线程工作内存(CPU高速缓存)中的最新缓存数据回刷到主内存中。
细分四种内存屏障:
屏障类型 | 指令实例 | 说明 |
---|---|---|
LoadLoad | Load1;LoadLoad;Load2 | 保证load1的读取操作在load2及后续读取操作之前执行 |
StoreStore | Store1;StoreStore;Store2 | 在Store2及其后的写操作执行前,保证Store1的写操作已经刷新到主内存 |
LoadStore | Load1;LoadStore;Store2 | 在Store2及其后逇写操作执行前,保证Load1的读操作已经读取完毕 |
StoreLoad | Store1;StoreLoad;Load2 | 保证Store1的写操作值已经更新到主内存之后,Load2的读操作才开始执行 |
第一个操作 | 第二个操作:普通读写 | 第二个操作:volatile读 | 第二个操作:volatile写 |
---|---|---|---|
普通读写 | 可以重排 | 可以重排 | 不可以重排 |
volatile读 | 不可以重排 | 不可以重排 | 不可以重排 |
volatile写 | 可以重排 | 不可以重排 | 不可以重排 |
当第一个操作为volatile读时,无论第二个操作是什么,都不能重排序。这个操作保证了volatile读之后的操作不会重排到volatile读之前。 |
---|
当第二个操作为volatile写时,无论第一个操作是什么,都不能重排序。这个操作保证了volatile写之前的操作不会重排序到volatile写之后 |
当第一个操作为volatile写,第二个操作为volatile读时,不能进行重排序 |
读屏障就是在每个volatile读之后插入一个LoadLoad屏障和一个LoadStore屏障
写屏障就是在每个volatile写操作之前插入一个StoreStore屏障,在每个volatile写操作之后插入一个StoreLoad屏障
volatile可见性验证
代码如下:
package multiThreads;
import java.sql.Time;
import java.util.concurrent.TimeUnit;
public class VolatileTest1 {
static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " come in");
while (flag) {
}
System.out.println(Thread.currentThread().getName() + " get out");
}, "a").start();
TimeUnit.MILLISECONDS.sleep(300L);
flag = false;
System.out.println(Thread.currentThread().getName() + "set flag to false");
}
}
结果如下:
可以看到上述while(flag)循环无法结束,也就是flag被主线程修改为false之后,线程a的工作内存的值并未更新,还是true。即主线程对flag值的修改,对于线程a来说是不可见的。
解决方法,将flag变量用volatile修饰,保证其在多线程之间的可见性。
代码如下:
package multiThreads;
import java.sql.Time;
import java.util.concurrent.TimeUnit;
public class VolatileTest1 {
static volatile boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " come in");
while (flag) {
}
System.out.println(Thread.currentThread().getName() + " get out");
}, "a").start();
TimeUnit.MILLISECONDS.sleep(300L);
flag = false;
System.out.println(Thread.currentThread().getName() + "set flag to false");
}
}
结果如下:
可以看到,此时mian线程将flag值更新为false之后,线程a立刻就获取到了最新的值,结束了while循环。
volatile变量的读写过程
Java内存模型中定义了8种每个线程自己的工作内存与主内存之间的原子操作:
read(读取)->load(加载)->use(使用)->assign(赋值)->store(存储)->write(写入)->lock(锁定)->unlock(解锁)
volatile不具备原子性案例详解
代码示例:
package multiThreads;
import java.util.concurrent.TimeUnit;
public class IPlus {
private int num;
public synchronized void selfPlus() {
num++;
}
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
public static void main(String[] args) throws InterruptedException {
IPlus iPlus = new IPlus();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
iPlus.selfPlus();
}
}).start();
}
TimeUnit.MILLISECONDS.sleep(3000L);
System.out.println(iPlus.getNum());
}
}
运行结果如下:
使用synchronized修饰plus()方法,最终累加结果是正确的。
如果使用volatile修饰num,去除synchroniezd,可以保证正确吗?
代码如下:
package multiThreads;
import java.util.concurrent.TimeUnit;
public class IPlus {
volatile int num;
public void selfPlus() {
num++;
}
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
public static void main(String[] args) throws InterruptedException {
IPlus iPlus = new IPlus();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
iPlus.selfPlus();
}
}).start();
}
TimeUnit.MILLISECONDS.sleep(3000L);
System.out.println(iPlus.getNum());
}
}
运行结果:
从结果来看,volatile显然是不能保证原子性的。
volatile自增失效解析:假设此时主内存中有个变量num的最新值是5,线程a和线程b都是对num进行+1操作。线程a和线程b同时读取num=5这个值保存到自己的工作内存中,线程a先执行完+1操作,然后将num的最新值回写到主内存中,此时线程b虽然还未操作完,但是由于volatile修饰的变量具有可见性,线程b立刻就知道num的值发生了改变,那么线程b的此次+1操作则会失效,num的最终值是6.
i++操作不具有原子性:
结论:volatile不适合参与到依赖当前值的运算。
volatile禁止指令重排案例详解
volatile的适用场景
1.单一赋值可以,但是包含复合运算赋值不可以(++i之类)
volatile int a = 10;//可以
volatile boolean flag = false;//可以
2.状态标识标志业务是否结束(volatile boolean flag = false)
3.开销较低的读,写锁策略
示例代码:
package multiThreads;
public class UseVolatileTest {
public static void main(String[] args) {
}
}
class Counter {
//读多写少的场景,使用volatile+内部锁的方式来优化程序性能
//复合写操作,需要使用synchronized来保证写操作的原子性。
//利用volatile来保证读操作的可见性
private volatile int num;
public int getNum() {
return num;
}
public synchronized int getValue() {
return ++num;
}
}
4.DCL双端锁的发布
问题代码示例:
package multiThreads;
public class SafeDoubleCheckSingleton {
private static SafeDoubleCheckSingleton safeDoubleCheckSingleton;
private SafeDoubleCheckSingleton() {
}
public static SafeDoubleCheckSingleton getInstance() {
if (null == safeDoubleCheckSingleton) {
synchronized (SafeDoubleCheckSingleton.class) {
if (null == safeDoubleCheckSingleton) {
//多线程情况下,由于指令重排,该对象可能还未初始化就被其他线程获取
safeDoubleCheckSingleton = new SafeDoubleCheckSingleton();
}
}
}
return safeDoubleCheckSingleton;
}
}
解决方案,给单例变量加上volatile
代码如下:
package multiThreads;
public class SafeDoubleCheckSingleton {
private volatile static SafeDoubleCheckSingleton safeDoubleCheckSingleton;
private SafeDoubleCheckSingleton() {
}
public static SafeDoubleCheckSingleton getInstance() {
if (null == safeDoubleCheckSingleton) {
synchronized (SafeDoubleCheckSingleton.class) {
if (null == safeDoubleCheckSingleton) {
//多线程情况下,由于指令重排,该对象可能还未初始化就被其他线程获取
safeDoubleCheckSingleton = new SafeDoubleCheckSingleton();
}
}
}
return safeDoubleCheckSingleton;
}
}
小结: