文章目录
一、线程安全的本质
每个内核都有自己独立的缓存空间,缓存之间的数据是不可见的,只有当数据写入公共缓存区之后,数据才是可见的。导致多线程下,数据不一致。
1. 原子性
public class Demo {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
for (int j = 0; j < 1000; j++) {
new Thread(()-> count++).start();
}
Thread.sleep(3000);
System.out.println(count);
}
}
正常结果count = 1000, 但是count++无法保证原子性,导致结果小于等于1000
2.可见性
3.有序性
二、Java内存模型
Java内存模型是一种抽象结构,它提供了合理的禁用缓存以及禁止指令重排序的方法来解决可见性、有序性问题。
1.JMM和硬件模型对应简图
2.可见性、有序性的解决方案
Volatile、synchronized、final关键字
Happens-Before原则
三、Synchronized的作用
可以解决原子性、可见性、有序性问题
1.锁的范围
1.对于普通同步方法,锁是当前实例对象
2.对于静态同步方法,锁是当前类的Class对象
3.对于同步代码块,锁是Synchronized括号里配置的对象
四、Volatile的作用
Volaile可以用来解决可见性和有序性问题
1. Lock指令的作用
将当前处理器缓存行的数据写回到系统内存。
写回内存操作会使在其他CPU里缓存了该内存地址的数据无效。
2. 什么时候用Volatile
当存在多个线程对同一个共享变量进行修改的时候。需要增加Volatile,保证数据修改的实时可见
3. CPU层面的内存屏障
Store Barrier: 强制所有在store屏障指令之前的store指令,都在该store屏障指令执行之前被执行,并把store缓冲区的数据都刷到CPU缓存
Load Barrier:强制所有在load屏障指令之后的load指令,都在该load屏障指令执行之后被执行,并且一直等到load缓冲区被该CPU读完才能执行之后的load指令
Full Barrier:复合了load和store屏障的功能
4. 总结
- volatile 实际上是通过内存屏障防止指令重排序以及禁止cpu告诉缓存来解决可见性问题
- Lock指令,它本意上是禁止高速缓存解决可见性问题,但实际上在这里,它表示的是一种内存屏障的功能。也就是说针对当前的硬件环境,JMM层面采用Lock指令作为内存屏障来解决可见性问题。
五、final域
final 在Java中是一个保留关键,可以声明成员变量、方法、类以及本地变量。一旦你将引用声明作final,你将不能改变这个引用。
1. final域和线程安全的关系
对于final域,编译器和处理器要遵守两个重排序规则
- 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
- 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。
2. 写final域重排序规则
- JMM禁止编译器把final域的写重排序到构造函数之外。
- 编译器会在final域的写之后,构造函数return之前,插入一个StoreStore屏障。这个屏障禁止处理器把final域的写重排序到构造函数之外。
3. 读final域重排序规则
在一个线程中,初次读对象引用与初次读改对象包含的final域,JMM禁止处理器重排序这两个操作,编译器会在读final域操作的前面插入一个LoadLoad屏障。
4. 溢出带来的重排序问题
六、Happers-Before规则
Happers-Before是一种可见性规则,它表达的含义是前面一个操作的结果对后续操作是可见的。
- 程序顺序规则
- 监视器锁规则
- Volatile变量规则
- 传递性规则
- start()规则
- Join()规则
1. 程序顺序规则
在单线程中,指令重排序,不会影响程序执行结果。
2. 监视器锁规则
对一个锁的解锁Happens-Before于后续对这个锁的加锁。(线程B读到的线程A解锁的变量,一定是线程A修改之后的值)
3. Volatile变量规则
对一个volatile域的写,Happens-Before于任意后续对这个volatile域的读。
4. 传递性规则
如果A Happends-Before B, 且B Happens-Before C,那么A Happens-Before C。