经典原始问题:生产者和消费者的问题,其实在实际项目中很容易遇到这样的无奈的问题,但是面对这样的问题的时候我们首先想到的就是多线程批处理,通过notify()…的处理,但只这样的处理只能给我们贴上对java多线程不熟悉的标签。比较赞的办法是用Semaphore 或者 BlockingQueue来实现生产者消费者模型。下面我们就来看看吧!
Semaphore
一个计数信号量。从概念上讲,信号量维护了一个许可集。如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可。每个 release() 添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore 只对可用许可的号码进行计数,并采取相应的行动。拿到信号量的线程可以进入代码,否则就等待。通过acquire()和release()获取和释放访问许可。
Semaphore分为单值和多值两种,前者只能被一个线程获得,后者可以被若干个线程获得。
以一个停车场运作为例。为了简单起见,假设停车场只有三个车位,一开始三个车位都是空的。这时如果同时来了五辆车,看门人允许其中三辆不受阻碍的进入,然后放下车拦,剩下的车则必须在入口等待,此后来的车也都不得不在入口处等待。这时,有一辆车离开停车场,看门人得知后,打开车拦,放入一辆,如果又离开两辆,则又可以放入两辆,如此往复。
在这个停车场系统中,车位是公共资源,每辆车好比一个线程,看门人起的就是信号量的作用。
更进一步,信号量的特性如下:信号量是一个非负整数(车位数),所有通过它的线程(车辆)都会将该整数减一(通过它当然是为了使用资源),当该整数值为零时,所有试图通过它的线程都将处于等待状态。在信号量上我们定义两种操作: Wait(等待) 和 Release(释放)。 当一个线程调用Wait(等待)操作时,它要么通过然后将信号量减一,要么一直等下去,直到信号量大于一或超时。Release(释放)实际上是在信号量上执行加操作,对应于车辆离开停车场,该操作之所以叫做“释放”是因为加操作实际上是释放了由信号量守护的资源。
在java中,还可以设置该信号量是否采用公平模式,如果以公平方式执行,则线程将会按到达的顺序(FIFO)执行,如果是非公平,则可以后请求的有可能排在队列的头部。
JDK中定义如下:
Semaphore(int permits, boolean fair)
//创建具有给定的许可数和给定的公平设置的Semaphore。
Semaphore当前在多线程环境下被扩放使用,操作系统的信号量是个很重要的概念,在进程控制方面都有应用。Java并发库Semaphore 可以很轻松完成信号量控制,Semaphore可以控制某个资源可被同时访问的个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。比如在Windows下可以设置共享文件的最大客户端访问个数。
Semaphore实现的功能就类似厕所有5个坑,假如有10个人要上厕所,那么同时只能有多少个人去上厕所呢?同时只能有5个人能够占用,当5个人中 的任何一个人让开后,其中等待的另外5个人中又有一个人可以占用了。另外等待的5个人中可以是随机获得优先机会,也可以是按照先来后到的顺序获得机会,这取决于构造Semaphore对象时传入的参数选项。单个信号量的Semaphore对象可以实现互斥锁的功能,并且可以是由一个线程获得了“锁”,再由另一个线程释放“锁”,这可应用于死锁恢复的一些场合
package concurrent;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
public class SemaphoreDemo {
Semaphore semaphore=new Semaphore(1);
List<String>list=new ArrayList<>();
public static void main(String[] args) {
SemaphoreDemo s=new SemaphoreDemo();
//创建线程管理
ExecutorService executorService=Executors.newCachedThreadPool();
executorService.execute(s.new ThreadTestA());//启动线程
executorService.execute(s.new ThreadTestB());//
executorService.shutdown();//结束服务
}
private class ThreadTestA implements Runnable{
@Override
public void run() {
try {
System.out.println("这个是线程A获取许可证之前");
semaphore.acquire();//获得许可证
System.out.println("这个是线程A获取许可证之后");
list.add("test");
Thread.sleep(1000);//模拟延迟
} catch (InterruptedException e) {
e.printStackTrace();
}
finally{
semaphore.release();//吊销
System.out.println("这个是线程A释放许可证之后");
}
}
}
private class ThreadTestB implements Runnable{
@Override
public void run() {
try {
System.out.println("这个是线程B获取许可证之前");
semaphore.acquire();
System.out.println("这个是线程B获取许可证之后");
if(list.size()!=0)
System.out.println(list.get(0));
} catch (InterruptedException e) {
e.printStackTrace();
}
finally{
semaphore.release();
System.out.println("这个是线程B释放许可证之后");
}
}
}
}
运行结果可能会出现几种情况
1
这个是线程A获取许可证之前
这个是线程A获取许可证之后
这个是线程B获取许可证之前
这个是线程A释放许可证之后
这个是线程B获取许可证之后
test
这个是线程B释放许可证之后
2
这个是线程A获取许可证之前
这个是线程A获取许可证之后
这个是线程B获取许可证之前
这个是线程B获取许可证之后
test
这个是线程B释放许可证之后
这个是线程A释放许可证之后
3
这个是线程B获取许可证之前
这个是线程B获取许可证之后
这个是线程B释放许可证之后
这个是线程A获取许可证之前
这个是线程A获取许可证之后
这个是线程A释放许可证之后
BlockingQueue
首先是一个接口。有很多子接口和实现类:
BlockingQueue也是java.util.concurrent下的主要用来控制线程同步的工具。
BlockingQueue有四个具体的实现类,根据不同需求,选择不同的实现类
1、ArrayBlockingQueue:一个由数组支持的有界阻塞队列,规定大小的BlockingQueue,其构造函数必须带一个int参数来指明其大小.其所含的对象是以FIFO(先入先出)顺序排序的。
2、LinkedBlockingQueue:大小不定的BlockingQueue,若其构造函数带一个规定大小的参数,生成的BlockingQueue有大小限制,若不带大小参数,所生成的BlockingQueue的大小由Integer.MAX_VALUE来决定.其所含的对象是以FIFO(先入先出)顺序排序的。
3、PriorityBlockingQueue:类似于LinkedBlockQueue,但其所含对象的排序不是FIFO,而是依据对象的自然排序顺序或者是构造函数的Comparator决定的顺序。
4、SynchronousQueue:特殊的BlockingQueue,对其的操作必须是放和取交替完成的。
LinkedBlockingQueue 可以指定容量,也可以不指定,不指定的话,默认最大是Integer.MAX_VALUE,其中主要用到put和take方法,put方法在队列满的时候会阻塞直到有队列成员被消费,take方法在队列空的时候会阻塞,直到有队列成员被放进来。
下面我们通过一个例子来说明:
Bread
package concurrent;
public class Bread {
private String name;
private String color;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
}
生产者机器 BreadMachine
package concurrent;
import java.awt.Color;
import java.util.Random;
import java.util.concurrent.BlockingQueue;
public class BreadMachine implements Runnable{
private BlockingQueue<Bread> queue;
public BreadMachine(BlockingQueue<Bread> queue){
this.queue=queue;
}
@Override
public void run() {
System.out.println("开始生产面包");
Bread bread=new Bread();
bread.setColor(Color.BLACK+"");
Random random=new Random();
bread.setName(random.nextInt(100)+"面包");
try {
queue.put(bread);
System.out.println(bread.getName()+"已经生产完毕!去吃吧");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
消费者People
package concurrent;
import java.util.concurrent.BlockingQueue;
public class People implements Runnable{
BlockingQueue<Bread> bread;
public People(BlockingQueue<Bread> bread ) {
this.bread=bread;
}
@Override
public void run() {
try {
System.out.println("我需要面包");
System.out.println("这个二货吃了"+bread.take().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
测试程序
package concurrent;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class BlockingQueenDemo {
static BlockingQueue<Bread> blockingQueue=new ArrayBlockingQueue<>(5);
public static void main(String[] args) {
BreadMachine breadMachine=new BreadMachine(blockingQueue);
People people=new People(blockingQueue);
for(int i=0;i<5;i++){
new Thread(people, "people").start();
new Thread(breadMachine, "breadMachine").start();
}
}
}
运行效果;
开始生产面包
开始生产面包
我需要面包
我需要面包
开始生产面包
我需要面包
开始生产面包
我需要面包
我需要面包
开始生产面包
29面包已经生产完毕!去吃吧
23面包已经生产完毕!去吃吧
这个二货吃了29面包
66面包已经生产完毕!去吃吧
31面包已经生产完毕!去吃吧
36面包已经生产完毕!去吃吧
这个二货吃了23面包
这个二货吃了66面包
这个二货吃了31面包
这个二货吃了36面包
由于队列的大小限定成了5,所以最多只有两个产品被加入到队列当中,而且消费者取到产品的顺序也是按照生产的先后顺序,原因就是LinkedBlockingQueue和ArrayBlockingQueue都是按照FIFO的顺序存取元素的。