-
- 重排序
-
- 数据依赖性
-
重排序对多线程的影响
-
- 如果解决?
-
volatile域的重排序规则
-
final域的重排序规则
-
- 写final域的重排序规则
-
读final域的重排序规则
-
如果final域修饰的是引用类型
- 在Java中,所有实例域、静态域和数组元素都存储在堆内存中,堆内存在线程之间共享。局部变量(Local Variables),方法定义参数(Java语言规范称之为Formal Method Parameters)和异常处理器参数(ExceptionHandler Parameters)不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响。
- 从图来看,如果线程A与线程B之间要通信的话,必须要经历下面2个步骤。
1线程A把本地内存A中更新过的共享变量刷新到主内存中去。
2)线程B到主内存中去读取线程A之前已更新过的共享变量。
- 在执行程序时,为了提高性能,编译器和处理器常常会对指合做重排序。重排序分3种类
型。
-
编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
-
指合级并行的重排序。现代处理器采用了指命级并行技术(Instruction-LevelParallelism,ILP)来将多条指命重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指爷的执行顺序。
-
内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
数据依赖性
-
编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。
-
所谓数据依赖性就是比如俩个操作同时操作一个变量, 并且只要有一个是写操作, 那这俩个操作就存在数据依赖性
-
这里所说的数据依赖性仅针对单个处理器中执行的指命序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑。
class Service {
private int a= 0;
private boolean flag= false;
public void write() {
a = 1; //1
flag= true; //2
}
public int read() {
int i = 0;
if (flag) { //3
i = a * a; //4
}
return i;
}
}
-
key是一个标记, 用来判断i是否被写入, 如果此时有俩个线程, A首先执行write操作, B执行read操作, 那线程B在执行4操作 , 也就是读取i的值 是否是已经写入的呢? 这个答案的否定的
-
由于操作1和操作2没有数据依赖的关系, 编译器和处理器可能会对这俩个操作进行重排序, 然后就会出现下面的问题
-
这时你会发现, 他是先置flag为true , 那么线程B就会判断为true, 所以线程B在读取a的值有可能就会读到脏数据
-
再看当三四操作的指令重排序
- 三四操作存在控制依赖关系, 会影响执行序列的执行并行度, 但是在编译器在进行调优重排序的时候, 是不鸟他的, 所以上图中线程B读取到的a值, 还是一个脏数据
如果解决?
- 使用同步程序达到一致性效果, 简单来说就是直接咔咔上锁
class Service {
private int i = 0;
private boolean key = false;
public synchronized void write() {
i = 1; //1
key = true; //2
}
public synchronized int read() {
int a = 0;
if (key) { //3
a = i * i; //4
}
return a;
}
}
- 在同步程序中, 这俩个write和read就成了同步方法, 也就是他们会串行执行, 这样即使在一个方法内发生了重排序, 对另一个线程来说是没有影响的
-
volatile修饰的变量只是保证内存可见性, 也就是只是说你这个线程读到的数据一定是从内存中读出来的. 但是他并不是原子的, 如果你在多线程环境下进行运算, 依旧不是安全的.
-
所以volatile域就会有重排序规则
- 上图中, 我们可以发现
-
当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。
-
当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
-
当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。
-
总而言之就是在单个volatile写之前和读之后是绝对不让重排序的, 俩个操作都是volatile操作 ,是更是不让重排序的,
-
写之前
- 读之后
- 对于final域,编译器和处理器要遵守两个重排序规则。
-
在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用 变量,这两个操作之间不能重排序。
-
初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能 重排序。
class FinalExample {
int i;
final int j;
static FinalExample obj;
public FinalExample() {
this.i = 1; //写普通域
this.j = 2; //写final域
}
public static void write() { //A线程执行
obj = new FinalExample();
}
public static void read() { //B线程执行
最后
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,不论你是刚入门Java开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!
如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
e5WfD-1715716855049)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,不论你是刚入门Java开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!
如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!