1. 线程安全
单线程和多线程执行相同的逻辑的代码,如果得到的结果是相同的就是线程安全,如果不相同就是线程不安全
2. 导致线程不安全的原因
- 操作系统对线程的调度是随机的
- 多线程对同一个变量进行修改
- 修改操作不是原子的
原子性 :一个操作要么完整地执行,要么完全不执行,在执行过程中不能被其他线程"插进来" ,可以理解为不可分割的 。
例如一行java代码看着好像是一行代码,但是这行代码变成机械语言的时候,就可能变成了多个 CPU 指令,在执行这些指令的过程中,可能被其他进程插队,程序就可能出现错误,这就是不满足原子性的
- 内存可见性
可见性是指一个线程修改共享变量,可以及时被其他线程看见
java 内存模型 :

java 进程中每个线程都有一个工作内存(work memory),这些线程会共享一个主内存(main memory)
- 当线程修改数据的时候会把
主内存中的数据拷贝到工作内存中,修改工作内存的数据后,将数据写回到主内存中 - 当线程读取数据的时候会把
主内存中的数据拷贝到工作内存中,读取工作内存的数据
由于每个线程有⾃⼰的⼯作内存,这些⼯作内存中的内容相当于同⼀个共享变量的"副本"
修改线程1的⼯作内存中的值,线程2的⼯作内存不⼀定会及时变化
这里的主内存指的是 平时说的内存; 工作内存指的是寄存器和缓存
3. 示例1
代码
定义 class Counter
java
class Counter {
public int count = 0;
public void add(){
count++;
}
}
创建出 2 个线程 来完成 10w 次 count 的自增
java
public static void main(String[] args) throws InterruptedException {
Counter c = new Counter();
Thread t1 = new Thread(() -> {
for(int i = 1; i <= 50000; i++) {
c.add();
}
});
Thread t2 = new Thread(() -> {
for(int i = 1; i <= 50000; i++) {
c.add();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count = " + c.count);
}

发现并不是预期的结果 count = 100000

再次运行,也不是预期结果,都是 < 10w 的数字
原因
CPU 在执行 count++ 分成了 3 个 指令
- 从内存中读取 count 到寄存器
- 修改寄存器中的值count + 1
- 把内存器中新的count值写回到内存
这就很可能在线程 t1在执行的这三个指令时,被线程 t2插队由于操作系统对线程的调度是随机的,这只是其中的一种被插队可能性
synchronized 关键字
Q :怎么把修改操作打包成一个整体,让它具有原子性 ?
A :
synchronized关键字,也就是锁
锁就像是一个厕所,A 想上厕所,进入厕所把门给锁上(加锁 ),其他人如果也想用这个厕所,进不去只能在外面等待
等 A 上完厕所把厕所上的门的锁打开(解锁),其他人才能使用。
如果一个线程加锁成功, 那么其它线程就不能加锁, 进入堵塞等待的状态, 此时线程的状态就是 BLOCKED(锁竞争)
synchronized会起到互斥效果
- 进⼊ synchronized 修饰的代码块,相当于加锁
- 退出 synchronized 修饰的代码块,相当于解锁

只需要在 counter 类的 add 方法加上 snychronized 关键字
java
public synchronized void add(){
count++;
}
加锁后 ------>达到了预期的结果 :

4. 示例 2
代码
定义 2 个线程
t1、t2和 1 个变量flag
t1中包含⼀个循环,这个循环以flag == 0为循环条件
t2中从键盘读⼊⼀个整数,并把这个整数赋值给flag.当
flag的值是 0 以外的数字,t1线程结束
java
private static int flag = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while(flag == 0){
}
System.out.println("t1 线程结束");
});
Thread t2 = new Thread(() ->{
Scanner sc = new Scanner(System.in);
System.out.println("输入 0 以外的数字中止线程 t1");
flag = sc.nextInt();
System.out.println("t2 线程结束");
});
t1.start();
t2.start();
}
预期:当⽤⼾输⼊⾮0的值的时候 , t1线程结束。但是 t1线程 没有结束那说明就出现了线程安全问题

原因
内存可见性问题是由于 编译器优化 导致的

在执行 while 循环的时候:
- CPU 要从内存中读取数据(flag)到寄存器中
- 比较寄存器中的值(flag) 和 0 是否相同,如果相同就继续执行;不相同就跳转到指定位置

从内存中读取数据的指令开销的是 远大于 在寄存器中比较数据的指令开销

虽然在另一个线程中,有修改操作,但是编译器并不知道另一个线程修改的时机 , 在读取用户输入的数据之前,编译器都是读到了相同的值,于是编译器就把从 内存中读数据(flag)到寄存器中 优化成 只从寄存器中读取数据(flag), 即使是 在 t2 线程中修改了内存中数据(flag), t1 线程读取的还是寄存器中的旧的数据(flag)
如果从 JMM 的角度来说 :
线程 t1 的 while 循环判定的是工作内存中的数值,但 t2 线程修改的是主内存的数据,由于工作内存中是主内存中数据的副本,修改主内存不会影响工作内存
volatile 关键字
使用 volatile 关键字修饰某个变量,volatile 关键字只是针对内存可见性,不保证原子性 、 没有互斥的特点
告诉编译器这个变量是可变的,之后编译器针对这个变量就不会进行优化了,就会从内存中读取数据
java
private volatile static int flag = 0;

wait & notify
wait & notify 的作用是让多个线程按照一定的顺序来执行代码逻辑
如果需要先执行 线程 A 的 逻辑1 再执行 线程 B 的逻辑2 :线程 A 要先执行 逻辑1 ,但是很可能 线程B 先被调度,线程 B 就先 wait 下,等线程A的 逻辑1 执行完 notify 线程B,线程B再执行逻辑2
wait 和 notify 是 Object 类方法, 必须是在 snychronized 代码块中使用
| 方法 | 说明 |
|---|---|
| wait() | 让当前线程进入等待状态(没有 notify 就一直等) |
| wait(long timeout) | 超时后解除等待(仍需要获取锁) |
| notify() | 如果有多个等待的线程随机唤醒一个 |
| notifyall() | 如果有多个等待的线程唤醒所有 |
wait()
java
public static void main(String[] args) {
Object obj = new Object();
Thread t = new Thread(()->{
synchronized (obj){
System.out.println("wait 前");
try {
obj.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("wait 后");
}
});
t.start();
}
调用 wait 后,线程进入堵塞状态,后面的逻辑不再执行

wait 方法会执行 :
- 释放当前对象的锁
- 让当线程进入堵塞状态
- 被 notify 唤醒,重新获取锁,从阻塞状态恢复到就绪状态
被加锁的对象和调用 wait() 的对象必须是同一个对象
notify()
java
public static void main(String[] args) {
Object obj = new Object();
Thread t1 = new Thread(() -> {
synchronized (obj) {
System.out.println("t1 wait 前");
try {
obj.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("t1 wait 后");
}
});
Thread t2 = new Thread(() -> {
synchronized (obj) {
System.out.println("t2 notify 前");
Scanner sc = new Scanner(System.in);
System.out.println("输入任意内容触发 notify");
sc.next();
obj.notify();
System.out.println("t2 notify 后");
}
});
t1.start();
t2.start();
}

wait() 和 notify() 是怎么让代码按照一定的顺序来执行的
wait() 和 sleep() 的区别
| 相同点 |
|---|
| 1. 都可以让线程堵塞,可以设置堵塞的时间 |
| 不同点 |
| 1. wait() 设计出来是为了被 notify;sleep() 是为了按照一定的时间进行堵塞 |
| 2. wait() 必须搭配锁使用;sleep() 是 Thread 类的静态方法不需要搭配锁 |
| 3. wait() 会释放锁再获取锁 ;sleep() 在锁内部休眠时不会释放锁 |

