多线程——volatile 关键字 wait 和 notify
volatile 能保证内存可见性

—.缓存层级的性能与容量特点

二、缓存的存在原因
寄存器虽然速度极快,但空间太小、存储能力有限。为了在速度和存储容量之间取得平衡,CPU 开发者在 CPU 上额外建设了 “缓存” 区域,用于临时存储内存数据,提升数据访问效率。
三、缓存的发展历程
- 最早的 CPU:无缓存。
- 后续演进:先出现 L1 缓存 → 再发展为 L1+L2 缓存 → 现在常见 L1+L2+L3 缓存 → 未来是否会有 L4 缓存暂不确定。
volatile 修饰的变量,能够保证"内存可见性"

代码在写入volatile修饰的变量的时候,
• 改变线程工作内存中volatile变量副本的值
• 将改变后的副本的值从⼯作内存刷新到主内存 代码在读取volatile修饰的变量的时候,
• 从主内存中读取volatile变量的最新值到线程的⼯作内存中
• 从工作内存中读取volatile变量的副本 前面我们讨论内存可见性时说了,直接访问工作内存(实际是CPU的寄存器或者CPU的缓存),速度非常快,但是可能出现数据不⼀致的情况. 加上volatile,强制读写内存.速度是慢了,但是数据变的更准确了
代码示例:
static class Counter {public int flag = 0;}public static void main(String[] args) {Counter counter = new Counter();Thread t1 = new Thread(() -> {while (counter.flag == 0) {// do nothing}System.out.println("循环结束!");});Thread t2 = new Thread(() -> {Scanner scanner = new Scanner(System.in);System.out.println("输⼊⼀个整数:");counter.flag = scanner.nextInt();});t1.start();t2.start();}// 执⾏效果 // 当⽤⼾输⼊⾮0值时, t1 线程循环不会结束. (这显然是⼀个 bug) 在这个代码中
• 创建两个线程t1和t2
• t1中包含一个循环,这个循环以flag==0为循环条件
• t2中从键盘读入⼀个整数,并把这个整数赋值给flag
t1 读的是自己工作内存中的内容
当t2对flag变量进行修改,此时t1感知不到flag的变化
如果给flag加上volatile
static class Counter {public volatile int flag = 0;}// 执⾏效果 // 当⽤⼾输⼊⾮0值时, t1 线程循环能够⽴即结束2、volatile 不保证原子性
volatile 和 synchronized 有着本质的区别:synchronized能够保证原子性,volatile保证的是内存可见性
代码示例:
static class Counter {volatile public int count = 0;void increase() {count++;}}public static void main(String[] args) throws InterruptedException {final 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);}这个是最初的演示线程安全的代码
给increase方法去掉synchronized
给count加上volatile关键字
此时可以看到,最终count的值仍然无法保证是100000
4个导致死锁的原因
1. 互斥
指资源在同一时间只能被一个线程占用。例子:打印机是典型的互斥资源,某时刻只有一个线程(或用户)能使用它打印文档,其他线程必须等待。
2. 不可剥夺 / 不可抢占
指线程占用的资源在未主动释放前,不能被其他线程强行夺取。例子:线程 A 获取了数据库连接资源,在它未完成操作并主动释放连接前,线程 B 无法强制从 A 手中夺取这个连接,只能等待 A 释放。
3. 请求和保持
指线程在保持已有资源的同时,又去请求新的资源。例子:线程 A 已占用资源 1,还在请求资源 2;同时线程 B 已占用资源 2,还在请求资源 1。双方都 “抱着已有资源不放,又想要对方的资源”,就可能引发死锁。
4. 循环等待
指多个线程之间形成资源请求的循环链。例子:线程 A 等待线程 B 释放资源 2,线程 B 等待线程 C 释放资源 3,线程 C 等待线程 A 释放资源 1,形成 “A→B→C→A” 的循环请求链,导致所有线程都无法继续执行。
二、wait 和 notify
由于线程之间是抢占式执行的,因此线程之间执行的先后顺序难以预知
但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序
一、join的 “等待”
- 核心逻辑:等待另一个线程彻底执行完毕,当前线程才会继续执行。
- 本质:是对线程执行完成状态的等待,直到目标线程的生命周期结束。
二、wait的 “等待”
- 核心逻辑:等待另一个线程执行
notify/notifyAll,当前线程就会被唤醒继续执行(不需要等对方线程完全执行完)。 - 本质:是基于 “消息通知” 的等待,由其他线程主动触发唤醒条件。
三、“锁” 的 “等待”
- 核心逻辑:线程执行到加锁逻辑时,不一定必然触发等待—— 只有当锁已被其他线程占用时,才会进入等待队列;若锁空闲,线程会直接获取锁并执行。
- 本质:这种等待是 “被动且不受控” 的,是否等待取决于锁的当前占用状态。
注意:当多个线程竞争一把锁的时候,获取到锁的线程如果释放了,其他的那个线程拿到的锁?
这是不确定的(随即调度)
操作系统的调度都是随机的,其他的线程都属于在锁上阻塞等待,是阻塞状态。当前的这个释放锁的线程,是就绪的状态。这个线程有很大的概率再次拿到这个锁~~
一、wait和notify的归属
wait和notify是 **Object类的方法 **,这意味着 Java 中任意对象都具备这两个方法,是多线程通信的基础工具。
二、InterruptedException的意义
- 触发场景:Java 标准库中,所有产生阻塞的方法(如
wait、sleep、join等)都会声明抛出InterruptedException。 - 作用:表示线程在阻塞过程中,可被
interrupt方法随时唤醒,用于灵活控制线程的阻塞状态。
线程 A 调用 threadB.join() 阻塞等待线程 B 执行完毕,此时线程 C 可以调用 threadA.interrupt(),线程 A 会立即被唤醒,不再等待线程 B 执行完成,同时会抛出 InterruptedException 来提示这种 “被中断唤醒” 的情况。
执行 object.wait() 时,第一件事就是释放 object 对象对应的锁。但要执行这个释放操作,前提是 object 对象当前处于加锁状态(即当前线程已经获取了 object 的锁)。
notifyAll

1. wait()/wait(long timeout):让线程进入等待状态
- 作用:当前线程释放持有的锁对象,进入该对象的 “等待队列” 并阻塞,直到被其他线程唤醒或超时。
wait():无限期等待,必须通过notify()/notifyAll()唤醒。wait(long timeout):等待指定毫秒数后自动唤醒;若在超时前被唤醒,也会提前返回。
- 核心逻辑:释放锁 → 阻塞等待 → 被唤醒后重新竞争锁 → 获得锁后继续执行。
2. notify()/notifyAll():唤醒等待的线程
- 作用:唤醒在当前锁对象上等待的线程,让它们重新参与锁的竞争。
notify():随机唤醒一个处于该对象等待队列中的线程。notifyAll():唤醒所有处于该对象等待队列中的线程。
- 注意:唤醒后,线程不会立即执行,而是需重新竞争锁对象,只有获得锁后才能继续执行
wait()之后的代码。
1、wait()方法
wait 做的事情:
使当前执行代码的线程进行等待(把线程放到等待队列中)
释放当前的锁
满足一定条件时被唤醒,重新尝试获取这个锁
wait 要搭配synchronized来使用,脱离synchronized使用wait会直接抛出异常
wait 结束等待的条件:
其他线程调用该对象的notify方法
wait等待时间超时(wait方法提供一个带有timeout参数的版本,来指定等待时间)
其他线程调用该等待线程的interrupted方法,导致wait抛出 InterruptedException 异常
代码示例:观察wait()方法使用
public static void main(String[] args) throws InterruptedException {Object object = new Object();synchronized (object) {System.out.println("等待中");object.wait();System.out.println("等待结束");}}这样在执行到object.wait()之后就一直等待下去,那么程序肯定不能一直这么等待下去了。这个时候就需要使用到了另外一个方法唤醒的方法notify()。
2、notify()方法
notify 方法是唤醒等待的线程
方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
如果有多个线程等待,则有线程调度器随机挑选出一个呈wait状态的线程。(并没有"先来后到")
在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()⽅法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。
代码示例:使用notify()方法唤醒线程
创建WaitTask类,对应一个线程,run内部循环调用wait
创建NotifyTask类,对应另一个线程,在run内部调用一次notify
注意,WaitTask和NotifyTask内部持有同一个Objectlocker.WaitTask 和 NotifyTask要想配合就 需要搭配同一个Object
3、notifyAll()方法
notify方法只是唤醒某⼀个等待线程,使用notifyAll方法可以一次唤醒所有的等待线程
范例:使用notifyAll()方法唤醒所有等待线程
4、wait 和 sleep的对比(面试题)
其实理论上wait和sleep完全是没有可比性的,因为一个是用于线程之间的通信的,一个是让线程阻塞一段时间, 唯一的相同点就是都可以让线程放弃执行一段时间
当然为了面试的目的,我们还是总结下:
1)wait需要搭配synchronized使用.sleep不需要
2)wait是Object的方法sleep是Thread的静态方法

static class WaitTask implements Runnable {private Object locker;public WaitTask(Object locker) {this.locker = locker;}@Overridepublic void run() {synchronized (locker) {while (true) {try {System.out.println("wait 开始");locker.wait();System.out.println("wait 结束");} catch (InterruptedException e) {e.printStackTrace();}}}}}static class NotifyTask implements Runnable {private Object locker;public NotifyTask(Object locker) {this.locker = locker;}@Overridepublic void run() {synchronized (locker) {System.out.println("notify 开始");locker.notify();System.out.println("notify 结束");}}}public static void main(String[] args) throws InterruptedException {Object locker = new Object();Thread t1 = new Thread(new WaitTask(locker));Thread t2 = new Thread(new NotifyTask(locker));t1.start();Thread.sleep(1000);t2.start();}