


专栏:JavaEE初阶起飞计划
个人主页:手握风云
目录
[1.1. 原理](#1.1. 原理)
[1.2. Java memory model(Java内存模型)](#1.2. Java memory model(Java内存模型))
[1.3. volatile与synchronized的区别](#1.3. volatile与synchronized的区别)
[2.1. wait()方法](#2.1. wait()方法)
[2.2. notify()方法](#2.2. notify()方法)
[2.3. notifyAll()方法](#2.3. notifyAll()方法)
一、volatile关键字
1.1. 原理
当给变量添加了volatile关键字后,当编译器看到volatile的时候,就会提醒JVM运行的时候不进行上述的优化。具体来说,在读写volatile变量的前后指令添加"内存屏障相关的指令"。
1.2. Java memory model(Java内存模型)
首先一个Java进程,会有一个"主内存"存储空间,每个Java线程又会有自己的"工作内存"存储空间。形如下面的代码,t1进行flag变量的判定,就会把flag值从主内存先读取到工作内存,用工内存中的值进行判定。同时t2对flag进行修改,修改的则是主内存的值,主内存中的值不会影响到工作内存中的值。这里的工作内存相当于是打了个比方,本质上是CPU的寄存器和CPU的缓存构成的统称。
java
import java.util.Scanner;
public class Demo1 {
private static int flag = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (flag == 0) {
}
System.out.println("t1线程结束");
});
Thread t2 = new Thread(() -> {
Scanner in = new Scanner(System.in);
System.out.println("请输入flag的值:");
flag = in.nextInt();
System.out.println("t2线程结束");
});
t1.start();
t2.start();
t1.join();
t2.join();
}
}
其实,存储数据,不光是只有内存,外存(硬盘)、CPU寄存器、CPU上的缓存。

上图中的缓存也是CPU上存储数据的单元。寄存器能存数据,但是空间小;内存能存数据,空间大,但是速度慢。为了能够更好地协调寄存器和内存的数据同步,因此现代CPU都引入了缓存。CPU的缓存,空间比寄存器要大,速度比内存快。

上图中,越往上,速度就越快,空间就越小,成本就越高。编译器优化,把本身从内存读取的值,优化成从寄存器或者L1缓存、L2缓存、L3缓存中读取。
编译器优化,并非是100%触发,根据不同的代码结构,可能产生出不同的优化效果。形如下面的代码
java
import java.util.Scanner;
public class Demo2 {
private static int flag = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (flag == 0) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t1线程结束");
});
Thread t2 = new Thread(() -> {
Scanner in = new Scanner(System.in);
System.out.println("请输入flag的值:");
flag = in.nextInt();
System.out.println("t2线程结束");
});
t1.start();
t2.start();
t1.join();
t2.join();
}
}

虽然没写volatile,但是加了sleep也不会触发上述优化:1. 循环速度大幅度降低了;2. 有了sleep之后,一次循环的瓶颈就不是load,在于sleep上,此时优化也没什么用;3. sleep本身会触发线程调度,调度过程触发上下文切换,再次加载也会触发这个值重新读取了。
如下代码,我们改为一个静态成员变量count,会发现count也会触发优化。
java
import java.util.Scanner;
public class Demo2 {
private static int flag = 0;
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (flag == 0) {
count++;
}
System.out.println("t1线程结束");
});
Thread t2 = new Thread(() -> {
Scanner in = new Scanner(System.in);
System.out.println("请输入flag的值:");
flag = in.nextInt();
System.out.println("t2线程结束");
});
t1.start();
t2.start();
t1.join();
t2.join();
}
}

1.3. volatile与synchronized的区别
java
public class Demo3 {
private static volatile int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50_000; i++) {
count++;
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50_000; i++) {
count++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}

volatile这个关键字,能够解决内存可见性引起的线程安全问题,但是不具备原子性这样的特点。synchronized和volatile是两个不同的维度,synchronized是针对两个线程进行修改,而volatile是读取一个线程,另一个修改。
二、wait和notify
因为线程调度的顺序是不确定的,那我们就得保证每一种可能下都是正确的,就有点难搞了。我们之前提到过,join()方法可以控制线程的结束顺序。两个线程在运行的时候,我们希望是持续执行下去,但是两个线程中的某些环节,我们希望是能够有一定的顺序。
例如,假设有线程1和线程2。我们希望线程1先执行完某个逻辑后,再让线程2执行,此时就可以让线程2通过wait()主动进行阻塞,让线程1先参与调度。等线程1执行完对应的逻辑后,就可以通过notify()唤醒线程2。
另外wait和notify也能解决线程饿死的问题。线程饿死指的是在多线程编程中,某个或某些线程由于长时间无法获取到执行所需的资源,导致其任务迟迟无法完成,甚至永远无法执行的情况。 简而言之,就是某个线程"饿着肚子"等了很久,但一直没能得到"食物"。
如下图所示,当一个滑稽老铁进入ATM机里面取钱时,会进行上锁,其他滑稽老铁就必须在外面阻塞等待。当先进去的滑稽老铁发现ATM机里面没钱,便出去,而后又怀疑自己是不是看错了,于是又再次进入ATM机......如此循环往复,造成其他线程无法去CPU上执行,导致线程饿死。

线程饿死不像死锁那么严重。死锁发生之后,就会僵持住,除非程序重启,否则一直卡住。线程饿死,其他线程还是有一定机会拿到锁的,只是拿到锁的时间会延迟,降低程序的效率。
注意:wait、notify、notifyAll都是Object类里的方法。Java中任何一个类,都会有上述三种方法。
2.1. wait()方法
java
public class Demo4 {
public static void main(String[] args) {
Object o = new Object();
System.out.println("wait之前");
o.wait();
System.out.println("wait之后");
}
}

在Java标准库中,但凡涉及到阻塞类的方法,都有可能抛出InterruptedException异常,所以我们这里也要在前面加上InterruptedException异常。
java
public class Demo4 {
public static void main(String[] args) throws InterruptedException {
Object o = new Object();
System.out.println("wait之前");
o.wait();
System.out.println("wait之后");
}
}

但我们一运行程序之后,在打印"wait之前"语句后出现了IllegalMonitorStateException异常。此处的Monitor指的是sychronized,因为sychronized在JVM的底层实现被称为"监视器锁"。上面的异常是指锁的状态不符合预期。wait内部的第一件事就是释放锁,但释放锁的前提是得先拿到锁。像前面提到的滑稽老铁发现ATM机里面没有钱,如果滑稽老铁在里面等,意味着一直持有这个锁,其他人进不来。wait方法就要搭配sychronized使用。
java
public class Demo4 {
public static void main(String[] args) throws InterruptedException {
Object o = new Object();
System.out.println("wait之前");
synchronized (o) {
o.wait();
}
System.out.println("wait之后");
}
}


此处的阻塞会持续进行,直到其他线程调用notify。
java
import java.util.Scanner;
public class Demo5 {
private static Object locker = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println("t1等待之前");
synchronized (locker) {
try {
locker.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t1等到之后");
});
Thread t2 = new Thread(() -> {
Scanner in = new Scanner(System.in);
System.out.println("请输入任意内容,唤醒t1");
in.next();
// 必须是同一个锁对象
synchronized (locker) {
locker.notify();
}
});
t1.start();
t2.start();
}
}

wait要做的事情:
- 使当前执行代码的线程进行等待(把线程放到等待队列中)
- 释放当前的锁
- 满⾜⼀定条件时被唤醒,重新尝试获取这个锁
使用wait的时候,阻塞其实是有两个阶段的:1. WAITING的阻塞,通过wait等待其他线程的通知;2. BLOCKED的阻塞,当收到通知之后,就会重新获取锁,可能又会遇到锁竞争。假设notify后面还有别的逻辑,此时锁就会多占用一会儿。
默认情况下,wait的阻塞是死等。wait也可以设置参数等待时间的上限。
java
import java.util.Scanner;
/**
* @author gao
* @date 2025/7/9 20:54
*/
public class Demo6 {
private static Object locker = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println("t1等待之前");
synchronized (locker) {
try {
// t1在1000ms内没有收到任何通知,就会自动唤醒
locker.wait(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t1等待之后");
});
Thread t2 = new Thread(() -> {
Scanner in = new Scanner(System.in);
System.out.println("请输入任何内容,唤醒t1");
in.next();
synchronized (locker) {
locker.notify();
}
});
t1.start();
t2.start();
}
}


2.2. notify()方法
java
import java.util.Scanner;
public class Demo7 {
public static Object locker = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println("t1等待之前");
synchronized (locker) {
try {
locker.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t1等待之后");
});
Thread t2 = new Thread(() -> {
System.out.println("t2等待之前");
synchronized (locker) {
try {
locker.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t2等待之后");
});
Thread t3 = new Thread(() -> {
System.out.println("t3等待之前");
synchronized (locker) {
try {
locker.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t3等待之后");
});
Thread t4 = new Thread(() -> {
Scanner in = new Scanner(System.in);
System.out.println("输入任意内容,唤醒一个线程:");
in.next();
synchronized (locker) {
locker.notify();
}
});
t1.start();
t2.start();
t3.start();
t4.start();
}
}

这里我们就唤醒了t3线程,而其他两个线程还在阻塞等待。我们多运行几次,结果也会不同。注意,这里操作系统的随机调度并不是概率论里的概率均等,而是无法预测的。在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。
2.3. notifyAll()方法
使⽤notifyAll⽅法可以⼀次唤醒所有的等待线程。
java
import java.util.Scanner;
public class Demo8 {
private static Object locker = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println("t1等待之前");
synchronized (locker) {
try {
locker.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t1等待之后");
});
Thread t2 = new Thread(() -> {
System.out.println("t2等待之前");
synchronized (locker) {
try {
locker.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t2等待之后");
});
Thread t3 = new Thread(() -> {
System.out.println("t3等待之前");
synchronized (locker) {
try {
locker.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t3等待之后");
});
Thread t4 = new Thread(() -> {
Scanner in = new Scanner(System.in);
System.out.println("输入任意内容,唤醒一个线程:");
in.next();
synchronized (locker) {
locker.notifyAll();
}
});
t1.start();
t2.start();
t3.start();
t4.start();
}
}

如果没有任何对象在wait,凭空调用notify或者notifyAll也不会有任何副作用。