1. 线程通信
简单案例:
两个线程操作一个初始值为0的变量,实现一个线程对变量增加1,一个线程对变量减少1,交替10轮。
线程间通信模型:
- 生产者+消费者
- 通知等待唤醒机制
多线程编程模板:
线程 操作 资源类
资源类 判断, 干活, 通知
代码实现:
java
//资源类
class Cache {
private int num = 0;
// 生产
public synchronized void produce() {
//这里故意使用if进行判断
if (num != 0) {
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
//干活
num++;
System.out.println(Thread.currentThread().getName() +"生产了一个,现在的数量是:" + num);
//通知
notifyAll();
}
//消费
public synchronized void consume() {
//判断
if (num != 1) {
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
//干活
num--;
System.out.println(Thread.currentThread().getName() +"消费了一个,现在的数量是:" + num);
//通知
notifyAll();
}
}
public class DemoWaitNotify {
public static void main(String[] args) {
Cache cache = new Cache();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
cache.produce();
}
}, "生产者1").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
cache.consume();
}
}, "消费者1").start();
}
}

notify()只随机唤醒一个 正在该对象监视器上wait()的线程,而notifyAll()会唤醒所有在那儿等待的线程(至于谁最终拿到锁继续跑,还是要抢)。
如果换成4个线程会怎样?
java
public class DemoWaitNotify {
public static void main(String[] args) {
Cache cache = new Cache();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
cache.produce();
}
}, "生产者1").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
cache.consume();
}
}, "消费者1").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
cache.produce();
}
}, "生产者2").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
cache.consume();
}
}, "消费者2").start();
}
}
出现了不期望的结果

2. 虚假唤醒
换成4个线程会导致错误,虚假唤醒
- 你如果用的是
if (条件不满足) wait();
那么醒来以后不会再判断一次条件 ,就直接往下跑,所以会出事(尤其是虚假唤醒 、或被notifyAll叫醒但条件仍不满足)。 - 你如果用的是
while (条件不满足) wait();
那么醒来以后会回到while的条件判断处再检查一次 ,不满足就继续wait(),这才是正确姿势。
线程从
wait()返回后从原位置继续执行;为了保证条件仍成立,必须用while重新判断等待条件,不能用if。
解决虚假唤醒:查看API,java.lang.Object的wait方法
https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/Object.html#wait()

if换成while
java
// 1. 判断
while (num != 0) {
this.wait();
}
再次测试,完美解决
3. 线程通信(Condition)
对标synchronized:
并提供了实现案例:

Lock 替代 synchronized;Condition 替代 wait/notify/notifyAll;并且 Condition 让你能有"多个等待队列"。
使用Condition实现线程通信,改造之前的代码:
java
// 多线程编程模版
// 线程操作资源类 , 资源类 判断 ,干活, 通知
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//资源类
class Cache {
private int num = 0; //0 : empty 1: full
private final Lock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition(); //生产者等待/被唤醒的条件队列 。 现在不满了,你可以生产了
private final Condition notEmpty = lock.newCondition(); // 消费者等待/被唤醒的条件队列 现在不空了,你可以消费
// 生产
public void produce() throws InterruptedException {
lock.lock();
//判断
try {
while (num != 0) { //说明有东西, 当前不需要生产
notFull.await();
}
//干活
num++;
System.out.println(Thread.currentThread().getName()+"生产了一个,现在的数量是:" + num);
//通知
notEmpty.signal();
} finally {
lock.unlock();
}
}
//消费
public void consume() throws InterruptedException {
lock.lock();
try {
//判断
while (num != 1) { //说明没有东西可以消费
notEmpty.await();
}
//干活
num--;
System.out.println(Thread.currentThread().getName() +"消费了一个,现在的数量是:" + num);
notFull.signal();
} finally {
//通知
lock.unlock();
}
}
}
public class DemoWaitNotify {
public static void main(String[] args) {
Cache cache = new Cache();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
cache.produce();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "生产者1").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
cache.consume();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "消费者1").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
cache.produce();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "生产者2").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
cache.consume();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "消费者2").start();
}
}
java
condition.signal(); // 唤醒一个等待的线程
condition.signalAll(); //唤醒所有等待的线程
两个条件是为了把"生产者等待"和"消费者等待"拆成两个独立等待队列,从而精准唤醒、减少无效唤醒,并降低死锁/卡死风险。
4. 定制化调用通信
案例:
多线程之间按顺序调用,实现AA->BB->CC。三个线程启动,要求如下:
AA打印5次,BB打印10次,CC打印15次
接着
AA打印5次,BB打印10次,CC打印15次
。。。打印10轮
分析实现方式:
- 有一个锁Lock,3把钥匙Condition
- 有顺序通知(切换线程),需要有标识位
- 判断标志位
- 输出线程名 + 内容
- 修改标识符,通知下一个
内容:
java
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//资源类
class Share {
private final Lock lock = new ReentrantLock();
private final Condition condition1 = lock.newCondition();
private final Condition condition2 = lock.newCondition();
private final Condition condition3 = lock.newCondition();
private Integer flag = 1;
public void print5() throws InterruptedException {
lock.lock();
try {
while (flag != 1) {
condition1.await();
}
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " i= : " + i);
}
flag = 2;
condition2.signalAll();
} finally {
lock.unlock();
}
}
public void print10() throws InterruptedException {
lock.lock();
try {
while (flag != 2) {
condition2.await();
}
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " i= : " + i);
}
flag = 3;
condition3.signalAll();
} finally {
lock.unlock();
}
}
public void print15() throws InterruptedException {
lock.lock();
try {
while (flag != 3) {
condition3.await();
}
for (int i = 0; i < 15; i++) {
System.out.println(Thread.currentThread().getName() + " i= : " + i);
}
flag = 1;
condition1.signalAll();
} finally {
lock.unlock();
}
}
}
public class DemoCondition {
public static void main(String[] args) {
Share share = new Share();
new Thread(() -> {
//都是10轮
for (int i = 0; i < 10; i++) {
try {
share.print5();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A").start();
new Thread(() -> {
//都是10轮
for (int i = 0; i < 10; i++) {
try {
share.print10();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
new Thread(() -> {
//都是10轮
for (int i = 0; i < 10; i++) {
try {
share.print15();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "C").start();
}
}
在Java中,一个ReentrantLock可以与多个Condition对象一起使用,每个Condition对象可以用于不同的线程协调和通信场景,以便更精细地控制多线程之间的执行顺序和互斥访问。
为什么要多个 Condition?
因为如果只有一个
Condition,你signal()唤醒的可能是"错误类型"的线程,醒来发现条件不满足又得继续睡,导致大量无效唤醒、逻辑更绕、性能更差。对比一下:
synchronized + wait/notify:基本只有一个隐式等待队列(this的 monitor wait-set)ReentrantLock + Condition:你可以创建多个等待队列,精细控制唤醒谁
Condition.await()/signal()前 必须先lock.lock(),并且通常写在try/finally里释放锁。
await()通常要放在while循环里检查条件,而不是if,因为可能有"虚假唤醒"(spurious wakeup)。
为什么要用 final关键字 表示lock这个引用一旦指向某个ReentrantLock,以后就不能再指向别的锁对象。同理
condition1/2/3也固定绑定到这把锁创建出来的那几个 Condition。因为
Condition必须 和创建它的那把锁配套使用:如果你不小心把
lock或某个condition重新赋值成别的对象,代码会变得非常危险(甚至直接抛异常,比如没持有正确的锁就await/signal会IllegalMonitorStateException)。依赖注入/锁/条件队列这种基础设施通常都应该是 final
5.小练习
**面试题:**两个线程,一个线程打印1-52,另一个打印字母A-Z打印顺序为12A34B...5152Z,要求用线程间通信
java
//**面试题:**两个线程,一个线程打印1-52,
// 另一个打印字母A-Z打印顺序为12A34B...5152Z,要求用线程间通信
//线程操作资源类, 资源类, 判断, 干活 ,通知
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//资源类
class MyCache {
int flag = 0; // 0 数字, 1 字母
private final Lock lock = new ReentrantLock();
private final Condition numTurn = lock.newCondition();
private final Condition charTurn = lock.newCondition();
public void print1To52() throws InterruptedException {
int n = 1;
lock.lock();
try {
for (int i = 0; i < 26; i++) {
while (flag != 0) {
numTurn.await();
}
System.out.print(n++);
System.out.print(n++);
flag = 1;
charTurn.signal();
}
} finally {
lock.unlock();
}
}
public void printAtoZ() throws InterruptedException {
lock.lock();
char n = 'A';
try {
for (int i = 0; i < 26; i++) {
while (flag != 1) {
charTurn.await();
}
System.out.print(n++);
flag = 0;
numTurn.signal();
}
} finally {
lock.unlock();
}
}
}
public class Practice {
public static void main(String[] args) {
MyCache myCache = new MyCache();
new Thread(() -> {
try {
myCache.print1To52();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
myCache.printAtoZ();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}