JavaEE初阶——多线程(4)线程安全
本文介绍了Java多线程中的volatile关键字以及wait/notify机制。volatile通过内存屏障保证内存可见性和有序性,但不保证原子性;通过MESI协议和缓存同步机制确保线程能感知变量修改。wait/notify用于线程间协调,必须在synchronized代码块中使用,wait使线程等待并释放锁,notify唤醒等待线程。notifyAll可唤醒所有等待线程。这些机制解决了线程间的
目录
一、volatile关键字
我们在之前的文章已经介绍了synchronized关键字用来保证“原子性”和间接保证“内存可见性”
1.1 volatile保证内存可见性
创建两个线程,t1线程用flag==0为while循环的条件,t2改变flag的值,我们希望观察到的现象是当t2改变flag值之后t1和t2线程都结束
public class Demo_502 {
// 定义退出标识
static int flag = 0;
public static void main(String[] args) {
// 创建执行任务的线程
Thread t1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "线程启动.");
while (flag == 0) {
// 不停的循环. 处理任务
}
System.out.println(Thread.currentThread().getName() + "线程退出.");
}, "t1");
// 启动线程
t1.start();
// 输入停止标识
Thread t2 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "线程启动.");
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个非零的整数:");
flag = scanner.nextInt();
System.out.println(Thread.currentThread().getName() + "线程退出.");
}, "t2");
// 线程启动
t2.start();
}
}

结果显示,当t2改变了flag的值为1后,t1并没有感知到flag已经不为0,仍在循环,未退出线程,与我们的预想不一样,出现了线程不安全现象!这是因为内存不可见,t1线程没有感知到t2线程对变量的修改。
我们在指令层面分析一下t1线程的循环判断逻辑while(flag==0)
这条语句对应的指令是
LOAD:把flag的值从主内存加载到t1的工作内存
CMP:比较flag的值和0是否相等
但是对于t1来说,判断逻辑是比较flag的值,而不是像flag++这样的修改操作。所以之后的比较,CPU都会认为这个值永远不会改变,从而也不会从主内存中读取flag,自然也不会得到修改后的flag值。
public class Demo_502 {
// 定义退出标识
static volatile int flag = 0;
public static void main(String[] args) {
// 创建执行任务的线程
Thread t1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "线程启动.");
while (flag == 0) {
// 不停的循环. 处理任务
}
System.out.println(Thread.currentThread().getName() + "线程退出.");
}, "t1");
// 启动线程
t1.start();
// 输入停止标识
Thread t2 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "线程启动.");
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个非零的整数:");
flag = scanner.nextInt();
System.out.println(Thread.currentThread().getName() + "线程退出.");
}, "t2");
// 线程
t2.start();
}
}
我们在flag前加上volatile关键词,此时再来观察运行结果,发现t1按预期退出了,这说明t1感知到了flag的更新
那volatile是如何实现这样的结果呢?
1.2 CPU层面
在CPU中,在缓存和主内存之间存在“MESI缓存一致性协议”,我们可以简单理解为一种通知机制

- 当所有处理器没有修改共享变量时,各自处理器只读取自己缓存中的值,而不用去主内存读取,从而提高效率
- 当某一个处理器往主内存中写回数据的时候,缓存中的值就会失效,通知其他处理器从主内存中重新获取新的值
这样就保证一个线程可以感知到另一个线程对共享变量的修改,从而保证内存可见性
1.3 JAVA层面
在Java中存在内存屏障,可以保证指令执行的先后顺序

我们以下面两个例子讲解内存屏障如何保证内存可见性

当加了volatile关键字的变量要进行修改时(volatile写)
- StoreStore屏障会使之前的普通写的结果“提前暴露”出来,然后再进行volatile写操作,避免其他线程读到普通写的过期数据
- StoreLoad屏障则会强制让后续普通读的操作等到volatile写的结果同步到主内存之后再进行

我们可以加多个内存屏障,当加了volatile关键字的变量要进行获取内存的值时(volatile读)
- StoreLoad屏障会等待其他线程的写操作结果同步到内存之后再进行读取,避免读到过期数据
- LoadLoad可以避免后续的普通读操作读到过期数据
- LoadStore保证后续的写操作依赖的是volatile读的最新结果
所以内存屏障通过强制缓存同步,让线程感知到变量的改变,从而保证内存可见性
1.4 volatile避免指令重排序
还是同样依靠我们上文提到的内存屏障,规定volatile读写操作和其他读写操作的顺序,比如StoreLoad屏障保证必须进行写操作之后才可以进行读操作,有效的避免了指令的重排序
1.5 volatile总结
- volatile保证了内存可见性
- volatile实现了有序性避免了指令重排序
- volatile不保证原子性
- 多个线程之间涉及到修改共享变量的逻辑就只管加volatile
- 因为不保证原子性,往往会和synchronized搭配使用
二、wait和notify
之前提到,线程之间是抢占性执行,所以多线程情况下线程执行的先后顺序难以预知。但是在实际开发中,我们有时候明确需要等待一个操作结束之后再执行另一个操作,所以我们需要协调线程之间的执行顺序
例如:
- 篮球场每个球员都是线程,想要得分就要不同线程配合,扣篮的线程要等待传球的线程
- 包子铺的顾客和老板是线程,买包子这个线程必须等到老板包好包子并且出锅这个线程
wait方法为等待线程,notify方法则为唤醒线程,我们还可以和之前提到的join()方法进行对比区分
我们还是以买包子为例:妈妈让我下楼买包子,我等包子出锅
join()方法属于Thread类,一般是针对主线程对子线程的等待:妈妈让我下楼买包子,妈妈发出的命令就是主线程,而我下楼买包子就是子线程。妈妈需要等我买包子这个线程运行结束才去做其他事情
wait()方法属于Object类,一般就不分主次线程之分:我买包子和包子出锅两个线程更适合抽象于对资源的等待,而不是对线程结束的等待。
- 比如包子出锅这个线程会从6点一直运行到10点才结束,我在8点等待包子这个资源
- 当包子没做好我就等待(判断是否有资源,没有就调用wait方法等待资源)
- 等到8点10分包子出锅我买到了(当资源充足了,调用notify方法,唤醒了刚刚调用wait方法的线程,我可以买包子了)
- 那么我买包子线程就结束了,而不是要一直等到包包子线程10点结束买包子的线程才结束。
wait方法和notify方法都一定要搭配synchronized一起使用
2.1 wait方法
调用wait的结果
- 使当前执行代码的线程进行等待,也就是把当前线程放到等待队列
- 释放当前锁
- 满足一定条件之后被唤醒,重新尝试获取这个锁
wait结束等待的条件
- 其他线程调用该对象的notify方法
- wait等待时间超时(wait方法可以设定等待时间)
- 其他线程调用该等待线程的interrupted方法,导致wait抛出异常
2.2 notify方法
notify用来唤醒处于等待状态的线程
- 方法notify也要和synchronized结合使用,用来唤醒持有相同锁对象的等待线程,并使他们重新获取该对象的对象锁
- 如果有多个线程等待,则有线程调度器随机挑选出一个wait状态的线程
- 在notify方法之后,当前线程不会马上释放对象锁,要等到notify方法所在的的线程执行完全,也就是退出同步代码块之后才会释放对象锁
public class Demo_503 {
public static void main(String[] args) {
// 定义一个锁对象
Object locker = new Object();
// 创建调用wait() 的线程
Thread t1 = new Thread(() -> {
while (true) {
synchronized (locker) {
System.out.println("调用wait()之前...");
// 执行线程的逻辑
// 如果没有满足线程所需要的数据,那么就等待
try {
locker.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("wait()唤醒之后..");
System.out.println("===================");
}
}
});
Thread t2 = new Thread(() -> {
while (true) {
System.out.println("notify()之前...");
// 同等待的锁对象进行唤醒
synchronized (locker) {
locker.notify();
}
System.out.println("notify()之后...");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 启动线程
t1.start();
t2.start();
}
}

我们能看到打印结果,调用wait之后t1进入等待状态,此时t2唤醒之后t1再继续执行
2.3 notifyAll方法
notify方法只是唤醒等待线程中的一个,而notifyAll方法可以一次性唤醒所有的等待线程
public class Demo_05 {
static class WaitTask implements Runnable {
private Object locker;
public WaitTask(Object locker) {
this.locker = locker;
}
@Override
public 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;
}
@Override
public 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 t3 = new Thread(new WaitTask(locker));
Thread t4 = new Thread(new WaitTask(locker));
Thread t2 = new Thread(new NotifyTask(locker));
t1.start();
t3.start();
t4.start();
Thread.sleep(1000);
t2.start();
}
}

我们使用notify唤醒线程,观察结果notify结束后能看到只有一个线程被唤醒后打印wait结束,继续循环打印wait开始继续等待
public class Demo_05 {
static class WaitTask implements Runnable {
private Object locker;
public WaitTask(Object locker) {
this.locker = locker;
}
@Override
public 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;
}
@Override
public void run() {
synchronized (locker) {
System.out.println("notifyAll 开始");
locker.notifyAll();
System.out.println("notifyAll 结束");
}
}
}
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Thread t1 = new Thread(new WaitTask(locker));
Thread t3 = new Thread(new WaitTask(locker));
Thread t4 = new Thread(new WaitTask(locker));
Thread t2 = new Thread(new NotifyTask(locker));
t1.start();
t3.start();
t4.start();
Thread.sleep(1000);
t2.start();
}
}

我们把notify替换成notifyAll,观察结果显示notifyAll调用后,三个等待线程都被唤醒,之后这三个线程重新竞争锁
2.4 总结
wait和notify必须配置synchronized一起使用,并且使用同一个锁对象
public class Demo_502 {
public static void main(String[] args) throws InterruptedException {
Object locker=new Object();
synchronized(locker){
locker.wait();
}
}
}
public class Demo_502 {
public static void main(String[] args) throws InterruptedException {
Object locker=new Object();
synchronized(locker){
locker.notify();
}
}
}
synchronized获取的锁对象,和调用wait和notify方法的对象要相同
更多推荐


所有评论(0)