1 线程间通信
线程间通信的模型有两种:共享内存和消息传递,以下方式都是基本这两种模型来实现的。我们来基本一道面试常见的题目来分析
场景:两个线程,一个线程对当前数值加 1,另一个线程对当前数值减 1,要求用线程间通信
1.1 synchronized方法
// 第一步 创建资源类,定义属性和操作方法
class Share{//初始值private int number = 0;// +1的方法public synchronized void incr() throws InterruptedException {// 判断while(number != 0) {this.wait();}// 干活number++;System.out.println(Thread.currentThread().getName() + " :: " + number);//通知其他线程this.notifyAll();}// -1的方法public synchronized void decr() throws InterruptedException {// 判断while(number != 1) {this.wait();}// 干活number--;System.out.println(Thread.currentThread().getName() + " :: " + number);//通知其他线程this.notifyAll();}}
public class ThreadDemo1 {// 第三步创建多个线程,调用资源类的操作方法public static void main(String[] args) {Share share = new Share();// 创建线程new Thread(()->{for (int i = 0; i < 10; i++) {try {share.incr();//+1} catch (InterruptedException e) {throw new RuntimeException(e);}}},"AA").start();new Thread(()->{for (int i = 0; i < 10; i++) {try {share.decr(); //-1} catch (InterruptedException e) {throw new RuntimeException(e);}}},"BB").start();new Thread(()->{for (int i = 0; i < 10; i++) {try {share.incr();//+1} catch (InterruptedException e) {throw new RuntimeException(e);}}},"CC").start();new Thread(()->{for (int i = 0; i < 10; i++) {try {share.decr(); //-1} catch (InterruptedException e) {throw new RuntimeException(e);}}},"DD").start();}
}
1.2 Lock方案
// 第一步 创建资源类,定义属性和操作方法
class Share{//初始值private int number = 0;//创建Lockprivate Lock lock = new ReentrantLock();private Condition condition = lock.newCondition();// +1public void incr() throws InterruptedException {//上锁lock.lock();try {//判断while (number != 0){condition.await();}//干活number++;System.out.println(Thread.currentThread().getName() + " :: " + number);//通知condition.signalAll();}finally {// 解锁lock.unlock();}}// -1public void decr() throws InterruptedException {//上锁lock.lock();try {//判断while (number != 1){condition.await();}//干活number--;System.out.println(Thread.currentThread().getName() + " :: " + number);//通知condition.signalAll();}finally {// 解锁lock.unlock();}}
}
public class ThreadDemo2 {// 第三步创建多个线程,调用资源类的操作方法public static void main(String[] args) {Share share = new Share();new Thread(()->{for (int i = 0; i < 10; i++) {try {share.incr();} catch (InterruptedException e) {throw new RuntimeException(e);}}},"AA").start();new Thread(()->{for (int i = 0; i < 10; i++) {try {share.decr();} catch (InterruptedException e) {throw new RuntimeException(e);}}},"BB").start();new Thread(()->{for (int i = 0; i < 10; i++) {try {share.incr();} catch (InterruptedException e) {throw new RuntimeException(e);}}},"CC").start();new Thread(()->{for (int i = 0; i < 10; i++) {try {share.decr();} catch (InterruptedException e) {throw new RuntimeException(e);}}},"DD").start();}
}
1.3 虚假唤醒问题
在Java中,线程间通信通常使用 Object
类的 wait()
、notify()
和 notifyAll()
方法来实现。这些方法与条件变量类似,但同样可能会出现虚假唤醒(Spurious Wakeup)的问题。
1.3.1 什么是虚假唤醒?
虚假唤醒是指一个线程在没有被显式通知的情况下被唤醒。换句话说,即使没有调用 notify()
或 notifyAll()
方法,等待的线程也可能被唤醒。这种现象在某些操作系统或线程库中是允许的,因为它可以简化某些实现。
1.3.2 为什么会出现虚假唤醒?
虚假唤醒的原因可能包括:
- 操作系统调度:操作系统可能在某些情况下唤醒线程,即使没有显式的通知。
- 多核处理器:在多核处理器上,线程可能在不同的核心上运行,导致某些同步机制不完全可靠。
- Java 实现:Java 的线程库实现可能会允许虚假唤醒,以提高性能或简化实现。
1.3.3 如何处理虚假唤醒?
为了避免虚假唤醒带来的问题,通常的做法是在循环中检查条件变量。这样,即使线程被虚假唤醒,它也会在循环中重新检查条件,如果条件不满足,线程会继续等待。
以下是一个使用 wait()
和 notifyAll()
的典型示例:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class SpuriousWakeupExample {private static final Lock lock = new ReentrantLock();private static final Condition condition = lock.newCondition();private static boolean ready = false;public static void main(String[] args) throws InterruptedException {Thread workerThread = new Thread(new Worker());workerThread.start();// 主线程设置条件并通知Thread.sleep(1000); // 模拟一些工作lock.lock();try {ready = true;condition.signalAll(); // 通知等待的线程} finally {lock.unlock();}workerThread.join();}static class Worker implements Runnable {@Overridepublic void run() {lock.lock();try {// 在循环中检查条件while (!ready) {condition.await(); // 等待条件满足}// 条件满足,执行工作System.out.println("Worker thread is processing data");} catch (InterruptedException e) {Thread.currentThread().interrupt();} finally {lock.unlock();}}}
}
1.3.4 关键点
- 循环检查条件:在
await()
方法周围使用while
循环来检查条件,而不是if
语句。这样可以确保即使线程被虚假唤醒,它也会重新检查条件。 - 使用
Lock
和Condition
:在示例中使用了ReentrantLock
和Condition
,这是 Java 中更灵活的同步机制。你也可以使用synchronized
关键字和Object
的wait()
、notify()
和notifyAll()
方法,但原理是相同的。 - 处理中断:在
await()
方法中捕获InterruptedException
,并处理线程中断的情况。
通过这种方式,可以有效避免虚假唤醒带来的问题,确保线程在条件真正满足时才继续执行。
1.4 多线程编程步骤
-
第一步创建资源类,在资源类创建属性和操作方法
-
第二步在资源类操作方法
- 判断
- 干活
- 通知
-
第三步创建多个线程,调用资源类的操作方法
-
第四步防止虚假唤醒问题
2 线程间定制化通信
让线程按照指定顺序进行通信。
2.1 案例介绍
启动三个线程,按照如下要求执行:
AA线程打印 5 次 A,BB 线程打印 10 次 B,CC 线程打印 15 次 C,按照此顺序循环 10 轮
2.2 流程分析
2.3 代码实现
// 第一步 创建资源类,定义属性和操作方法
class ShareResourse{// 定义标识位private int flag = 1; // 1:AA; 2:BB; 3:CC//创建Lockprivate Lock lock = new ReentrantLock();//创建三个conditionprivate Condition c1 = lock.newCondition();private Condition c2 = lock.newCondition();private Condition c3 = lock.newCondition();//打印5次,参数第几轮public void print5(int loop) throws InterruptedException {//上锁lock.lock();try{while (flag != 1){//等待c1.await();}//干活for (int i = 0; i <= 5; i++) {System.out.println(Thread.currentThread().getName() + " :: " + i +", 轮数:"+ loop);}//通知flag = 2; //修改标识位2c2.signal();//通知BB线程}finally {// 释放锁lock.unlock();}}//打印10次,参数第几轮public void print10(int loop) throws InterruptedException {//上锁lock.lock();try{while (flag != 2){//等待c2.await();}//干活for (int i = 0; i <= 10; i++) {System.out.println(Thread.currentThread().getName() + " :: " + i +", 轮数:"+ loop);}//通知flag = 3; //修改标识位3c3.signal();//通知CC线程}finally {// 释放锁lock.unlock();}}//打印15次,参数第几轮public void print15(int loop) throws InterruptedException {//上锁lock.lock();try{while (flag != 3){//等待c3.await();}//干活for (int i = 0; i <= 15; i++) {System.out.println(Thread.currentThread().getName() + " :: " + i +", 轮数:"+ loop);}//通知flag = 1; //修改标识位1c1.signal();//通知AA线程}finally {// 释放锁lock.unlock();}}
}
public class ThreadDemo3 {// 第三步创建多个线程,调用资源类的操作方法public static void main(String[] args) {ShareResourse shareResourse = new ShareResourse();new Thread(()->{for (int i = 0; i <= 10; i++) {try {shareResourse.print5(i);} catch (InterruptedException e) {throw new RuntimeException(e);}}},"AA").start();new Thread(()->{for (int i = 0; i <= 10; i++) {try {shareResourse.print10(i);} catch (InterruptedException e) {throw new RuntimeException(e);}}},"BB").start();new Thread(()->{for (int i = 0; i <= 10; i++) {try {shareResourse.print15(i);} catch (InterruptedException e) {throw new RuntimeException(e);}}},"CC").start();}
}
synchronized实现同步的基础:Java中的每一个对象都可以作为锁。具体表现为以下3种形式。
对于普通同步方法,锁是当前实例对象。
对于静态同步方法,锁是当前类的class对象。
对于同步方法块,锁是synchonized括号里配置的对象
3 思维导图
4 参考链接
【【尚硅谷】大厂必备技术之JUC并发编程】