线程之间的协作
引言
线程之间可以通过wait()
、notify()
和notifyAll()
等方法,结合“生产者、消费者和队列”等设计模式来相互协作。线程之间协作的基础是任务之间的握手,其基础特性是互斥。
wait()、notify()、notifyAll()
方法介绍:
- 以上三个方法都要获取对象监视器;
wait()
:挂起线程,并放弃当前对象监视器;notify()
:唤醒一个等待‘notify()所在代码持有的对象监视器’的线程;notifyAll()
:唤醒所有等待线程,并让他们竞争对象监视器;
其他方法
shutdown()
:shutdownNow()
:
一.wait()
和notifyAll()
1.1基本特性
当任务遇到运行需要但是自身无法改变的条件时,可以调用wait()
来等待这个条件发生变化,而且只有在notify()、notifyAll()
发生时才会检查条件是否发生了变化。
注意,只有在同步代码块中才能使用wait()
方法(notify()
方法同理),否则编译可以通过但是运行时会抛出IllegalMonitorStateException
。
与sleep()、yield()
不同的时:
- 在
wait()
期间锁是释放的,因此其他方法或代码块可以获得锁并执行; - 可以通过
notify()、notifyAll()
或者指定时间的wait(XX,YY)
方法来从wait()
中恢复执行;
1.2源码示例
两个线程(打蜡-抛光)来给一辆车反复的打蜡-抛光。一共有4各类,汽车类、打蜡类、抛光类和驱动类:
class Car{
private boolean waxOn=false;//刚开始没有打蜡
public synchronized void waxed(){
waxOn=true;//打上蜡
notifyAll();//唤醒所有等待当前对象(this)监视器的线程;
}
public synchronized void buffed(){
waxOn=false;//抛光,此时准备另一个车打蜡抛光;
notifyAll();
}
public synchronized void waitForWaxing() throws InterruptedException {
while(waxOn==false)//没打蜡,等待打蜡
wait();
}
public synchronized void waitForBuffing() throws InterruptedException {
while(waxOn==true)//打完蜡,等待抛光.
wait();
}
}
//打蜡类
class WaxOn implements Runnable{
private Car car;
public WaxOn(Car car) {
this.car = car;
}
@Override
public void run() {
try{
while(!Thread.interrupted()){//测试当前线程是否已经中断
System.out.println("Wax On!");
TimeUnit.MILLISECONDS.sleep(200);
car.waxed();//打蜡
car.waitForBuffing();//等待抛光
}
}catch (InterruptedException e){
System.out.println("exetint via interrupt");
}
System.out.println("退出打蜡任务");
}
}
class WaxOff implements Runnable{
private Car car;
public WaxOff(Car car) {
this.car = car;
}
@Override
public void run() {
try{
while(!Thread.interrupted()){
car.waitForWaxing();//等待打蜡,如果当前car对象被另一个任务成功打蜡,则继续运行;
System.out.println("wax off");
TimeUnit.MILLISECONDS.sleep(200);
car.buffed();//抛光
}
}catch (InterruptedException e){
System.out.println("通过interrupt 退出");
}
System.out.println("抛光任务结束");
}
}
public class WaxOmatic {
public static void main(String[] args) throws InterruptedException {
Car car=new Car();
ExecutorService exec= Executors.newCachedThreadPool();
exec.execute(new WaxOff(car));//抛光
exec.execute(new WaxOn(car));//打蜡
TimeUnit.SECONDS.sleep(5);
// exec.shutdown(); 无法退出;
exec.shutdownNow();
}
}
汽车类只有一个标识是否已经打蜡的变量,程序基本流程如下:
- 抛光线程获取car对象监视器,调用
wait()
方法放弃对象监视器; - 打蜡线程获取car对象监视器,设置打蜡方法将变量设置为“已经打蜡”,然后唤醒等待car对象监视器的线程(notify方法),并在
waitForBuffing()
方法中放弃car对象监视器; - 抛光线程被notify方法唤醒(因为他在等待car对象监视器),并开始抛光————然后将car打蜡状态置为FALSE,继续下一轮循环,car为FALSE状态则被打蜡线程中调用的“等待打蜡”方法感知,打蜡线程开始下一轮循环;
while(condition){ wait(); }
+synchronized
的正确使用方式
thread1:
synchronized(sharedMonitor){
//setup condition for thread2
sharedMonitor.notify();//等待sharedMonitor对象监视器的线程被唤醒;
}
thread2:
while(condition){
//重要X:线程调度器可能再次切换线程;
synchronized(sharedMonitor){
sharedMonitor.wait();
}
}
以上,如果thread2在“重要X”处被切换到thread1,然后thread1将thread2的while判定条件设置为false,并唤醒thread2,此时线程2将毫无意义的进入wait,并可能无限制的挂起来等待信号,从而产生死锁。正确的写法应该防在condition上产生竞争条件:
thread1:
synchronized(sharedMonitor){
//setup condition for thread2
sharedMonitor.notify();//等待sharedMonitor对象监视器的线程被唤醒;
}
thread2:
synchronized(sharedMonitor){
while(condition){
sharedMonitor.wait();
}
}
二.生产者和消费者
生产者是厨师、消费者是服务员,对这个实例建模,有肉、厨师、服务员和驱动类(餐厅)四个类,代码如下:
package concurrency;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* @author 杜艮魁
* @date 2018/1/31
*/
//Meal是Restaurant中未初始化的一个实例,并被其另外两个对象WaitPerson和Chef不断置null和实例化;
class Meal{
private final int orderNum;
public Meal(int orderNum) {
this.orderNum = orderNum;
}
@Override
public String toString() {
return "Meal "+orderNum;
}
}
//应当注意WaitPerson是Restaurant中的一个对象,对象创建方式
// WaitPerson waitPerson=new WaitPerson(this) 调用其构造参数如下,以此来使WaitPerson
//作为Restaurant的变量的同时能够操作Restaurant中的变量:即Meal引用状态和唤醒Chef线程
class WaitPerson implements Runnable{
private Restaurant restaurant;
public WaitPerson(Restaurant restaurant) {
this.restaurant = restaurant;
}
@Override
public void run() {
try{
while(!Thread.interrupted()){
synchronized (this){
//对条件的判定放在锁里边,防止产生竞态条件
while(restaurant.meal==null)//
//加锁对象是this-WaitPerson对象:当厨房没有肉时,服务员等待;
wait();
}
System.out.println("waiter got "+restaurant.meal);
//Meal不为null时Chef类中用this加锁的代码块被挂起。而以下代码将Meal置为null,因此需要唤醒Chef对象,修改meal引用;
synchronized (restaurant.chef){
//服务员已经拿到肉,此时将肉实例设置为空,并唤醒等待上行锁的线程
restaurant.meal=null;
restaurant.chef.notifyAll();//fixme notifyAll()相当于this.notifyAll(),唤醒的是等待当前对象监视器的所有线程而非等待Restaurant对象中Chef对象监视器的线程;
}
}
}catch (InterruptedException e){
System.out.println("waiter interrupted");
}
}
}
//含义同WaitPerson
class Chef implements Runnable{
private int count=0;
Restaurant restaurant;
public Chef(Restaurant restaurant) {
this.restaurant = restaurant;
}
@Override
public void run() {
try{
while(!Thread.interrupted()){
synchronized (this){
while(restaurant.meal!=null)
wait();
}
if(++count==10){
System.out.println("out of food,closing");
restaurant.exec.shutdownNow();//fixme 注意关闭的方法仍然是调用Restaurant中的对象
}
System.out.println("order up!");//要上菜啦!!
synchronized (restaurant.waitPerson){
restaurant.meal=new Meal(count);
//对应tag1处waitPerson发现"Meal"实例不为空时,在synchronized(this-waitPerson)代码块中调用wait——这句代码唤醒wait()的等待
restaurant.waitPerson.notifyAll();
}
TimeUnit.MILLISECONDS.sleep(100);
}
}catch (InterruptedException e){
System.out.println("chef interrupted");
}
}
}
public class Restaurant {
Meal meal;
WaitPerson waitPerson=new WaitPerson(this);
Chef chef=new Chef(this);
ExecutorService exec= Executors.newCachedThreadPool();
public Restaurant() {//创建对象时开始执行这两个线程
exec.execute(chef);
exec.execute(waitPerson);
}
public static void main(String[] args) {
new Restaurant();
}
}
注意代码中的两个TODO:
obj.notifyAll()
和直接使用notifyAll()
的区别是后者唤醒的是等待当前对象this监视器的线程;
三.生产者和消费者和队列
1.1吐司的制作与消费
设想模型:吐司制造、放黄油、加果酱以及被消费。一共有七个类:吐司类、吐司阻塞队列类,吐司生产类、放黄油类、放果酱类和消费类,驱动类。其中:
- 吐司类含有id和当前状态——即是否摸了黄油和果酱等;吐司队列类继承自
LinkedBlockingQueue
,也是一个重要的java类库; - 吐司生产类、放黄油类、放果酱类和消费类都是“任务类”负责通过队列操控生产;
- 示例代码没有显示的同步,因为由
LinkedBlockingQueue
和系统设计隐式的处理了,根据LinkedBlockingQueue
源码可以发现,使用队列消除了显式的使用wait()
造成的类之间的耦合因为每个类都和自己相关的不lockingQueue通信; - 流程图如下:
实例代码:
class ToastQueue extends LinkedBlockingQueue<Toast>{}
/**
* @author 杜艮魁
* @date 2018/1/31
*/
//id、状态,还有将他的状态设置为“添加黄油”、“添加果酱”
class Toast{
public enum Status{DRY,BUTTERED,JAMMED}
private Status status=Status.DRY;//默认状态为DRY
private final int id;
public Toast(int id) {
this.id = id;
}
public void butter(){status=Status.BUTTERED;}
public void jam(){status=Status.JAMMED; }
public Status getStatus() {
return status;
}
public int getId() {
return id;
}
@Override
public String toString() {
return "Toast "+id+": "+status;
}
}
//吐司制造者:用其负责的吐司队列做构造参数
class Toaster implements Runnable{
private ToastQueue toastQueue;
private int count=0;
private Random rand=new Random(47);
public Toaster(ToastQueue toastQueue) {
this.toastQueue = toastQueue;
}
@Override
public void run() {
try{
while(!Thread.interrupted()){
TimeUnit.MILLISECONDS.sleep(100+rand.nextInt(500));
Toast t=new Toast(count++);
System.out.println(t);
toastQueue.put(t);//阻塞插入
}
}catch (InterruptedException e){
System.out.println("Toaster interrupter");
}
System.out.println("Toast off");
}
}
//给吐司抹黄油的类
class Butterer implements Runnable{
private ToastQueue dryQueue,butteredQueue;
public Butterer(ToastQueue dryQueue, ToastQueue butteredQueue) {
this.dryQueue = dryQueue;
this.butteredQueue = butteredQueue;
}
@Override
public void run() {
try{
while(!Thread.interrupted()){
//fixme 取出队头,如果队列为空则调用Lock的‘类wait()"方法
Toast t=dryQueue.take();//阻塞获取
t.butter();
System.out.println(t);
butteredQueue.put(t);
}
}catch(InterruptedException e){
System.out.println("Butter interrupted");
}
}
}
class Jammer implements Runnable{
private ToastQueue butteredQueue,finishedQueue;
public Jammer(ToastQueue butteredQueue, ToastQueue finishedQueue) {
this.butteredQueue = butteredQueue;
this.finishedQueue = finishedQueue;
}
@Override
public void run() {
try{
while(!Thread.interrupted()){
Toast t=butteredQueue.take();
t.jam();
System.out.println(t);
finishedQueue.put(t);
}
}catch (InterruptedException e){
System.out.println("Jam interrupted");
}
System.out.println("jam off");
}
}
class Eater implements Runnable{
private ToastQueue finishedQueue;//即jammed后的吐司
private int counter=0;
public Eater(ToastQueue finishedQueue) {
this.finishedQueue = finishedQueue;
}
@Override
public void run() {
try{
while(!Thread.interrupted()){
Toast t=finishedQueue.take();
if(t.getId()!=counter++||t.getStatus()!=Toast.Status.JAMMED){//如果出队吐司序号不对或者状态不对,则关闭系统;
System.out.println("error: "+t);
System.exit(1);
}else{
System.out.println("chomp! "+t);
}
}
}catch (InterruptedException e){
System.out.println("eater interrupted");
}
System.out.println("Eater off");
}
}
public class ToastOMatic {
public static void main(String[] args) throws InterruptedException {
//fixme 无参构造器是默认存在的,导出类调用父类无参构造器—继而调用含参构造器,从而可以发现队列容量为Integer.MAX_VALUE
ToastQueue dryQueue=new ToastQueue(),
butteredQueue=new ToastQueue(),
finishedQueue=new ToastQueue();
ExecutorService exec= Executors.newCachedThreadPool();
exec.execute(new Toaster(dryQueue));//启动一个“制作吐司”线程来初始化吐司的id和状态并放进dryQueue队列
exec.execute(new Butterer(dryQueue,butteredQueue));//以上,同理
exec.execute(new Jammer(butteredQueue,finishedQueue));
exec.execute(new Eater(finishedQueue));
TimeUnit.SECONDS.sleep(8);
exec.shutdownNow();
}
}
四.管道通信:任务间输入输出
管道中两个重要的类是PipedWriter和PipedReader,分别负责向管道中读写数据:
- PipedWriter:向管道中写数据;
- PipedReader:向管道中读数据;
pipedReader=new PipedReader(pipedWriter)
,每个piperReader读取的管道都要对应一个wirter,可以一对多;
/**
* 线程间通过管道通信:
* 1.PipedWriter:向管道中写数据;
* 2.PipedReader:向管道中读数据;
* .pipedReader=new PipedReader(pipedWriter),每个piperReader读取的管道都要对应一个wirter
*/
class Sender implements Runnable{
private PipedWriter out=new PipedWriter();
public PipedWriter getOut() {
return out;
}
@Override
public void run() {
try{
while(true){
for(char c='A';c<='z';c++){
out.write(c);//fixme 向管道中写数据
TimeUnit.MILLISECONDS.sleep(500);
}
}
}catch (IOException e){
System.out.println(e+" sender write exception");
}catch (InterruptedException e){
System.out.println(e+" sender sleep interrupted");
}
}
}
class Recerver implements Runnable{
private PipedReader in;
public Recerver(Sender sender) throws IOException {//注意参数是Sender类
in=new PipedReader(sender.getOut());//fixme 用PipedWriter创建对应读取管道数据的类;
}
@Override
public void run() {
try{
while(true){
System.out.println("Read: "+(char)in.read()+",");
}
}catch (IOException e){
System.out.println(e+" Receive read exception");
}
}
}
public class PipedIO {
public static void main(String[] args) throws IOException, InterruptedException {
Sender sender=new Sender();
Recerver recerver=new Recerver(sender);
ExecutorService executorService= Executors.newCachedThreadPool();
executorService.execute(sender);
executorService.execute(recerver);
TimeUnit.SECONDS.sleep(4);
executorService.shutdownNow();
}
}
五.其他
Thread.join()
:等待此线程死亡。如果在一个线程中用另一个线程对象调用了join
方法,那么当前线程会等待电泳join方法的线程结束才继续运行;