文章目录
- 一个线程不安全的案例
- 造成线程不安全的原因
- 抢占式执行
- 多个线程修改同一个变量
- 修改操作不是原子的
- 内存可见性问题
- 指令重排序问题
- 如何让线程变得安全?
- 加锁
- volatile
一个线程不安全的案例
题目:有较短时间让变量count从0加到10_0000
解决方案:我们创建两个线程分别让count加5_0000次
结果:count < 10_0000
class Count{public int count = 0;public void increase(){count++;}}
public class Demo {//验证线程不安全问题public static void main(String[] args) {Count count1 = new Count();// 操作同一个变量Thread thread1 = new Thread(() -> {for (int i = 0; i < 50000; i++){count1.increase();}});Thread thread2 = new Thread(() -> {for (int i = 0; i < 50000; i++){count1.increase();}});thread1.start();thread2.start();try {thread1.join();} catch (InterruptedException e) {e.printStackTrace();}try {thread2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println(count1.count); // <100000}
}
造成线程不安全的原因
抢占式执行
操作系统调度线程的时候,是一个“随机”的过程,当两个线程都参与调度的时候,谁先谁后不确定。
多个线程修改同一个变量
线程之间是“并发执行的”,当多个线程修改同一个变量时,多个线程同时获取到了变量值。某一个线程修改了变量,修改的结果不能被其他线程知道,其他线程还会修改原先获取到的值,导致结果错误。
如果修改的是不同的变量,线程之间独立执行,不会出现问题。
修改操作不是原子的
count++操作底层是三条指令在CPU上完成的:
load:把内存中的值读到CPU寄存器中;
add:count+1;
save:把寄存器的值写回内存
由于这三条指令不是原子的,两个线程在执行时就会有不同的执行顺序:
在这些执行顺序下,都会使count没有正确的++,使最终结果出错。
内存可见性问题
JVM优化引入的BUG。例如,两个线程在操作同一个变量,一个线程读并且比较,一个线程修改。假设读操作非常频繁的情况下,比较操作也会非常的频繁。但是读是从内存中读,比较是在CPU里比较。比较的速度远远大于读的速度。而且每次读到的值还一样,这时编译器就会大胆优化:只读取一次,后面就不从内存中读了。每次比较都和前面读取到的值比较,不和内存中的值比较。这时另一个线程把内存中的值修改了但是这个线程比较的还时原来的值,就会有问题。
指令重排序问题
JVM优化引入的BUG。由我们自己写的代码在大多数情况下的执行流程中,指令的执行顺序往往都不是最优选择,即没有使运行速度达到最快。因此,JVM在编译时,就会在逻辑等价的前提下,对我们的指令进行重新排序使代码的运行速度变快。
这样的优化在单线程时,是没有问题的。但是在多线程的情况下,线程之间是抢占式执行的,哪条指令先执行哪条指令后执行不确定,就可能有问题。
如何让线程变得安全?
“抢占式执行”是线程调度的基本方式,我们无法干预。
“多个线程修改同一个变量”:我们在特定场景下就是得修改同一个变量,也无法改变。
“操作不是原子的”:我们保证线程安全的主要方式,通过synchronized加锁。
“内存可见性”“指令重排序”:JVM优化的问题。使用volatile解决
加锁
volatile
public volatile int count = 0;
volatile只有一个用法就是修饰变量,表示该变量的值必须从内存中读取,不能从缓存中读取。
即:volatile禁止了编译器优化,避免了直接读取CPU寄存器中缓存的数据,而是每次都读取内存。
但是volatile并不能保证是操作是原子性的,因此,它只适合用于一个线程读,一个线程修改的场景。不适合用于两个线程都修改的场景。
谈到volatile就会联想到JMM(Java Memory Model):Java内存模型。
在JMM中,引入了新的术语:
工作内存(work memory):即CPU寄存器(缓存)
主内存(main memory):真正读取的内存
站在JMM的角度看待volatile:
正常程序的执行过程中,先会把主内存的数据加载到工作内存中,再进行计算处理。编译器优化可能会导致不是每次都会真正的读取主内存,而是直接读取工作内存中的缓存数据,就可能导致内存可见性问题。volatile起到的效果就是保证每次读取数据都是真的从主内存中重新读取。