1. 多线程协作概述
1.1 狭义的线程同步
广义的线程同步被定义为一种机制,用于确保两个或多个并发的线程不会同时进入临界区。从该定义来看,线程同步和线程互斥是相同的。
狭义的线程同步在线程互斥的基础上增加了对多个线程执行顺序的要求,即两个或多个并发的线程应按照特定的顺序进入临界区。
可以简单地总结为,狭义的线程同步是一种强调执行顺序的线程互斥,也称为多线程协作。
例如,在多个线程输出1-10案例中,仅要求同一时间仅能有一个线程执行printNum方法,即线程互斥,如果在案例中要求两个线程必须交替打印数字,不能出现一个线程连续打印连个数字的情况,就属于多线程协作的范畴。
1.2 为什么需要多线程协作
在现实生产中,我们经常会遇到多个人分工协作的场景,其中很多场景是强调工作的顺序的。例如,A同学负责编写代码,B同学负责测试代码,C同学负责修改代码中的问题。
在一个程序的运行过程中也会有很多相似的场景,例如在下载软件中,A、B、C三个线程负责分别下载某一段数据,D线程负责周期性的统计这3个线程的下载情况,显示最新下载进度,E线程负责在所有下载任务完成后关闭计算机。
2. 线程同步
2.1 wait、notify和notifyAll
在线程的协作中,一种常用的方式是wait/notify等待通知方式。等待通知方式就是将处于等待状态的线程由其他线程发出通知后重新获取CPU资源,继续执行之前没有执行完的任务。
Java提供了如下3个方法来实现线程之间的消息传递:
- wait():导致当前线程等待,并释放持有的锁;直到其他持有相同锁的线程调用notify()方法或notifyAll()方法来唤醒该线程
- notify():随机唤醒一个在此锁上等待的线程
- notifyAll():唤醒所有在此锁上等待的线程
上述3个方法必须在同步代码块或同步方法中调用,否则会出现IllegalMonitorStateException异常。
等待通知方式主要应用于如下场景:当一个线程获取锁后,发现自己不满足某些条件,不能执行锁住部分的代码,此时需要进入等待列表,直到满足条件时才会重新竞争线程。
2.2 两个线程交替打印数字示例
编写代码,用两个线程交替打印数字:
public class WaitNotifyDemo {
public static void main(String[] args) {
Number number1 = new Number();
Thread t1 = new Thread(number1);
Thread t2 = new Thread(number1);
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}
class Number implements Runnable {
private int number = 1;
@Override
public void run() {
while (true) {
synchronized (this) {
// 唤醒等待池中的一个线程,该线程进入锁池,等待当前线程释放锁
this. notify();
String name = Thread.currentThread().getName();
// 当前线程执行打印操作
if (number <= 10) {
System.out.println( name + "打印" + number);
number++;
} else{
break;
}
try {
// 当前线程进入等待池,并释放持有的锁
this. wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
2.3 等待阻塞状态
当一个线程因wait()方法进入阻塞状态时,该线程处于等待阻塞状态。当一个处于等待阻塞的线程被notify()或notifyAll()方法唤醒时,该线程先进入同步阻塞状态,得到锁后进入可运行状态。
线程状态如下图所示:
3 经典案例
3.1 生产者消费者问题
生产者消费者问题(英语:Producer-consumer problem),也称有限缓冲问题(Bounded-buffer problem),是一个多线程同步问题的经典案例。该问题描述了共享固定大小缓冲区的多个线程-即所谓的“生产者”和“消费者”-在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区空时消耗数据。
要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者。
3.2 生产者消费者问题示例
假设一个餐厅有多名厨师和多名服务员,厨师负责做菜,并将做好的菜放到一个柜台上,服务员负责从柜台上取菜并上菜。当柜台上的菜满时,厨师休息不再做菜;当柜台上没有菜时,服务员休息不再上菜。
请通过多线程编程模拟这一场景,要求:共有3名厨师和3名服务员,柜台上最多能放5个菜。
该案例中用到的类包括:
- Food:用于封装菜品信息的实体类
- SleepUtil:用于实现线程随机睡眠一段时间的工具类
- Cook:用于模拟厨师执行逻辑的类,继承Thread类
- Waiter:用于模拟服务员执行逻辑的类,继承Thread类
- Restaurant:main方法所在的类,用于创建柜台队列、创建和启动子线程
代码示意如下:
public class Food {
private static int counter = 0; // 所有菜共用的计数器
private int i; // 当前菜的编号
public Food() {
// 修改计数器,并给当前菜设置编号
i = ++counter;
}
@Override
public String toString() {
return "第" + i + "个菜";
}
}
import java.util.Random;
import java.util.concurrent.TimeUnit;
/**
* 使当前线程随机休眠一段时间的工具类
*/
public class SleepUtil {
private static Random random = new Random();
public static void randomSleep() {
try {
TimeUnit.MILLISECONDS.sleep(random.nextInt(1000));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
import java.util.Queue;
/**
* 厨师线程类
*/
public class Cook extends Thread{
private Queue<Food> queue; // 窗口队列
public Cook(Queue<Food> queue, String name) {
super(name);
this.queue = queue;
}
@Override
public void run() {
while (true) {
SleepUtil.randomSleep(); // 模拟厨师炒菜时间
Food food = new Food(); // 生产一个菜
System.out.println(getName() + " 生产了" + food);
synchronized (queue) {
while (queue.size() >=5 ) { // 窗口已满
try {
System.out.println("窗口已满,当前有:" + queue.size()
+ "个菜," + getName() + "等待中");
queue.wait(); // 线程等待
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
// 窗口未满
queue.add(food);
// 唤醒所有等待的线程 - 这里主要是服务员
queue.notifyAll();
}
}
}
}
import java.util.Queue;
public class Waiter extends Thread{
private Queue<Food> queue; // 窗口队列
public Waiter(Queue<Food> queue, String name) {
super(name);
this.queue = queue;
}
@Override
public void run() {
while (true) {
Food food;
synchronized (queue) {
while (queue.size() < 1) { // 窗口已空
try {
System.out.println("当前有:" + queue.size() + "个菜,"
+ getName() + "等待中");
queue.wait(); // 线程等待
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
food = queue.remove();
System.out.println(getName() + " 获取到:" + food);
queue.notifyAll();
}
SleepUtil.randomSleep(); //模拟服务员端菜时间
}
}
}
import java.util.LinkedList;
import java.util.Queue;
public class Restaurant {
public static void main(String[] args) {
Queue<Food> queue = new LinkedList<>();
new Cook(queue, "1号厨师").start();
new Cook(queue, "2号厨师").start();
new Cook(queue, "3号厨师").start();
new Waiter(queue, "1号服务员").start();
new Waiter(queue, "2号服务员").start();
new Waiter(queue, "3号服务员").start();
}
}
4 总结
1. 狭义的线程同步在线程互斥的基础上增加了对多个线程执行顺序的要求,即两个或多个并发的线程应按照特定的顺序进入临界区,也称为多线程协作。
2. 在线程的协作中,一种常用的方式是wait/notify等待通知方式。等待通知方式就是将处于等待状态的线程由其他线程发出通知后重新获取CPU资源,继续执行之前没有执行完的任务。Java提供了如下3个方法来实现线程之间的消息传递。
- wait():导致当前线程进入等待阻塞状态,并释放持有的锁
- notify():随机唤醒一个在此锁上等待的线程
- notifyAll():唤醒所有在此锁上等待的线程
3. 生产者消费者问题,也称有限缓冲问题,是一个多线程同步问题的经典案例,该问题描述了共享固定大小缓冲区的多个线程在实际运行时会发生的问题。