目录
JMM(Java Memory Model) java内存模型
1.重现线程不安全
场景:用两个线程同时对一个变量自增5万次,预期结果自增和为10万次,观察实际结果与预期是否一致
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
class Counter {
public int count = 0;
void increase() {
count++;
}
}
运行三次甚至多次,都没有达到预期目标,此时就出现了线程不安全问题
2. 线程安全的概念
在多线程环境下运行的结果符合我们的预期(即在与单线程结果相同),此时说这个线程安全。
3.造成线程不安全的原因
1.线程抢占式执行
线程抢占式执行的结果会导致我们线程执行顺序与我们预期的顺序不符,这也是导致线程不安全的主要原因,而且这个问题我们解决不了,完全由CPU自己随机调度。
2.多个线程修改同一个变量
多个线程修改不同变量,不会出现线程安全问题
多个线程读取同一个变量,不会出现线程安全问题
一个线程读取或修改同一个变量,不会出现线程安全问题
3.原子性
原子性是指在一个操作中就是CPU不可以在中途暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行。原子性就是指该操作是不可再分的。
上述代码中要进行以下操作:
1.从内存或者寄存器把count的值读出来 LOAD
2.执行自增操作 ADD
3.把计算结果写回内存或者寄存器 STORE
左边保证了原子性,右边未保证原子性,会出现线程不安全
针对右图,起始count=0,t1读入后被CPU调度走,紧接着t2读入。此时t1调度回来操作,完成后将count= 1 保存到内存,t2继续执行,结果依然是count=1,并存入到内存。此时两个线程都执行操作,但是count的值实际改变了一次,那么此时就是线程不安全。
4.内存可见性
内存可见性指:一个线程队共享变量的值修改,其他线程可以感知到。
此时我们要提到一个模型——JMM(Java Memory Model) java内存模型
每一个工作内存都是独立的,相互之间不可访问,假若线程1改变了变量的值,但是没有及时写入主内存,其他线程未感知到,就造成了线程不安全问题。
JMM(Java Memory Model) java内存模型
- 线程之间的共享变量存在 主内存 (Main Memory).
- 每一个线程都有自己的 "工作内存" (Working Memory) .
- 当线程要读取一个共享变量的时候, 会先把变量从主内存拷贝到工作内存, 再从工作内存读取数据.
- 当线程要修改一个共享变量的时候, 也会先修改工作内存中的副本, 再同步回主内存
此时若线程修改之后变量的值,不能及时同步到主内存,那么就会出现问题
为什么有这么多内存?
在JMM中,主内存对应的是物理内存,而工作内存指的是CPU中的寄存器,L1,L2,L3各级缓存的一个统称。
为什么在这些存储的地方拷来拷去?
因为 CPU 访问自身寄存器的速度以及高速缓存的速度, 远远超过访问内存的速度(快了 3 - 4 个数量级, 也就是几千倍, 上万倍).
比如某个代码中要连续 10 次读取某个变量的值, 如果 10 次都从内存读, 速度是很慢的. 但是如果只是第一次从内存读, 读到的结果缓存到 CPU 的某个寄存器中, 那么后 9 次读数据就不必直接访问内存了. 效率就大大提高了。但是CPU太贵了!!!CPU访问速度:寄存器 > 内存 > 硬盘
所以不能一直采用寄存器!
JMM规定 :
- 所有线程不能直接修改主内存的共享变量
- 如果要修改内存中的共享变量,那么就需要把这个变量从主内存加载到工作内存,修改完成后再刷新到主内存
- 各个线程之间不能相互通信,做到内存级别的线程隔离
- 如果要通过某种方式实现线程间的相互通信,就叫内存可见性。
5.指令重排序
一段代码是这样的:
1. 去前台取下 U 盘
2. 去教室写 10 分钟作业
3. 去前台取下快递
如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按 1->3->2的方式执行,也是没问题,可以少跑一次前台。这种叫做指令重排序指令重排序在单线程情况下没有什么问题;其必须要建立在能输出正确结果的基础上
发生重排序条件:
- 指结果必须正确
- 指令重排序逻辑上互不影响