什么是Java内存模型(JMM)
- 通俗来说,JMM是一套多线程读写共享数据时,对数据的可见性,有序性和原子性的规则
为什么会有Java内存模型
JVM实现不同会造成“翻译”的效果不同,不同CPU平台的机器指令有千差万别,无法保证同一份代码并发下的效果一致。所以需要一套统一的规范来约束JVM的翻译过程,保证并发效果一致性
原子性
- 什么是原子性
- 原子性指一系列的操作,要么全部执行成功,要么全部不执行,不会出现执行一半的情况,是不可分的。
- 原子性怎么实现
- 使用synchronized或Lock加锁实现,保证任一时刻只有一个线程访问该代码块
- 使用原子操作
- Java中的原子操作有哪些
- 除long和double之外的基本类型的赋值操作(64位值,当成两次32位的进行操作)
- 所有引用reference的赋值操作
- java.concurrent.Atomic.*包中所有类的原子操作
- 创建对象的过程是否是原子操作(常应用于双重检查+volatile创建单例场景)
- 创建对象实际上有3个步骤,并不是原子性的
创建一个空对象
调用构造方法
创建好的实例赋值给引用
可见性
- 什么是可见性问题
- 可见性指的是当一个线程修改了某个共享变量的值,其他线程是否能够马上得知这个修改的值。
- 为什么会有可见性问题
- 对于单线程程序来说,可见性是不存在的,因为我们在任何一个操作中修改了某个变量的值,后续的操作中都能读取这个变量值,并且是修改过的新值。
- 对于多线程程序而言。由于线程对共享变量的操作都是线程拷贝到各自的工作内存进行操作后才写回到主内存中的,这就可能存在一个线程A修改了共享变量x的值,还未写回主内存时,另外一个线程B又对主内存中同一个共享变量x进行操作,但此时A线程工作内存中共享变量x对线程B来说并不可见,这种工作内存与主内存同步延迟现象就造成了可见性问题
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NQ64nejr-1621230902081)(_v_images/20210517113554734_1788954426.png)]
- 如何解决可见性问题
- 解决方法1: 加volatile关键字保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值立即被其他的线程看到,即修改的值立即更新到主存中,当其他线程需要读取时,它会去内存中读取新值
- 解决方法2:使用synchronized和Lock保证可见性。因为它们可以保证任一时刻只有一个线程能访问共享资源,并在其释放锁之前将修改的变量刷新到内存中
- 案例
/**
* 〈可见性问题分析〉
*
* @author Chkl
* @create 2020/3/4
* @since 1.0.0
*/
public class FieldVisibility {
int a = 1;
int b = 2;
private void change() {
a = 3;
b = a;
}
private void print() {
System.out.println("b=" + b + ";a=" + a);
}
public static void main(String[] args) {
while (true) {
FieldVisibility test = new FieldVisibility();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.change();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.print();
}
}).start();
}
}
}
循环创建两类线程,一个线程用于做值的交换,一个线程用于打印值
比较直观的三种结果
* 打印线程先执行:b = 2, a = 1
* 交换线程先执行:b = 3, a = 3
* 交换线程执行到一半就切出去打印了,只执行了a=3赋值操作:b = 2 , a =3
实际上除了很容易想到的三种情况外还有一种特殊情况:b = 3 , a = 1
- 这种情况就是可见性问题
- a的值在线程1(执行交换线程)的本地缓存中进行了更新,但是并没有同步到共享缓存,而b的值成功的更新到了共享缓存,导致线程2(执行打印线程)从共享缓存中获取到的数据并不是实时的最新数据
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8BlxDtVA-1621230902084)(_v_images/20210517134341209_471766678.png)]
有序性(重排序)
- 什么是重排序
在线程内部的两行代码的实际执行顺序和代码在Java文件中的逻辑顺序不一致,代码指令并不是严格按照代码语句顺序执行的,他们的顺序被改变了,这就是重排序。
-
重排序的意义
- JVM能根据处理器特性(CPU多级缓存系统、多核处理器等)适当的对机器指令进行重排序,使机器指令能更符合CPU的执行特性,最大限度的发挥机器性能。
- 案例
计算:
a = 3;
b = 2;
a = a + 1;
重排序优化前的instructions
load a
set to 3
store 3
load b
set to 2
store b
load a
set to 4
store a
经过重排序处理后
load a
set to 3
set to 4
store a
load b
set to 2
store b
上述少了两个指令,优化了性能
- 重排序的3种情况
- 编译器优化(JVM,JIT编辑器等): 编译器在不改变单线程程序语义放入前提下、可以重新安排语句的执行顺序
- 指令级并行的重排序:现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
- 内存系统的重排序: 由于处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cPlNH0Yd-1621230902086)(_v_images/20210517134937282_770007123.png)]
volatile
-
什么是volatile
* volatile是一种同步机制,比synchronized或者Lock相关类更轻量级,因为使用volacile并不会发生上下文切换等开销很大的行为 * volatile是无锁的,并且只能修饰单个属性
-
什么时候适合用volatile
* 一个共享变量始终只被各个线程赋值,没有其他操作 * 作为刷新的触发器,引用刷新之后使修改内容对其他线程可见(如CopyOnRightArrayList底层动态数组通过volatile修饰,保证修改完成后通过引用变化触发volatile刷新,使其他线程可见)
-
volatile的作用
* 可见性保障:修改一个volatile修饰变量之后,会立即将修改同步到主内存,使用一个volatile修饰的变量之前,会立即从主内存中刷新数据。保证读取的数据都是最新的,之前的修改都是可见的。 * 有序性保障(禁止指令重排序优化):有volatile修饰的变量,赋值后多了一个“内存屏障“( 指令重排序时不能把后面的指令重排序到内存屏障之前的位置)
-
volatile的性能
- volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。