多线程并发的三个特性
- 原子性:即一个操作或多个操作,要么全部执行,要么就都不执。执行过程中,不能被打断
- 有序性:程序代码按照先后顺序执行
为什么会出现无序问题呢?因为指令重排(重排序是编译器和处理器为了提高程序运行效率,会对输入代码进行优化的一种手段。它不保证程序中,各个语句执行先后顺序的一致。
) - 可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值
有序性
1 单线程中的重排序
int count = 0;
boolean flag = false;
count = 1; //语句1
flag = true; //语句2
在单线程的环境中,语句1是在语句2前面的,但是JVM在执行的时候,并不不一定保证语句1一定会在语句2前面执行。这里可能会发生指令重排。
图片原文链接
但是无论怎么排序,程序最终运行的结果和不重新排序是一致的。Java编译器、运行时和处理器都会保证,在单线程下遵循as-if-serial语义。
as-if-serial:
这个语义就是保证我们排序后的结果和没有进行排序的结果一致。所以编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作可能被编译器和处理器重排序。
什么事存在依赖关系呢?
boolean count = 0;
boolean flag = false;
flag = true; //语句1
count = flag; //语句2
比如这个时候如果发生重排序count的值最终为false,这和没有排序之前不一致因为我们没有排序最终的运行结果应该是true。这是因为count的结果依赖flag的结果。
2 多线程中的重排序
// Thread 1
init = false; // 语句1
context = loadContext();
init = true; // 语句2
// Thread 2
while (!init) {sleep();
}
execute(context);
在多线程的环境中如果线程1中发生了重排序,这个时候环境还没有初始化,但是init由于重排序的原因已经执行了语句2变为了true,这个时候线程2中使用的是没有初始化的环境,必然导致非常多的问题。
可见性
在多核 CPU 中,每个核的自己的缓存,关于同一个数据的缓存内容可能不一致。
为了进一步加快程序的执行效率,在CPU和物理内存之间还有一个高速缓存的存在,这样程序的执行过程也就发生了改变,变成了程序在运行过程中,会将运算所需要的数据从主内存复制一份到CPU的高速缓存中,当CPU进行计算时就可以直接从高速缓存中读数据和写数据了,当运算结束再将数据刷新到主内存就可以了。
如果是单核这样的机制不会有任何问题,但是一旦出现多核CPU就会有问题,每个核对同一份数据的副本可能值不一样。
Java内存模型(JMM)
Java为了保证并发编程中可以满足原子性、可见性及有序性,诞生出了一个重要的概念,那就是内存模型,内存模型定义了共享内存系统中多线程程序读写操作行为的规范。通过这些规则来规范对内存的读写操作,从而保证指令执行的正确性,它解决了 CPU 多级缓存、处理器优化、指令重排等导致的内存访问题。
强烈推荐看这篇博文
MM本质上可以理解为,Java 内存模型规范了 JVM 如何提供按需禁用缓存和编译优化的方法。具体来说,这些方法包括:
- volatile(保证可见性和有序性)、synchronized(保证三个特性) 和 final 三个关键字
- Happens-Before 规则(https://www.pdai.tech/md/java/thread/java-thread-x-theorty.html)