JUC 小试牛刀:从源码分析「ArrayBlockingQueue」,Java自带的线程安全的、有界的阻塞队列

场景引入

正如标题所言,ArrayBlockingQueue是一个阻塞队列的数组实现,如果要生动地描述阻塞队列的应用场景,我想还是餐厅取餐、出餐的场景是最合适的(虽然我想这个场景已经被用烂了)。

试想在学校的窗口取餐,一个窗口是预制好的烤肉拌饭,另一个窗口则是现炒的小炒菜,要如何选择?先把目光移向烤肉拌饭,因为出餐太快,预制好的菜已经放满了窗台前,出餐阿姨只能在窗口闲等,等哪个着急的学生过来拿走一份饭,她才能继续向上摆。再看向小炒菜,由于味道美味,很多学生都想吃,但炒菜的出餐量有限,学生没法立即享受到美味,来晚的只能排队等待。哪怕学生再想吃,窗台前都是空的,没得拿,只有出餐了才能拿。

这就是阻塞队列的典型场景,烤肉拌饭的窗台口已经摆满饭了,阿姨只能阻塞等待有空了再放餐(put操作)。小炒菜的窗口太火爆了,学生要想吃只能阻塞等待,等轮到自己了在取餐(take操作)。

API介绍

放餐操作:add、offer、put

add: 如果队列容量未满,则插入指定元素; 成功时返回 true,若当前无空位则抛出【IllegalStateException】

offer: 如果队列容量未满,则插入指定元素; 成功时返回 true,若当前无空位则抛出 false

put: 如果队列容量未满,则插入指定元素; 如果满了,则阻塞等待有空位。如果等待时被打断,则抛异常

offer(long timeout, TimeUnit unit): 超时等待的put()。如果等着空了返回true,如果超过timeout还是没有空位,返回false

取餐操作:take、poll

poll: 如果队列容量不为空,则取出队列头部并返回。如果为空返回null

take: 如果队列容量不为空,则取出队列头部并返回。如果为空则阻塞等待

poll(long timeout, TimeUnit unit): 超时等待的take(),如果队列容量不为空,则取出队列头部并返回。如果为空则阻塞等待指定时间。如果超过时间还为空,则返回null

源码分析

ArrayBlockingQueue的精髓之处就是阻塞等待的处理,因此我将详细分析阻塞 take, put 相关的源码,理解"阻塞"的灵魂所在。

在此之前,我们要先理解ArrayBlockingQueue的结构。从名字来看,这个阻塞队列是以Array为基础的,因此有界就很好理解。它是以数组为基础的,数组的分配必须是连续的空间,这段连续的空间就是阻塞队列的容量。

同时,我开篇也提到了,这样的阻塞队列必须是线程安全的,它底层使用了ReentrantLock来保证线程的安全。

为什么还要有Condition?等我们下文看源码就能知道了。

enqueue、dequeue:入队出队的底层

ini 复制代码
//入队操作

private void enqueue(E e) {      
  final Object[] items = this.items;    
  items[putIndex] = e;    
  if (++putIndex == items.length) 
    putIndex = 0;    //如果当前元素入队后队列已满,重置索引,下一次再从队末进
  count++;    
  notEmpty.signal();//如果元素加入到空队列中,则唤醒阻塞等待的元素
}

为什么不直接操作items,反而要先定义一个局部变量 final Object[] items = this.items 来接收它?

一、避免多次访问堆内存,提升执行效率

首先要明确 Java 的内存访问特性:

  • this.items

    是类的成员变量,存储在堆内存 中,每次访问 this.items 都需要通过 this 引用(堆地址)去寻址,相对耗时;

  • 局部变量 items 存储在栈内存中,栈内存的访问速度远快于堆内存,而且局部变量的寻址是直接的,无需额外间接引用。

enqueue 方法中多次用到了 itemsitems[putIndex] = eputIndex == items.length),如果直接用 this.items,会多次触发堆内存寻址;而先把 this.items 赋值给局部变量,后续只需要访问栈上的局部变量即可,减少了堆内存访问次数,提升了执行效率。

二、防止指令重排导致的可见性问题,保证线程安全

ArrayBlockingQueue 是线程安全类,items 是共享成员变量,虽然有 ReentrantLock 保证原子性,但 Java 虚拟机的指令重排可能会导致潜在的可见性问题

简单说:final 局部变量相当于给 items 数组拍了一张 "快照",确保整个 enqueue 方法执行期间,使用的是同一个数组引用,不会因为 JVM 优化而读取到错误的数组对象。

ini 复制代码
//出队操作
private E dequeue() {      
  final Object[] items = this.items;    
  @SuppressWarnings("unchecked")    
  E e = (E) items[takeIndex];    //取出队列头部元素
  items[takeIndex] = null;    //设为null
  if (++takeIndex == items.length) 
    takeIndex = 0;    //与入队对应,回到队末
  count--;    
  if (itrs != null)        
    itrs.elementDequeued();    
  notFull.signal();   //唤醒因队列满而阻塞等待的队列,队列有空了 
  return e;
}

itrs是干什么的?

itrsArrayBlockingQueue 用来管理当前所有活跃迭代器(Iterator)的集合,目的是保证迭代器的弱一致性,避免迭代过程中出现异常或错误的元素引用

当队列执行 dequeue 出队操作(删除队首元素)时,会调用 itrs.elementDequeued(),核心目的是通知所有活跃迭代器:"某个元素被删除了,你们需要更新自己的状态,避免遍历出错"

scss 复制代码
void elementDequeued() {    
// assert lock.isHeldByCurrentThread();    
if (count == 0)        
  queueIsEmpty();    
else if (takeIndex == 0)        
  takeIndexWrapped();
}

queueIsEmpty() :在本次出队后队列变空时,重置迭代器状态(``nextIndex=-1remaining=0),标记迭代器遍历完毕,避免无效遍历;

takeIndexWrapped() :在本次出队后 ``takeIndex 循环回绕到数组开头时,修正迭代器的 nextWrapped 状态,保证迭代器能正确适配队列的循环结构;

put、take:入队出队的实现

csharp 复制代码
public void put(E e) throws InterruptedException {    
  Objects.requireNonNull(e);    
  final ReentrantLock lock = this.lock;    
  lock.lockInterruptibly();    
  try {        
    while (count == items.length)            
      notFull.await();        //如果队列满了,阻塞等待
    enqueue(e);    
  } finally {        
    lock.unlock();    
  }
}
public E take() throws InterruptedException {    
  final ReentrantLock lock = this.lock;    
  lock.lockInterruptibly();    
  try {        
    while (count == 0)            
      notEmpty.await();     //如果队列是空的,阻塞等待   
    return dequeue();    
  } finally {        
    lock.unlock();    
  }
}

可以看到,ArrayBlockingQueue使用了ReentrantLock保证了入队出队的线程安全,同时使用了条件变量实现了当队列为空/队列满了的情况下的阻塞等待。

思考

  1. 可以看到,take、put操作使用的都是同一把ReentrantLock,这说明这两个操作是会互相阻塞的。怎么样才能让这两个操作独立起来?

  2. 这个数组阻塞队列是强制有界的,如果不确定队列大小的情况下,该怎么处理?

这两个问题在 LinkedBlockingQueue 都给出了答复,有兴趣的可以自行了解。

我是沐浴露zz,将持续更新有趣的技术,欢迎互动交流!

相关推荐
nnsix2 小时前
Unity SenseGlove力反馈手套 基础配置
java·unity·游戏引擎
百***24372 小时前
GPT-Image 1.5 vs Nano Banana Pro 深度对比:国内业务落地的场景适配与避坑指南
java·数据库·gpt
李广坤2 小时前
Rust常用集合
后端
代码栈上的思考2 小时前
MyBatis——动态SQL讲解
java·开发语言·数据库
@淡 定2 小时前
JVM调优参数配置详解
java·jvm·算法
总会落叶2 小时前
Spring AOP 面向切面编程完全指南 🚀
后端
撩得Android一次心动2 小时前
Android 四大组件——Service(服务)【基础篇2】
android·java·服务·四大组件·android 四大组件
爱宇阳2 小时前
在 Docker 环境中为 GitLab 实例配置邮件服务器
java·docker·gitlab
Moment2 小时前
到底选 Nuxt 还是 Next.js?SEO 真的有那么大差距吗 🫠🫠🫠
前端·javascript·后端