lesson2:java阻塞队列的demo及源码分析
本文向大家展示了java阻塞队列的使用场景、源码分析及特定场景下的使用方式。java的阻塞队列是jdk1.5之后在并发包中提供的一组队列,主要的使用场景是在需要使用生产者消费者模式时,用户不必再通过多线程自己实现,可以通过阻塞队列直接实现消息的分发和消费,方便简单,降低了开发难度,在本章的最后,我们在分析阻塞队列源码时,也会有demo展示因为对代码的不了解而错误的使用阻塞队列时的灾难情况。下面列举出了所有实现BlockingQueue接口的队列:ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue、DelayQueue、LinkedTransferQueue、LinkedBlockingDeque和SynchronousQueue。本文主要介绍LinkedBlockingQueue相关的使用场景及源码分析,关于其它的阻塞队列,后面我会在额外的章节做详细的介绍。
demo源码:https://github.com/mantuliu/javaAdvance 中的类Lesson2BlockingQueueDemo
我们先看一下BlockingQueue接口的几个主要方法在LinkedBlockingQueue:add(),offer(),put(),take(),poll();
a.首先来看add(E e)方法,此方法是在LinkedBlockingQueue的父类AbstractQueue中实现的,下面的代码展示了add的实现方法,就是直接调用offer()方法:
public boolean add(E e) { if (offer(e)) return true; else throw new IllegalStateException("Queue full"); }
b.我们来看看第二个方法,offer(E e)的实现:
public boolean offer(E e) { if (e == null) throw new NullPointerException();//判断新增的元素如果是空,则直接抛出异常 final AtomicInteger count = this.count;//自增整数 if (count.get() == capacity)//判断容量是否已经到达最大值,已经达到变直接返回,后面我们会看到put方法的不一样地方 return false; int c = -1; Node<E> node = new Node(e);//将新增的节点e包装成节点Node final ReentrantLock putLock = this.putLock;//获取阻塞队列的入队列的锁,可以想的到,此队列还有一个出队列的锁 putLock.lock();//将入队列的锁上锁 try { if (count.get() < capacity) {//上锁之后再次判断容量是否达到最大值 enqueue(node);//将元素入队列 c = count.getAndIncrement();//c的值是之前队列元素的数量 if (c + 1 < capacity)//此元素存入到队列后队列所有元素的数量和依然小于容量 notFull.signal();//Condition notFull发出信号通知给关注此信号量的线程 } } finally { putLock.unlock();//释放锁 } if (c == 0) signalNotEmpty();//信号通知,具体见下面的分析 return c >= 0; }
private void enqueue(Node<E> node) {
// assert putLock.isHeldByCurrentThread();
// assert last.next == null;
last = last.next = node;//入队列,链表操作
}
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
notEmpty.signal();//Condition notEmpty发出信号通知给关注此信号量的线程,主要是当队列元素为空时,take()方法已经处于等待状态,这时有元素进入到队列需要唤醒
} finally {
takeLock.unlock();
}
}
c.我们再来分析一下offer(E e, long timeout, TimeUnit unit),通过下面的分析,我们可以看出offer(E e, long timeout, TimeUnit unit)与offer(E e)方法的区别是当队列元素的数量已经达到容量上限时,offer(E e, long timeout, TimeUnit unit)会等待timeout的时间,再这个过程中会循环判断元素是否可以进入队列,最后在超时后,还没有进入队列,则丢弃此元素,返回入队列失败。
public boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException { if (e == null) throw new NullPointerException();//存储的元素为null,直接抛出异常 long nanos = unit.toNanos(timeout);//计算超时时间 int c = -1; final ReentrantLock putLock = this.putLock;//拿到put锁 final AtomicInteger count = this.count;//拿到已有的元素数量 putLock.lockInterruptibly();//上锁,准备存入元素 try { while (count.get() == capacity) {//循环判断是否达到容量的上限,如果没到容量上限,则不进入while循环 if (nanos <= 0)//如果剩余等待时间已经小于0,则直接返回添加元素失败 return false; nanos = notFull.awaitNanos(nanos);//线程终止,释放put锁,等待notFull的condition的通知 } enqueue(new Node<E>(e));//入队列,下面与offer(E e)方法相同 c = count.getAndIncrement(); if (c + 1 < capacity) notFull.signal(); } finally { putLock.unlock(); } if (c == 0) signalNotEmpty(); return true; }
d.put(E e)方法,put(E e)方法与offer(E e)方法的主要区别就在于,如果队列元素已满,则put()方法的线程一直处于等待状态,对于put()方法的使用,如果我们的业务系统每有设计好,很可能会带来灾难性的后果,后面我会有一个demo代码来分析解释。
public void put(E e) throws InterruptedException { if (e == null) throw new NullPointerException(); // Note: convention in all put/take/etc is to preset local var // holding count negative to indicate failure unless set. int c = -1; Node<E> node = new Node(e); final ReentrantLock putLock = this.putLock; final AtomicInteger count = this.count; putLock.lockInterruptibly(); try { /* * Note that count is used in wait guard even though it is * not protected by lock. This works because count can * only decrease at this point (all other puts are shut * out by lock), and we (or some other waiting put) are * signalled if it ever changes from capacity. Similarly * for all other uses of count in other wait guards. */ while (count.get() == capacity) {//put方法与offer方法的主要区别就在这里,如果队列元素已经满了,线程处于一直等待状态 notFull.await(); } enqueue(node); c = count.getAndIncrement(); if (c + 1 < capacity) notFull.signal(); } finally { putLock.unlock(); } if (c == 0) signalNotEmpty(); }
e.E take()方法,此处注意,我们用到了读锁,由于take()和offer()、put()用的锁不是同一把锁,所以他们之间互不干扰,唯一的交集是队列元素的数量,这也是队列元素的数量用AtomicInteger来记录的原因,因为AtomicInteger的加减操作都是原子操作。
public E take() throws InterruptedException { E x; int c = -1; final AtomicInteger count = this.count; final ReentrantLock takeLock = this.takeLock;//注意这里获取的是读锁 takeLock.lockInterruptibly();//加读锁 try { while (count.get() == 0) {//循环判断队列元素是否为空 notEmpty.await();//等待非空信号 } x = dequeue();//出队列 c = count.getAndDecrement();//元素个数减一操作 if (c > 1) notEmpty.signal();//如果剩余元素数大于0,发出notEmpty信号 } finally { takeLock.unlock();//释放锁 } if (c == capacity) signalNotFull();//发出未满信号 return x; }
f.E poll()方法,poll()是一个非阻塞的取元素的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | public E poll() { final AtomicInteger count = this .count; if (count.get() == 0 ) return null ; E x = null ; int c = - 1 ; final ReentrantLock takeLock = this .takeLock; takeLock.lock(); try { if (count.get() > 0 ) { x = dequeue(); c = count.getAndDecrement(); if (c > 1 ) notEmpty.signal(); } } finally { takeLock.unlock(); } if (c == capacity) signalNotFull(); return x; } |
g.E poll(long timeout, TimeUnit unit),当队列元素为空时,阻塞timeout,循环取元素,如果超时后未取到,则直接返回
public E poll(long timeout, TimeUnit unit) throws InterruptedException { E x = null; int c = -1; long nanos = unit.toNanos(timeout); final AtomicInteger count = this.count; final ReentrantLock takeLock = this.takeLock; takeLock.lockInterruptibly(); try { while (count.get() == 0) { if (nanos <= 0) return null; nanos = notEmpty.awaitNanos(nanos); } x = dequeue(); c = count.getAndDecrement(); if (c > 1) notEmpty.signal(); } finally { takeLock.unlock(); } if (c == capacity) signalNotFull(); return x; }
h.构造方法源码分析
public LinkedBlockingQueue() { this(Integer.MAX_VALUE);//默认容量是整数最大值 } /** * Creates a {@code LinkedBlockingQueue} with the given (fixed) capacity. * * @param capacity the capacity of this queue * @throws IllegalArgumentException if {@code capacity} is not greater * than zero */ public LinkedBlockingQueue(int capacity) {//可以自行设置队列容量 if (capacity <= 0) throw new IllegalArgumentException(); this.capacity = capacity; last = head = new Node<E>(null); }
以我的经验,一般我们在使用生产者-消费者模式时,所使用的消费方法,都是take(),比较方便简单,poll()方法在特殊情况下才会使用到。因为阻塞队列有容量的限制,一旦发生容量已满(可能是消费者线程挂了或者消费速度太慢),并且入队列的方法是put或offer(timeout)的方法,线程就会一直等待,目前我们的业务系统大多数都是在多线程的环境下运行,就会造成线程被耗光导致整个服务停服,就算使用了线程池,也会造成线程池内的工作线程全部被耗光,线程池不能再提供服务,下面的demo模拟展示了线程被快速耗光停止服务的情况:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | package com.mantu.advance; import java.util.concurrent.*; /** * blog http://www.cnblogs.com/mantu/ * github https://github.com/mantuliu/ * @author mantu * */ public class Lesson2BlockingQueueDemo { public static LinkedBlockingQueue queue = new LinkedBlockingQueue( 5 ); //声明阻塞队列的数量为5, public static void main(String [] args){ Receiver receiver = new Receiver(); //消费者 for ( int i = 0 ;i< 20 ;i++){ new Thread( new SenderPut()).start(); //发送者,20个线程同时发送,在生产环境,可能会有成千上万的线程同时发送 } /* for(int i = 0;i<20;i++){ new Thread(new SenderOffer()).start();//大家可以试一下offer方法与put方法完全不一样 } */ try { Thread.currentThread().sleep(3000L); //停顿3秒钟 } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } new Thread(receiver).start(); //消费者线程启动 } } class SenderPut implements Runnable { @Override public void run() { // TODO Auto-generated method stub try { System.out.println( "已经进入线程id:" +Thread.currentThread().getId()+ "的内部" ); //标识此线程已经被执行 Lesson2BlockingQueueDemo.queue.put(Thread.currentThread().getId()); System.out.println( "当前发送的线程id为:" +Thread.currentThread().getId()); //标识此线程已经发送消息到队列完毕 } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } class SenderOffer implements Runnable { @Override public void run() { // TODO Auto-generated method stub try { System.out.println( "已经进入线程id:" +Thread.currentThread().getId()+ "的内部" ); Lesson2BlockingQueueDemo.queue.offer(Thread.currentThread().getId()); System.out.println( "当前发送的线程id为:" +Thread.currentThread().getId()); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } } class Receiver implements Runnable { @Override public void run() { // TODO Auto-generated method stub while ( true ){ try { System.out.println( "当前取出的线程id为:" +Lesson2BlockingQueueDemo.queue.take()); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } |
执行结果如下,执行结果证明了我们之前的结论:线程全部被耗光。
已经进入线程id:10的内部
已经进入线程id:11的内部
已经进入线程id:9的内部
已经进入线程id:12的内部
已经进入线程id:15的内部
已经进入线程id:16的内部
已经进入线程id:19的内部
已经进入线程id:20的内部
已经进入线程id:21的内部
已经进入线程id:18的内部
已经进入线程id:24的内部
已经进入线程id:17的内部
已经进入线程id:28的内部
当前发送的线程id为:15
已经进入线程id:14的内部
已经进入线程id:22的内部
当前发送的线程id为:9
当前发送的线程id为:12
当前发送的线程id为:10
当前发送的线程id为:11
已经进入线程id:25的内部
已经进入线程id:13的内部
已经进入线程id:27的内部
已经进入线程id:23的内部
已经进入线程id:26的内部
当前取出的线程id为:11
当前发送的线程id为:16
当前发送的线程id为:19
当前取出的线程id为:10
当前取出的线程id为:12
当前发送的线程id为:20
当前取出的线程id为:9
当前发送的线程id为:21
当前发送的线程id为:18
当前取出的线程id为:15
当前取出的线程id为:16
当前取出的线程id为:19
当前发送的线程id为:17
当前发送的线程id为:24
当前发送的线程id为:28
当前取出的线程id为:20
当前取出的线程id为:21
当前发送的线程id为:14
当前发送的线程id为:22
当前取出的线程id为:18
当前取出的线程id为:24
当前发送的线程id为:25
当前发送的线程id为:13
当前取出的线程id为:17
当前取出的线程id为:28
当前发送的线程id为:27
当前发送的线程id为:23
当前取出的线程id为:14
当前取出的线程id为:22
当前发送的线程id为:26
当前取出的线程id为:25
当前取出的线程id为:13
当前取出的线程id为:27
当前取出的线程id为:23
当前取出的线程id为:26
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?