java中的阻塞队列

Java中的阻塞队列

一、什么是阻塞队列

所谓的阻塞队列,指的是:如果当前队列为空,那么获取元素的线程就会被阻塞,处于等待状态,直到队列变为非空;如果当前队列满的时候,添加元素的线程就会被阻塞,处于等待状态,直到队列有存储空间。阻塞队列是一个经典的生产者-消费者模式,生产者不停的往队列里面添加元素,消费者不停的从队列消费元素。阻塞队列就是实现生产者-消费者模式借助的第三方,很多时候,我们找到这个第三方,就可以很好的理解它们的原理,学习设计模式同样是这个道理。

二、阻塞队列的方法

阻塞队列提供的方法如下:


Throws Exception:是指当阻塞队列满时候,再往队列里插入元素,会抛出IllegalStateException(“Queue full”)异常。当队列为空时,从队列里获取元素时会抛出NoSuchElementException异常

Special Value:插入方法会返回是否成功,成功则返回true。移除方法,则是从队列里拿出一个元素,如果没有则返回null

Blocks:当阻塞队列满时,如果生产者线程往队列里put元素,队列会一直阻塞生产者线程,直到拿到数据,或者响应中断退出。当队列空时,消费者线程试图从队列里take元素,队列也会阻塞消费者线程,直到队列可用。

Times Out:当阻塞队列满时,队列会阻塞生产者线程一段时间,如果超过一定的时间,生产者线程就会退出

三、阻塞队列的实现

Java的JDK中,已经给我们提供了一个简易的阻塞队列的实现,代码如下(源码见官方文档):

public class BoundedBuffer {
	// 队列的全局锁
	final Lock lock = new ReentrantLock();
	// 状态Condition
	final Condition notFull = lock.newCondition();
	// 状态Condition
	final Condition notEmpty = lock.newCondition();
	// 队列的大小
	final Object[] items = new Object[3];
	// 放入元素的索引
	int putptr;
	// 取元素的索引
	int takeptr;
	// 当前队列的大小
	int count;

	/**
	 * 描述:放入元素的操作
	 * @param x 放入的对象
	 * @throws InterruptedException
	 */
	public void put(Object x) throws InterruptedException {
		System.out.println("获取写锁!");
		// 为了保证多线程的安全,此方法需要加锁
		lock.lock();
		try {
			/*
			 *  如果count和队列的元素空间大小相等,说明当前队列已满,无法再放入元素,阻塞放元素操作
			 *  此处使用while而不使用if,主要是为了防止线程之间的假唤醒
			 */
			while (count == items.length){
				System.out.println("队列已满,阻塞写线程!");
				// 放入元素的线程等待,注意,此处会释放锁
				notFull.await();
			}
			System.out.println("写入的数据为:"+x);
			// 如果不满,则将x放入队列
			items[putptr] = x;
			// 如果放入的索引和队列的长度一直,则说明该队列已满,需要从索引0开始放入原色
			if (++putptr == items.length){
				putptr = 0;
			}
			// 队列的大小自增
			++count;
			// 唤醒取元素线程
			notEmpty.signal();
		} finally {
			System.out.println("释放写锁!");
			// 释放锁
			lock.unlock();
		}
	}

	/**
	 * 描述:取元素的操作
	 * @return 取出的元素
	 * @throws InterruptedException
	 */
	public Object take() throws InterruptedException {
		System.out.println("获取读锁!");
		// 加锁
		lock.lock();
		try {
			// 如果count=0,说明队列为空,没有元素可以被取出,阻塞取操作
			while (count == 0){
				System.out.println("队列为空,阻塞读线程!");
				notEmpty.await();
			}
			// 从队列里面取出一个元素
			Object x = items[takeptr];
			// 如果取索引和队列的长度相等,说明已经取完队列的最后一个,需要从队列的第一个位置取元素
			if (++takeptr == items.length){
				takeptr = 0;
			}
			// 队列长度自减
			--count;
			// 唤醒放元素线程
			notFull.signal();
			// 返回取出的元素
			System.out.println("读取的数据为:"+x);
			return x;
		} finally {
			System.out.println("释放读锁!");
			// 释放锁
			lock.unlock();
		}
	}
}
测试代码如下:
public static void main(String[] args) throws InterruptedException {
		final BoundedBuffer demo = new BoundedBuffer();
		// 新建N个写线程,且写比对快
		ExecutorService put = Executors.newFixedThreadPool(1);
		put.execute(new Runnable() {
			@Override
			public void run() {
				for(int i=0; i<5; i++){
					try {
						demo.put("chhliu"+i);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		});
		
		// 新建N个读线程,读比写慢
		ExecutorService take = Executors.newFixedThreadPool(1);
		take.execute(new Runnable() {
			@Override
			public void run() {
				while(true){
					try {
						// 模拟长时间读
						Thread.sleep(200);
						demo.take();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		});
	}

测试结果如下:

获取写锁!
写入的数据为:chhliu0
释放写锁!
获取写锁!
写入的数据为:chhliu1
释放写锁!
获取写锁!
写入的数据为:chhliu2
释放写锁!
获取写锁!
队列已满,阻塞写线程!
获取读锁!
读取的数据为:chhliu0
释放读锁!
写入的数据为:chhliu3
释放写锁!
获取写锁!
队列已满,阻塞写线程!
获取读锁!
读取的数据为:chhliu1
释放读锁!
写入的数据为:chhliu4
释放写锁!
获取读锁!
读取的数据为:chhliu2
释放读锁!
获取读锁!
读取的数据为:chhliu3
释放读锁!
获取读锁!
读取的数据为:chhliu4
释放读锁!
获取读锁!
队列为空,阻塞读线程!
从上面的测试结果可以看出,阻塞队列的通知机制,队列满时,阻塞写线程,队列空时,阻塞读线程。

四、Java中提供的阻塞队列

Java中的阻塞队列分为2种,一种是单向的阻塞队列,一种是双向的阻塞队列,所谓的双向的阻塞队列,指的是可以从队列的两端来操作队列,例如插入,删除等,而单向队列则是只能从队列的一端来操作队列。

Java中提供的单向阻塞队列如下:


· ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。

· LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。

· PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。

· DelayQueue:一个使用优先级队列实现的无界阻塞队列。

· SynchronousQueue:一个不存储元素的阻塞队列。

· LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。

· LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

下面来简单的介绍下这些阻塞队列:

ArrayBlockingQueue

ArrayBlockingQueue是一个用数组实现的有界阻塞队列。此队列按照先进先出(FIFO)的原则对元素进行排序。默认情况下不保证访 问者公平的访问队列,所谓公平访问队列是指阻塞的所有生产者线程或消费者线程,当队列可用时,可以按照阻塞的先后顺序访问队列,即先阻塞的生产者线程,可 以先往队列里插入元素,先阻塞的消费者线程,可以先从队列里获取元素。通常情况下为了保证公平性会降低吞吐量。我们可以使用以下代码创建一个公平的阻塞队列:

private BlockingQueue<String> queue = new ArrayBlockingQueue<String>(queueSize, false);

LinkedBlockingQueue

LinkedBlockingQueue是一个用链表实现的有界阻塞队列。此队列的默认和最大长度为Integer.MAX_VALUE。此队列按照先进先出的原则对元素进行排序。

PriorityBlockingQueue

PriorityBlockingQueue是一个支持优先级的无界队列。默认情况下元素采取自然顺序排列,也可以通过比较器comparator来指定元素的排序规则。元素按照升序排列。

DelayQueue

DelayQueue是一个支持延时获取元素的无界阻塞队列。队列使用PriorityQueue来实现。队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。我们可以将DelayQueue运用在以下应用场景:

· 缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。

· 定时任务调度。使用DelayQueue保存当天将会执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,从比如TimerQueue就是使用DelayQueue实现的。

SynchronousQueue

SynchronousQueue是一个不存储元素的阻塞队列。每一个put操作必须等待一个take操作,否则不能继续添加元素。 SynchronousQueue可以看成是一个传球手,负责把生产者线程处理的数据直接传递给消费者线程。队列本身并不存储任何元素,非常适合于传递性 场景,比如在一个线程中使用的数据,传递给另外一个线程使用,SynchronousQueue的吞吐量高于LinkedBlockingQueue 和 ArrayBlockingQueue,其中,CachedThreadPool线程池中就使用了SynchronousQueue队列,如下:


用这种队列创建的线程池,只要线程池中的线程等待时间超时,就会销毁这个线程,当有新的任务到来的时候,又重新创建一个新的线程来执行这个新的任务,这种线程池不存在核心线程数和最大线程数。

LinkedTransferQueue

LinkedTransferQueue是一个由链表结构组成的无界阻塞TransferQueue队列。相对于其他阻塞队列LinkedTransferQueue多了tryTransfer和transfer方法。

transfer方法。如果当前有消费者正在等待接收元素(消费者使用take()方法或带时间限制的 poll()方法时),transfer方法可以把生产者传入的元素立刻transfer(传输)给消费者。如果没有消费者在等待接收元 素,transfer方法会将元素存放在队列的tail节点,并等到该元素被消费者消费了才返回

tryTransfer方法。则是用来试探下生产者传入的元素是否能直接传给消费者。如果没有消费者等待接收元素,则返回false。和transfer方法的区别是tryTransfer方法无论消费者是否接收,方法立即返回。而transfer方法是必须等到消费者消费了才返回。对于带有时间限制的tryTransfer(E e, long timeout, TimeUnit unit)方法,则是试图把生产者传入的元素直接传给消费者,但是如果没有消费者消费该元素则等待指定的时间再返回,如果超时还没消费元素,则返回 false,如果在超时时间内消费了元素,则返回true。

Java中提供的双向阻塞队列如下:


双向阻塞队列提供的方法如下:


从提供的方法中很容易看出,以First结尾的方法基本上是操作队列头的,以Last结尾的方法基本上是操作队列尾的,但由于BlockingDeque继承了BlockingQueue,所以他也提供了BlockingQueue对应的方法,除非你对add、put、offer这类Deque中的方法非常熟悉,准确的知道这些方法操作的是队首还是队尾,要不然,就老老实实的用Deque提供的带标志性的方法,例如以First和以Last结尾的方法,这些方法非常容易理解,不容易出错。双向的阻塞队列在一种情况下非常有用,那就是“任务窃取”,何为“任务窃取”,下面用一个图来简单的说明一下:


从上图中可以看出,线程1有4个任务要完成,线程2也有4个任务要完成,假如线程1已经完成了所有的任务,线程2还有3个任务没有完成,线程1与其等在那里,还不如帮线程2来干活,于是它就去其他线程的队列里窃取一个任务来执行。而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行,这样就节省了一部分线程之间竞争的开销。有兴趣的同事,可以了解一下Fork/Join框架。

五、使用阻塞队列实现简单的生产者-消费者模式

下面,我们使用阻塞队列,来实现一个简单的生产者-消费者模式,代码如下:

public class BlockingQueueDemo {
	// 阻塞队列的大小,初始化为10
	private int queueSize = 10;
	/*
	 * 创建一个ArrayBlockingQueue,非公平性的阻塞队列
	 */
	private BlockingQueue<String> queue = new ArrayBlockingQueue<String>(queueSize, false);

	public static void main(String[] args) {
		BlockingQueueDemo demo = new BlockingQueueDemo();
		// 创建一个消费者线程池,线程池的大小为4
		ExecutorService consumers = Executors.newFixedThreadPool(4);
		// 创建一个生产者线程池,线程池大小为2
		ExecutorService producers = Executors.newFixedThreadPool(2);
		// 提交给生产者线程池100个任务,生产100个产品
		for (int i = 0; i < 100; i++) {
			producers.submit(demo.new Producer());
		}
		// 提交给消费者5个任务,消费5个产品
		for (int i = 0; i < 5; i++) {
			consumers.submit(demo.new Consumer());
		}
	}

	class Producer extends Thread {

		@Override
		public void run() {
			produce();
		}

		private void produce() {
			try {
				String str = UUID.randomUUID().toString();
				System.out.println(Thread.currentThread().getName()
						+ " 插入队列的元素为:" + str);
				// 模拟生产产品
				queue.put(str);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}

	class Consumer extends Thread {

		@Override
		public void run() {
			consume();
		}

		private void consume() {
			try {
				while(true){
					// 模拟消费产品
					String str = queue.take();
					System.out.println(Thread.currentThread().getName()
							+ " 从队列取走的元素为:" + str);
				}
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

上面的代码中,我们借用了ArrayBlockingQueue阻塞队列来实现生产者-消费者模式,从代码中,很容易看出,借用阻塞队列来实现的话,非常的方便,我们不需要再考虑多线程的安全同步问题,也无须考虑线程之间如何互相通知,生产者只需要往阻塞队列里面放产品,消费者只需从阻塞队列里面取出产品消费即可。

六、阻塞队列的改进

在我们的实际开发中,其实阻塞队列是可以进一步改进从而提高效率的,毕竟阻塞队列经过了一次转存,如果我们不经过这层转存的话,效率岂不是更高嘛,在java中,已经有一个现成的改进的东东,那就是线程池,当我们把任务提交给线程池处理的时候,如果线程池有足够的资源,是不用转存的,可以直接处理掉,只有当线程资源不足的时候,才会存放在阻塞队列里面,当资源到位的时候,再从阻塞队列里面取任务来执行,这种改进可以大大的提高阻塞队列的使用效率。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值