1. volatile 关键字
volatile 修饰的变量,能够保证 “内存可见性”,用于处理内存可见性引起的线程安全问题
如下代码:
import java.util.Scanner;
public class Demo18 {
private static int n = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (n == 0) {
//啥都不写
}
System.out.println("t1 线程结束循环");
});
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个整数:");
n = scanner.nextInt();
});
t1.start();
t2.start();
}
}
分析:
按照逻辑,输入非 0 值,n 就变了,t1 中的循环条件不成立,t1 应该结束
但实际上并没有结束,通过 jconsole 看到 t1 线程(Thread-0)仍然是持续工作的
这中情况就是内存可见性引起的线程安全问题,同时也是 bug
内存可见性问题出现原因:
上述 while 循环会循环非常多次,每次循环都要执行一个 n == 0 的判定,其分为以下两步:
1) 从内存中读取数据到寄存器(和 操作2 相比,这个操作的速度非常慢)
2) 通过类似于 cmp 指令比较寄存器和 0 值(和 操作1 相比,这个操作的速度非常块)
当 JVM 执行这个代码时,发现每次循环的过程中,执行 操作1 的开销非常大,而且每次执行 操作1 结果都是一样的,并且 JVM 并不会意识到用户可能会在未来修改 n,于是 JVM 就直接把 操作1 给优化掉了(每次循环不会重新读取内存中的数据,而是直接读取寄存器/cache 中的数据(缓存的结果)
当 JVM 做出上述优化后,循环的开销大幅度降低了,但是当用户修改 n 的值时,内存中的 n 改变了,但是由于 t1 线程每次循环不会去读取内存,感知不到 n 的变化,内存中的 n 对于线程 t1 来说是 “不可见的”,这就引起了 bug(内存可见性问题)
解决方案:
1) 加入 sleep
此处即使 sleep 的时间非常短,但是内存可见性问题消失了,t2 的修改可以被 t1 感知到
说明加入 sleep 之后,JVM 就不针对读取内存数据进行优化了,这是因为和读取内存相比,sleep 开销是更大的,远远超过了读取内存,就算把读取内存操作优化掉也没有意义
2) 加入 volatile 关键字
JVM 进行上述优化的前提,是其认为针对这个变量 n 的频繁读取结果都是固定的
volatile 关键字修饰一个变量,提示 JVM 说:这个变量是 “易变” 的,此时 JVM 就会禁止上述的优化,确保每次循环都是从内存中重新读取数据
引入 volatile 的时候,编译器生成这个代码的时候,就会给这个变量的读取操作附近生成一些特殊的指令,称为 “内存屏障”,后续 JVM 执行到这些特殊的指令就知道不能进行上述优化了
tip:volatile 只是解决内存可见性问题,不能解决原子性问题,如果两个线程对同一个变量进行修改(count++),volatile 就无能为力了
2. wait 和 notify
由于线程之间是抢占式执行,因此线程之间执行的顺序难以预知,但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序,而完成这个协调工作,主要涉及到三个方法:
wait() / wait(long timeout):让当前线程进入等待状态
notify():唤醒在当前对象上的等待线程
notifyAll():唤醒在当前对象上所有的等待线程
tip:上面三个方法都是 Object 类的方法
2.1 wait() 方法(可解决 “线程饿死” 问题)
wait 做的事情:
- 使当前执行代码的线程进行等待(把线程放到等待队列中)
- 释放当前的锁
- 满足一定条件时被唤醒,重新尝试获取这个锁
- tip:wait 要搭配 synchronized 来使用,否则 wait 会直接抛出异常
wait 结束等待的条件:
- 其他线程调用该对象的 notify 方法
- wait 等待时间超时(wait 方法提供一个带有 timeout 参数的版本,来指定等待时间)
- 其他线程调用该等待线程的 interrupted 方法,导致 wait 抛出 InterruptedException 异常
代码示例:
public class Demo20 {
public static void main(String[] args) throws InterruptedException {
Object obj = new Object();
System.out.println("wait 之前");
synchronized (obj) {
obj.wait();
}
System.out.println("wait 之后");
}
}
运行结果:
tip:
这样在执行到 obj.wait() 之后就会一直阻塞等待下去,若要唤醒,需要用到 notify() 方法
wait 和 notify 都是 Object 提供的方法,任意的 Object 对象都可以用来 wait 和 notify
wait 一共做了三件事:
1) 释放锁
2) 进入阻塞等待,准备接收通知
3) 收到通知后唤醒,并且重新尝试获取锁
wait 默认是 “死等”,它也提供带参数的版本,可以指定超时时间,当其到达超时时间后,即使没有 notify 也不会继续等待了,而是继续执行
2.2 notify()
notify() 方法是唤醒等待的线程
- 方法 notify() 也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其他线程,对其发出通知 notify,并使他们重新获取该对象的对象锁
- 如果有多个线程等待,则由线程调度器随机挑选一个呈 wait 状态的线程(并没有“先来后到”的规则)
- 在 notify() 之后,当前线程不会马上释放该对象锁,要等到执行 notify() 的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁
import java.util.Scanner;
public class Demo21 {
private static Object locker = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (locker) {
System.out.println("t1 wait 之前");
try {
locker.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("t1 wait 之后");
}
});
Thread t2 = new Thread(() -> {
System.out.println("t2 notify 之前");
Scanner scanner = new Scanner(System.in);
scanner.next();//此处输入什么都无妨,主要是通过这个 next 构造阻塞
synchronized (locker) {
locker.notify();
}
System.out.println("t2 notify 之后");
});
t1.start();
t2.start();
}
}
运行结果:
tip:
1) 在 notify 中也需要确保先加锁才能执行
2) 若 notify 通知的时候,没有线程在 wait 没有任何副作用
3) notifyAll 方法可以一次呼唤醒所有等待的线程,大部分场景下还是使用 notify 方法一个一个的唤醒的,否则唤醒所有线程,就会无序的竞争锁,与我们一开始想要控制唤醒顺序的初心不符了
2.3 wait 和 sleep 的区别
使用 wait 的目的是为了提前唤醒;sleep 就是固定时间的阻塞,不涉及唤醒操作(虽然 sleep 可以被 Interrupt 唤醒,但是 Interrupt 操作表示的意思不是 “唤醒”,而是要终止线程了
wait 必须搭配 synchronized 使用,并且 wait 会先释放锁,同时进行等待;sleep 和锁无关,如果不加锁,sleep 可以正常使用,若是加了锁,sleep 操作不会释放锁,而是 “抱着锁” 一起睡,其他线程无法拿到锁
2.4 创建三个线程,循环顺序打印10次 ABC
通过 wait、notify 约定线程的打印顺序
public class Demo23 {
private static Object locker1 = new Object();
private static Object locker2 = new Object();
private static Object locker3 = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.print("A");
synchronized (locker1) {
locker1.notify();
}
synchronized (locker3) {
try {
locker3.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
synchronized (locker1) {
try {
locker1.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.print("B");
synchronized (locker2) {
locker2.notify();
}
}
});
Thread t3 = new Thread(() -> {
for (int i = 0; i < 10; i++) {
synchronized (locker2) {
try {
locker2.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("C");
synchronized (locker3) {
locker3.notify();
}
}
});
t1.start();
t2.start();
t3.start();
}
}