Java进阶——多线程之生产者-消费者模型小结

引言

上一篇文章Java进阶——多线程之线程间的通信、同步、等待唤醒机制小结末尾我们留两个问题,这篇文章我们将好好分析出现那个问题的原因和解决之道。

一、Thread一些重要的操作和方法

1、停止线程

如何停止线程?只有一种方法,就是通过标记,让线程中的run方法自然结束。
当涉及到同步操作时,使用标记可能不能使得线程停止,因为此时可能线程前面已经进入冻结态,那么需要主动调用interrupt方法强制让其停止。

2、守护线程

守护线程可以理解成是“后台”线程(与我们常见的前台线程唯一区别就是会随着它“守护”的线程自动结束),必须在启动线程前调用通过线程对象调用setDaemon方法可以把该线程设置为当前运行的线程的守护线程(比如说在main线程里创建了子线程t1,并在main线程里t1.setDaemon(true),则t1是main的守护线程,当main线程运行结束之后t1线程也随之自动结束),当虚拟机此时运行的全是守护线程时,JVM会自动呢退出。所谓“守护”其实更多的像是依赖关系,守护线程依赖于其守护的线程,如果其守护的线程依然运行,那么守护线程与普通线程没啥区别,公平抢夺CPU执行权,但是其守护的线程一旦结束,它也不会独活,它也会自动结束。

3、Join方法

Join方法是申请加入到运行中来,相当于是主动抢夺CPU执行权,假如在主线程中创建了一个线程t2并启动,再执行t2.join()方法,当执行到这句时,此时main线程拥有CPU执行权,但是执行了join方法之后相当于是t2 要向main抢夺CPU执行权,main线程进入冻结状态,t2获得CPU执行权,直到t2执行完毕之后,main线程才会重新进入运行态。换言之,当线程t 执行到了线程t3的join方法时,线程t就会等待直到t3 运行结束,t才会重新进入运行态

4、yield方法

暂停当前执行的线程,并执行其他线程,执行Thread.yield()方法的线程就会主动放弃当前的CPU执行权,让给其他线程争夺,可以稍微减缓某一个长时间占用CPU执行权。

二、生产者-消费者模型概述

生产者-消费者模式是一个十分经典的多线程并发协作的模式,所谓生产者-消费者问题,实际上主要是包含了两类线程:一类是生产者线程用于生产数据;另一类是消费者线程用于消费数据。目的是为了解耦生产者和消费者,通常会采用共享的数据区域(就像是一个仓库),生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为;而消费者只需要从共享数据区中去获取数据,就不再需要关心生产者的行为。通常这个共享数据区域应该满足的线程间并发协作的条件:

  • 如果共享数据区已满的话,阻塞生产者继续生产数据放置入内;
  • 如果共享数据区为空的话,阻塞消费者继续消费数据;

而在实现生产者消费者模式时,目前可以采用三种方式:

  • 使用Object的等待-唤醒(wait/notify)的消息通知机制;
  • 使用Lock-Condition的await/signal的消息通知机制;
  • 使用BlockingQueue各种阻塞队列实现。

三、使用wait-notify等待唤醒机制实现生产者-消费者模型

public class ProduceConsumeModel {

	public static void main(String[] args) {
		Product res = new Product();
		new Thread(new ProduceThread(res)).start();
		new Thread(new ConsumeThread(res)).start();
		new Thread(new ProduceThread(res)).start();
		new Thread(new ConsumeThread(res)).start();
	}
}

class Product {
	private String name;
	private int number = 1;
	private boolean flag = false;

	public synchronized void produce(String name) {
		if (this.flag) {
			try {
				// 表示调用这个方法后,会使得此时持有这个对象锁的线程wait
				this.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		this.name = name + "--" + number++;
		System.out.println(Thread.currentThread().getName() + "...生产:"
				+ this.name);
		flag = true;
		this.notify();
	}

	public synchronized void consume() {
		if (!this.flag) {
			try {
				// 表示调用这个方法后,会使得此时持有这个对象锁的线程wait
				this.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		System.out.println(Thread.currentThread().getName() + "******消费:"
				+ this.name);
		flag = false;
		this.notify();
	}
}

// 生产者
class ProduceThread implements Runnable {
	private Product res;// 要操作的资源对象

	public ProduceThread(Product res) {
		this.res = res;
	}

	public void run() {
		while (true && !Thread.interrupted()) {
			res.produce("产品");
		}
	}
}

// 消费者
class ConsumeThread implements Runnable {
	private Product res;// 要操作的资源对象

	public ConsumeThread(Product res) {
		this.res = res;
	}

	public void run() {
		while (true && !Thread.interrupted()) {
			res.consume();
		}
	}
}

多个线程运行之后的执行结果可能为
在这里插入图片描述
从上图部分运行结果我们可以看到,由于同步是可以嵌套的,在多个线程的生产-消费者模型执行时会导致线程不安全。具体原因是什么呢?我们跟着代码逻辑逐步分析下:现在我们创建了t1、t2两个生产者线程和t3、t4两个消费者线程,都是在主线程中依次启动(互相争夺CPU执行权),假设t1 首先得到CPU执行权,执行produce方法,首先判断flag为false,直接给产品赋值,flag设置为true,最后调用notify方法,假如此时t1依然占有CPU执行权,则再次执行produce方法,flag为true 则执行wait方法,主动放弃CPU执行权进入冻结状态(此时produce方法还未执行完毕,虚拟机会记录当前运行的位置),这时候t2、t3、t4抢夺CPU执行权,假设t2抢到了,执行produce方法,flag依然为true,t2也进入冻结状态,这时候t3、t4抢夺CPU执行权,假设t3抢到CPU执行权执行consume 方法,正常消费之后,flag置为false,还调用了notify方法把t1唤醒(t1 仅仅是具备争取CPU执行权的资格)此时CPU执行权还是在t3这,再次执行consume方法,t3 放弃CPU执行权进入冻结态,此时t4、t1抢夺CPU执行权,假如t4抢到了也执行connsume方法,t4也放弃了CPU执行权,t1重新获取CPU执行权,继续之前冻结前的语句往下执行(而不是从produce方法的第一句语句开始执行),给产品赋值,flag设置为true,最后调用notify方法把t2 唤醒,此时t1还是拥有CPU执行权再次从头开始执行produce方法,再次进冻结态,此时t2(因为t3、t4依然处于冻结态)继续之前冻结前的语句往下执行,t2 又生产了一个导致前一个产品被覆盖,t2 接着唤醒t3,t3消费了t2生产的产品,从而导致生产了两次只消费一次的现象,其他现象分析大致相同。原因分析完毕之后,只需要改动两处即可:唤醒所有线程和循环进行条件判断

package thread;

public class ProduceConsumeModel {

	public static void main(String[] args) {
		Product res = new Product();
		new Thread(new ProduceThread(res)).start();
		new Thread(new ConsumeThread(res)).start();
		new Thread(new ProduceThread(res)).start();
		new Thread(new ConsumeThread(res)).start();
	}
}

class Product {
	private String name;
	private int number = 1;
	private boolean flag = false;

	public synchronized void produce(String name) {
		while (this.flag) {
			try {
				// 表示调用这个方法后,会使得此时持有这个对象锁的线程wait
				this.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		this.name = name + "--" + number++;
		System.out.println(Thread.currentThread().getName() + "...生产:"
				+ this.name);
		flag = true;
		this.notifyAll();//这里必须唤醒所有的线程,否则可能会导致所有线程都进入冻结态
	}

	public synchronized void consume() {
		while (!this.flag) {
			try {
				// 表示调用这个方法后,会使得此时持有这个对象锁的线程wait
				this.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		System.out.println(Thread.currentThread().getName() + "******消费:"
				+ this.name);
		flag = false;
		this.notifyAll();
	}
}

// 生产者
class ProduceThread implements Runnable {
	private Product res;// 要操作的资源对象

	public ProduceThread(Product res) {
		this.res = res;
	}

	public void run() {
		while (true && !Thread.interrupted()) {
			res.produce("产品");
		}
	}
}

// 消费者
class ConsumeThread implements Runnable {
	private Product res;// 要操作的资源对象

	public ConsumeThread(Product res) {
		this.res = res;
	}

	public void run() {
		while (true && !Thread.interrupted()) {
			res.consume();
		}
	}
}

在这里插入图片描述

四、使用Lock-Condition机制实现升级版生产者-消费者模型

在JDK 1.5之前我们实现生产者-消费者模型只能依靠wait-notify等待唤醒机制,很明显有些弊端:每次都得唤醒等待池中的所有线程,而且超过2个线程还能导致所有线程都进入冻结状态,导致系统全部被挂起(并非死锁),幸亏JDK1.5之后引入了一套更高级些的Lock-Condition机制,Lock-Condition机制一般的使用步骤:

  • 创建Lock对象
  • 获取Condition对象
  • 获取锁
  • Condition对象使得线程等待
  • 释放锁
final Lock lock=new ReetrantLock();//1、创建Lock对象
final Condition condition=lock.newCondition();//2、获取Condition对象

lock.lock();//3、获取锁
try{
	while(...){
		condition.await();//4、当前线程等待
	}
	...
}finally{
	lock.unlock();//5、释放锁
}

1、Lock概述

JDK1.5之后引入了一套更高级些的Lock-Condition机制为我们多线程同步提供了更优秀的解决方案,其中Lock是一个接口类,比较常见的实现类有ReetrantLock、ReadWriteLock,主要提供了与同步synchronized类似的操作,可以看成使用Lock替换了synchronized,而且一个Lock可以持有多个Condition对象

方法 说明
void lock() 获取锁,如果锁不可用,则让当前线程一直处于休眠状态,直到获取锁,相当于是开始进入synchronized 代码块或者synchronized 方法,把这个共享资源显式锁住了
Condition newCondition() 返回绑定到该Lock对象上的Condition实例
Void unlock() 释放锁

2、Condition 概述

Condition条件变量或条件队列也是一个接口,将Object的wait、notify、notifyAll方法分解为Condition的操作,以便结合任意Lock对象实现组合使用,同时为每个对象提供多个wait-set,可以看成用于替换Object 部分的wait、notify、notifyAll方法。

方法 说明
void await() 使得当前线程处于等待状态,通过对应的Condition对象进行等待,只能通过同一个Condition对象signa唤醒
void signal() 唤醒Condition所属Lock的的线程
void signalAll() 唤醒所有等待所属线程
package lock;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ProduceConsumeModel2 {

	public static void main(String[] args) {
		Product res = new Product();
		new Thread(new ProduceThread2(res)).start();
		new Thread(new ConsumeThread2(res)).start();
		new Thread(new ProduceThread2(res)).start();
		new Thread(new ConsumeThread2(res)).start();
	}
}

class Product {
	private String name;
	private int number = 1;
	private boolean flag = false;
	private final Lock lock = new ReentrantLock();
	private final Condition produceCondition = lock.newCondition();
	private final Condition consumeCondition = lock.newCondition();

	public void produce(String name) {
		lock.lock();
		try {
			while (this.flag) {
				// 表示调用这个方法后,会使得此时持有这个对象锁的线程wait
				// /this.wait();
				produceCondition.await();
			}
			this.name = name + "--" + number++;
			System.out.println(Thread.currentThread().getName() + "...生产:"
					+ this.name);
			flag = true;
			// /this.notifyAll();
			consumeCondition.signal();
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}

	public void consume() {
		lock.lock();
		try {
			while (!this.flag) {
				// 表示调用这个方法后,会使得此时持有这个对象锁的线程wait
				// /this.wait();
				consumeCondition.await();
			}
			System.out.println(Thread.currentThread().getName() + "******消费:"
					+ this.name);
			flag = false;
			// /this.notifyAll();
			produceCondition.signal();
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}
}

// 生产者
class ProduceThread2 implements Runnable {
	private Product res;// 要操作的资源对象

	public ProduceThread2(Product res) {
		this.res = res;
	}

	public void run() {
		while (true && !Thread.interrupted()) {
			res.produce("产品");
		}
	}
}

// 消费者
class ConsumeThread2 implements Runnable {
	private Product res;// 要操作的资源对象

	public ConsumeThread2(Product res) {
		this.res = res;
	}

	public void run() {
		while (true && !Thread.interrupted()) {
			res.consume();
		}
	}
}

发布了241 篇原创文章 · 获赞 136 · 访问量 54万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 精致技术 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览