Java内存模型,JMM(Java Memory Model)。
概念:Java内存模型定义了final、volatile和synchronized关键字的行为并确保正确同步的Java程序能够正确运行在不同架构的处理器上。
作用:主要解决三个方面的问题
-
原子性问题
-
可见性问题
-
有序性问题
分析:
-
原子性:保证指令不会受到线程上下文切换的影响。
理解:
【原子】:字面意思是不可分割的意思,如果一个线程访问共享变量的操作,在其它线程看来是不可分割的,那么该操作就是原子操作,这个操作就具有原子性。
【不可分割】:当一个线程访问共享变量的操作,在其他线程看来,这个操作要么执行结束,要么没有发生,其他线程无法看到该线程访问时的中间过程,讲到这里,是否让你联想到了MySQL数据库事务的原子性。另外,访问同一组共享变量的原子操作是不能够被交错的。
注意:
-
原子操作是针对访问共享变量的操作而言的。对于局部变量的访问,无所谓其是否是原子的。
-
原子操作是从该操作线程以外的线程来描述的,所以,在多线程环境下才会有意义。
实现原理:
-
锁:锁具有排他性,它能保证一个共享变量在任意时刻只能被一个线程访问(竞态消除)。
-
CAS(Compare -and-Swap):它实现原子性的方式和锁实现原子性的方式实质上是一样的,差别在于,锁通常是在软件层面实现,而CAS直接在硬件(处理器和内存)层面实现,也就是俗称的”硬件锁“。
常见的原子操作:
基础类型中,除了long和double意外,其它基础类型变量的写操作都是原子操作。long和double类型变量的写操作,需要用到关键字volatile实现写的原子性。
2、可见性
概念:可见性就是指一个线程对共享变量的更新结果,对于读取相应共享变量的线程是否可见。
实质:保证指令不受cpu缓存的影响。
示例展示:
-
@Slf4j
public class TestOne {
//创建一个标记
static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
//当标记为假 线程结束
while(run){
}
},"t");
t.start();
sleep(1000);
//更改标记 退出循环
run = false;
log.debug("标记已更改");
}
}
运行结果展示:
虽然标记已经被更改了,但是线程还是一直在循环,没有结束。
接下来分析一下程序执行的过程:
分析之前大家应该要知道,Java内存模型把内存分为主存和工作内存:
-
主存:存储共享信息的内存。
-
工作内存:每个线程存储私有信息的内存。
-
初始状态:主内存存储共享变量run,t线程从主内存中读取run变量的值到工作内存。
2. 示例代码的while循环中,t线程会不断的从主线程读取run的值,JIT编译器会将run变量的值缓存到自己工作内存的告诉缓存中,减少对主内存的访问,提升效率。
3. 主线程sleep一秒之后,修改了run的值,并同步到主内存,但是t线程还是从自己的工作内存里面读取run的值,此时读取到的值没有得到更新,所以线程不会结束。
解决方式:
-
使用volatile关键字,使得t线程读取run变量时,就不会去自己的工作内存中读取,而是去主存中读取,run变量得到了即使的更新。
@Slf4j
public class TestOne {
//创建一个标记,加上volatile关键字
static volatile boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
//当标记为假 线程结束
while(run){
}
},"t");
t.start();
sleep(1000);
//更改标记 退出循环
run = false;
log.debug("标记已更改");
}
}
2. 使用synchronized关键字,下面这段代码,如果去掉synchronized,那么线程将不会停止,而加上锁,就会实现volatile关键字一样的效果。
@Slf4j
public class TestOne {
//创建一个标记,去掉了volatile
static boolean run = true;
//创建一个对象
final static Object object = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while(true) {
synchronized (object) {
if (!run) {
break;
}
}
}
},"t");
t.start();
sleep(1000);
//更改标记 退出循环
run = false;
log.debug("标记已更改");
}
}
两种解决方式比较:
-
解决可见性问题使用volatile关键字更加轻量级一点,使用synchronized同步代码块代价更高,因为还要创建Monitor。
volatile的特点:
-
volatile只能保证变量的可见性,不能保证原子性。
-
synchronized既能保证代码块的原子性,也能保证代码块内变量的可见性,但是属于重量级操作,性能相对较低。
3、有序性
概念:有时一个处理器上运行的一个线程所执行的内存访问操作,在另外一个处理器上运行的其它线程看来是乱序的。
指令重排序:在源码顺序与程序顺序不一致,或者程序顺序与执行顺序不一致的情况下,就是发生了指令重排序。
解决指令重排序:创建变量时加上volatile关键字,在代码使用的时候,使用到该变量的代码行之前的代码无法进行指令重排序。
理解volatile保证可见性的原理:volatile底层实现原理是内存屏障。
-
对volatile变量的写指令后会加入写屏障。
-
对volatile变量的读指令前会加入读屏障。
理解保证可见性的写/读屏障:
-
写屏障保证在该屏障之前,对共享变量的改动,都同步到主存中,不仅是加了volatile关键字的共享变量,写屏障之前的所有共享变量。
-
读屏障保证在该屏障之后,对共享变量的读取,加载的是主内存中的最新数据。
-
理解volatile保证有序性的原理:
理解保证可见性的写/读屏障:
-
写屏障会保证指令重排序时,不会将写屏障之前的代码排在写屏障之后。
-
读屏障会保证指令重排序时,不会将读屏障之后的代码排到读屏障之前。
注意:只能而解决本线程的指令重排,解决不了线程之间的指令重排。