程序的基本实现
在多线程的开发过程之中最为著名的案例就是生产者与消费者操作,该操作的主要流程如下:
——生产者负责信息内容的生产;
——每当生产者生产完成一项完整的信息之后消费者要从这里面取走信息;
——如果生产者没有生产完则消费者要等待它生产完成,如果消费者还没有对信息进行消费,则生产者应该等消费者处理完成后再继续生产。
真实世界中的生产者消费者模式生产者和消费者模式在生活当中随处可见,它描述的是协调与协作的关系。比如一个人正在准备食物(生产者),而另一个人正在吃(消费者),他们使用一个共用的桌子用于放置盘子和取走盘子,生产者准备食物,如果桌子上已经满了就等待,消费者(那个吃的)等待如果桌子空了的话。这里桌子就是一个共享的对象。在Java Executor框架自身实现了生产者消费者模式它们分别负责添加和执行任务。
生产者消费者模式的好处它的确是一种实用的设计模式,常用于编写多线程或并发代码。下面是它的一些优点:
1.它简化的开发,你可以独立地或并发的编写消费者和生产者,它仅仅只需知道共享对象是谁
2.生产者不需要知道谁是消费者或者有多少消费者,对消费者来说也是一样
3.生产者和消费者可以以不同的速度执行
4.分离的消费者和生产者在功能上能写出更简洁、可读、易维护的代码
可以将生产者与消费者定义为两个独立的线程类对象,但是对于现在生产的数据,可以使用如下的组成:
——数据一:title=王建、content=宇宙大帅哥;
——数据二:title=小高、content=猥琐第一人;
既然消费者与生产者是两个独立的线程,那么这两个独立的线程之间就需要有一个数据的保存集中点,那么可以单独定义一个Message类实现数据的保存。
范例:实现程序基本结构
package cn.mldn.demo;
public class ThreadDemo {
public static void main(String[] args) throws Exception {
Message msg = new Message();
new Thread(new Producer(msg)).start(); //启动生产者线程
new Thread(new Consumer(msg)).start(); //启动消费者线程
}
}
class Producer implements Runnable {
private Message msg;
public Producer(Message msg) {
this.msg = msg;
}
@Override
public void run() {
for (int x = 0; x < 100; x ++) {
if(x % 2 == 0) {
this.msg.setTitle("王建");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
this.msg.setContent("宇宙大帅哥");
} else {
this.msg.setTitle("小高");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
this.msg.setContent("猥琐第一人");
}
}
}
}
class Consumer implements Runnable {
private Message msg;
public Consumer(Message msg) {
this.msg = msg;
}
@Override
public void run() {
for (int x = 0; x < 100; x ++) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(this.msg.getTitle() + " - " + this.msg.getContent());
}
}
}
class Message {
private String title;
private String content;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
通过整个代码的执行你会发现此时有两个主要问题:
——问题一:数据不同步了;
——问题二:生产一个取走一个,但是发现了有重复生产和重复取出问题;
解决数据同步问题
如果要解决问题,首先解决的就是数据同步的处理问题,如果要想解决数据同步最简单的做法是使用synchronized关键字定义同步代码块或同步方法,于是这个时候对于同步的处理就可以直接在Message类中完成。
范例:解决同步操作
package cn.mldn.demo;
class Message {
private String title;
private String content;
public synchronized void set(String title,String content) {
this.title = title;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
this.content = content;
}
public synchronized String get() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return this.title + " - " + this.content;
}
}
public class ThreadDemo {
public static void main(String[] args) throws Exception {
Message msg = new Message();
new Thread(new Producer(msg)).start(); //启动生产者线程
new Thread(new Consumer(msg)).start(); //启动消费者线程
}
}
class Producer implements Runnable {
private Message msg;
public Producer(Message msg) {
this.msg = msg;
}
@Override
public void run() {
for (int x = 0; x < 100; x ++) {
if(x % 2 == 0) {
this.msg.set("王建","宇宙大帅哥");
} else {
this.msg.set("小高","猥琐第一人");
}
}
}
}
class Consumer implements Runnable {
private Message msg;
public Consumer(Message msg) {
this.msg = msg;
}
@Override
public void run() {
for (int x = 0; x < 100; x ++) {
System.out.println(this.msg.get());
}
}
}
在进行同步处理的时候肯定需要有一个同步处理的对象,那么此时肯定要将同步操作交由Message类处理是最合适的。这个时候发现数据已经可以正常的保持一致了,但是对于重复操作的问题依然存在。
线程等待与唤醒
如果说现在想要解决生产者与消费者的问题,那么最好的解决方案就是使用等待与唤醒机制。而对于等待与唤醒机制,主要依靠的是Object类中提供的方法处理的:
——等待机制:
——死等:public final void wait() throws InterruptedException
——设置等待时间:public final void wait(long timeout)throws InterruptedException
——设置等待时间:public final void wait(long timeout,int nanos)
throws InterruptedException
——唤醒第一个等待线程:public final void notify()
——唤醒全部等待线程:public final void notifyAll()
如果此时有若干个等待线程的话,那么notify()表示的是唤醒第一个等待的,而其它的线程继续等待,而notifyAll()表示会唤醒所有等待的线程,那个线程的优先级高就有可能先执行。
对于当前问题主要的解决应该通过Message类完成处理。
范例:修改Message类
package cn.mldn.demo;
class Message {
private String title;
private String content;
private boolean flag = true; //表示生产或消费的形式
//flag = true:允许生产,但是不允许消费
//flag = false :允许消费,不允许生产
public synchronized void set(String title,String content) {
if(this.flag == false) {
try {
super.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
this.title = title;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
this.content = content;
this.flag = false; //已经生产过了
super.notify();
}
public synchronized String get() {
if (this.flag == true) { //还未生产,需要等待
try {
super.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
try {
return this.title + " - " + this.content;
} finally { //不管如何都要执行
this.flag = true; //继续生产
super.notify(); //唤醒等待线程
}
}
}
public class ThreadDemo {
public static void main(String[] args) throws Exception {
Message msg = new Message();
new Thread(new Producer(msg)).start(); //启动生产者线程
new Thread(new Consumer(msg)).start(); //启动消费者线程
}
}
class Producer implements Runnable {
private Message msg;
public Producer(Message msg) {
this.msg = msg;
}
@Override
public void run() {
for (int x = 0; x < 100; x ++) {
if(x % 2 == 0) {
this.msg.set("王建","宇宙大帅哥");
} else {
this.msg.set("小高","猥琐第一人");
}
}
}
}
class Consumer implements Runnable {
private Message msg;
public Consumer(Message msg) {
this.msg = msg;
}
@Override
public void run() {
for (int x = 0; x < 100; x ++) {
System.out.println(this.msg.get());
}
}
}
这种处理形式就是在进行多线程开发过程之中最原始的处理方案,整个的等待、同步、唤醒机制都由开发者自行通过原生代码实现控制。
使用阻塞队列实现生产者消费者模式
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。
这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。
阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。
另外,阻塞队列也可以用于线程池中。
生产者消费者模式是并发、多线程编程中经典的设计模式,生产者和消费者通过分离的执行工作解耦,简化了开发模式,生产者和消费者可以以不同的速度生产和消费数据。这篇文章我们来看看什么是生产者消费者模式,这个问题也是多线程面试题中经常被提及的。如何使用阻塞队列(Blocking Queue)解决生产者消费者模式,以及使用生产者消费者模式的好处。
多线程中的生产者消费者问题
生产者消费者问题是一个流行的面试题,面试官会要求你实现生产者消费者设计模式,以至于能让生产者应等待如果队列或篮子满了的话,消费者等待如果队列或者篮子是空的。这个问题可以用不同的方式来现实,经典的方法是使用wait和notify方法在生产者和消费者线程中合作,在队列满了或者队列是空的条件下阻塞,Java5的阻塞队列(BlockingQueue)数据结构更简单,因为它隐含的提供了这些控制,现在你不需要使用wait和nofity在生产者和消费者之间通信了,阻塞队列的put()方法将阻塞如果队列满了,队列take()方法将阻塞如果队列是空的。在下部分我们可以看到代码例子。
使用阻塞队列实现生产者消费者模式
阻塞队列实现生产者消费者模式超级简单,它提供开箱即用支持阻塞的方法put()和take(),开发者不需要写困惑的wait-nofity代码去实现通信。
BlockingQueue 一个接口,Java5提供了不同的现实,如ArrayBlockingQueue和LinkedBlockingQueue,两者都是先进先出(FIFO)顺序。而ArrayLinkedQueue是自然有界的,LinkedBlockingQueue可选的边界。
下面这是一个完整的生产者消费者代码例子,对比传统的wait、nofity代码,它更易于理解。
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class ThreadDemo {
public static void main(String[] args) {
BlockingQueue shareQueue = new ArrayBlockingQueue();
new Thread(new Producer(shareQueue)).start();
new Thread(new Consumer(shareQueue)).start();
}
}
//消费者
class Consumer implements Runnable {
BlockingQueue shareQueue;
public Consumer(BlockingQueue shareQueue) {
this.shareQueue = shareQueue;
}
@Override
public void run() {
while (true) {
try {
System.out.println("Consumed:" + shareQueue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//生产者
class Producer implements Runnable {
private final BlockingQueue shareQueue;
public Producer(BlockingQueue shareQueue) {
this.shareQueue = shareQueue;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
shareQueue.put(i);
System.out.println("Produced:" + i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}