可阻塞的队列
下面我们开始来了解java5并发库提供的集合。首先我们来了解一个队列,这个队列就是一个数据的集合。就相当于一系列的单元格,我们可以往这个单元格里面逐一的放数据。我们取数据的时候是随机取一个呢,还是先取哪一个后取哪一个?我们取数据的时候是先取那个最先放进去的。就是先进先出就叫队列。这个队列呢,有固定大小的,也有不固定大小的,固定大小的当已经到了固定大小了,假如说,这个队列里面我放了8个数据,总共大小就是8,结果还一个都没取走,再要往里面放的时候就会阻塞,或者报错。就说,啊,放不进去了,或者就等待,苦苦等待。这就几种情况都会有。这是这个队列。如果是非阻塞队列,那就不阻塞,就直接报错。如果是阻塞队列,就会阻塞。队列又分为固定长度的,还有不固定长度的。
ArrayBlockingQueue
Java5提供的对阻塞队列的描述的类为ArrayBlockingQueue. 一个由数组支持的有界阻塞队列。一旦创建了这样的缓存区,就不能再增加其容量。这个类他实现了一个接口叫BlockingQueue.
BlockingQueue支持两个附加操作的 Queue,这两个操作是:获取元素时等待队列变为非空,以及存储元素时等待空间变得可用。
BlockingQueue 方法以四种形式出现,对于不能立即满足但可能在将来某一时刻可以满足的操作,这四种形式的处理方式不同:第一种是抛出一个异常,第二种是返回一个特殊值(null 或 false,具体取决于操作),第三种是在操作可以成功前,无限期地阻塞当前线程,第四种是在放弃前只在给定的最大时间限制内阻塞。下表中总结了这些方法:
抛出异常 |
特殊值 |
阻塞 |
超时 |
|
插入 |
||||
移除 |
||||
检查 |
不可用 |
不可用 |
add(e),offer(e),put(e)这3个方法都可以往队列里面加数据,假设这个队列的大小是8个空间,刚开始他是空的,我往他里面想加一个元素进去,请问,调用这三个当中的哪一个?调用这三个当中的任何一个都成。都可以。这三个方法的区别在于,当这个队列满了,它有8个空间,最后8个空间都装满了,这时候再用add的时候,装不进去了,它就会抛异常,而不是堵。调用offer呢,就是立马返回,如果放进去了,那就是true,没放进去它就不放了,它就告诉你,我放不进去,返回一个false.它也不报错。程序没报错,程序只是返回值为fale,表示放入失败。程序没有抛异常。这个方法你要检查放进去没有,你就检查是不是true.add方法要知道放进去没有,就是try catch。put方法呢,就是放不进去就堵在那里,阻塞在那里,直到空出一个单元格,立马就放进去。同样,取数据的方法也是一样的道理。如果一个都没有,取不到,remove方法抛异常,poll方法返回null,take方法阻塞在那里,直到人家放了一个,我就马上把放的那个拿走。
下面,我们通过代码来演示可阻塞队列的运行效果。我们这个代码是有两个线程,从队列里面取数据,有一个线程往队列里面放数据。如果放的比较快呢,你就可以看到那个放数据的线程堵着,如果取得比较快呢,你就可以看到取的那个线程阻塞。
import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; public class BlockingQueueTest { public static void main(String[] args) { final BlockingQueue queue = new ArrayBlockingQueue(3);// 创建了一个数组队列,这个队列允许放3个数据 for (int i = 0; i < 2; i++) {// 循环创建两个放数据的线程 new Thread() { public void run() { while (true) { try { Thread.sleep((long) (Math.random() * 1000)); System.out.println(Thread.currentThread().getName() + "准备放数据"); queue.put(1);//如果队列已经放满了,这句代码会造成线程阻塞 System.out.println(Thread.currentThread().getName() + "已经放了数据,队列目前有" + queue.size() + "个数据"); } catch (InterruptedException e) { e.printStackTrace(); } } } }.start(); } new Thread() {// 取数据的那个线程 public void run() { while (true) { try { // 将此处的睡眠时间分别修改为100和1000,观察运行结果 Thread.sleep(1000); System.out.println(Thread.currentThread().getName() + "准备取数据"); queue.take();//如果队列取空了,这句代码会造成线程阻塞 //不能保证上面的take和下面的打印语句是原子性的,当执行完take,并不是马上执行打印,这中间会被其它线程打断 System.out.println(Thread.currentThread().getName() + "已经取走了数据,队列目前有" + queue.size() + "个数据"); } catch (InterruptedException e) { e.printStackTrace(); } } } }.start(); } } |
下面再看一个代码示例,用阻塞队列来实现同步通知的功能:首先来看一道题,首先有一个队列queue1,刚开始它是空的,空的我就想把数据放进去,我就调用put方法,放数据进去。然后有一个queue2队列,它一开始就是满的,它也去put,这时候它就阻塞。我那边queue它put完了以后,我让queue2调用take方法取数据,queue2中被取了一个数据后,它的put方法就不再阻塞了,代码继续向下运行,它下面的代码是让queue1调用take方法取数据….如下图所示:就这样,你一下,我一下,你通知我,我通知你,就是用阻塞队列也可以实现互斥,这个队列的单元格只有一个,把数据一放进去了以后,就不能继续放了,就只能等,除非我把数据拿走了,你才能走。
l 下面我们用代码来实现一下:用两个只有1个空间的队列来实现同步通知的功能。
import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; public class BlockingQueueCommunication { /** * @param args */ public static void main(String[] args) { final Business business = new Business(); new Thread(new Runnable() { public void run() { for (int i = 1; i <= 50; i++) { business.sub(i); } } }).start(); for (int i = 1; i <= 50; i++) { business.main(i); } } static class Business { BlockingQueue<Integer> queue1 = new ArrayBlockingQueue<Integer>(1); BlockingQueue<Integer> queue2 = new ArrayBlockingQueue<Integer>(1); {//匿名构造方法,在所有的构造方法之前执行 try { queue2.put(1);//创建对象的时候就给queue2放满 } catch (InterruptedException e) { e.printStackTrace(); } } public void sub(int i) { try { queue1.put(1); } catch (InterruptedException e) { e.printStackTrace(); } for (int j = 1; j <= 10; j++) { System.out.println("sub thread sequece of " + j + ",loop of " + i); } try { queue2.take();//直到它take之前main方法都堵在queue2.put(1);这句代码 } catch (InterruptedException e) { e.printStackTrace(); } } public void main(int i) { try { queue2.put(1); } catch (InterruptedException e1) { e1.printStackTrace(); } for (int j = 1; j <= 100; j++) { System.out.println("main thread sequece of " + j + ",loop of " + i); } try { queue1.take();//直到它take之前,sub方法都堵在queue1.put(1);这句代码 } catch (InterruptedException e) { e.printStackTrace(); } } } } |
SynchronousQueue
接下来我们再介绍一个同步的阻塞队列:SynchronousQueue
一种阻塞队列,其中每个插入操作必须等待另一个线程的对应移除操作。也就是说,我往队列里面去插入数据,插不插得进去?插不进去。哪怕队列是空的我也插不进去,一旦有一个人来读得时候,读不读得到?都不到,队列里面没有。但是,这个队列是,我已发现有人来读了,啪,我就放进去了。那个读得人啪,马上读走了。很像那个交换情况,有个人举着毒粉要跟人交换,换不换的出去?换不出去,只有那个来买毒粉的人到达了才换出去。也就是说,我想往队列里面插数据,插进去了吗?没有。就在那个读数据的人到达了,它在读数据的一刹那间,我插进去了,他读走了。之前那个Exchanger对象是单对单的交换数据,现在呢这个是可以有多个来抢数据。有人举着一包毒粉想插进去,结果,买毒粉的人没来,他能不能放到队列里面去呢?放不进去。结果,买毒粉的一下来了3个,他能不能放进去?能放进去。那谁拿走了呢?看谁抢的快!如果说他同时有3包白粉,同时都扔进去了,那就3个拿的人各抢到1包走了。大概呢是这么个意思。如果没有线程等着要拿,你就会阻塞,放不进去。如果有线程等着要拿,你就直接交给他。也就是这只手交那只手,中间并没有把东西存放到哪个第三方的存放点。暂时这么理解,具体还要研究源码才知道。
同步队列没有任何内部容量,甚至连一个队列的容量都没有。它非常适合于传递性设计,在这种设计中,在一个线程中运行的对象要将某些信息、事件或任务传递给在另一个线程中运行的对象,它就必须与该对象同步。