摘要:本文通过完整的案例复现来演示在什么情况会触发该问题,同时给出了处理建议。希望读者在编程时加以借鉴,避免再次遇到此类问题。

本文分享自华为云社区《Priority Blocking Queue比较器异常导致的NPE问题分析》,作者:谢照昆、王嘉伟。

编者按:笔者在使用PriorityBlockingQueue实现按照优先级处理任务时遇到一类NPE问题,经过分析发现根本原因是在任务出队列时调用比较器异常,进而导致后续任务出队列抛出NullPointerException。本文通过完整的案例复现来演示在什么情况会触发该问题,同时给出了处理建议。希望读者在编程时加以借鉴,避免再次遇到此类问题。

背景知识

PriorityBlockingQueue是一个无界的基于数组的优先级阻塞队列,使用一个全局ReentrantLock来控制某一时刻只有一个线程可以进行元素出队和入队操作,并且每次出队都返回优先级别最高的或者最低的元素。PriorityBlockingQueue通过以下两种方式实现元素优先级排序:

  1. 入队元素实现Comparable接口来比较元素优先级;
  2. PriorityBlockingQueue构造函数指定Comparator来比较元素优先级;

关于PriorityBlockingQueue中队列操作的部分,基本和PriorityQueue逻辑一致,只不过在操作时加锁了。在本文中我们主要关注PriorityBlockingQueue出队的take方法,该方法通过调用dequeue方法将元素出队列。当没有元素可以出队的时候,线程就会阻塞等待。

public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
E result;
try {
// 尝试获取最小元素,即小顶堆第一个元素,然后重新排序,如果不存在表示队列暂无元素,进行阻塞等待。
while ( (result = dequeue()) == null)
notEmpty.await();
} finally {
lock.unlock();
}
return result;
}

现象

在某个业务服务中使用PriorityBlockingQueue实现按照优先级处理任务,某一天环境中的服务突然间不处理任务了,查看后台日志,发现一直抛出NullPointerException。将进程堆dump出来,使用MAT发现某个PriorityBlockingQueue中的size值比实际元素个数多1个(入队时已经对任务进行非空校验)。

异常堆栈如下:

java.lang.NullPointerException
at java.util.concurrent.PriorityBlockingQueue.siftDownComparable(PriorityBlockingQueue.java:404)
at java.util.concurrent.PriorityBlockingQueue.dequeue(PriorityBlockingQueue.java:333)
at java.util.concurrent.PriorityBlockingQueue.take(PriorityBlockingQueue.java:548)
...

MAT结果:

原因分析

在此我们分析下PriorityBlockingQueue是如何出队列的,PriorityBlockingQueue最终通过调用dequeue方法出队列,dequeue方法处理逻辑如下:

  1. 将根节点(array[0])赋值给result;
  2. array[n] 赋值给 arrary[0];
  3. 将 array[n] 设置为 null;
  4. 调用siftDownComparable或siftDownUsingComparator对队列元素重新排序;
  5. size大小减1;
  6. 返回result;

如果在第4步中出现异常,就会出现队列中的元素个数比实际的元素个数多1个的现象。此时size未发生改变,arry[n]已经被置为null,再进行siftDown操作时就会抛出NullPointerException。继续分析第4步中在什么情况下会出现异常,通过代码走读我们可以发现只有在调用Comparable#compareTo或者Comparator#compare方法进行元素比较的时候才可能出现异常。这块代码的处理逻辑和业务相关,如果业务代码处理不当抛出异常,就会导致上述现象。

 /**
* Mechanics for poll(). Call only while holding lock.
*/
private E dequeue() {
int n = size - 1;
if (n < 0)
return null;
else {
Object[] array = queue;
E result = (E) array[0]; //step1
E x = (E) array[n]; //step2
array[n] = null; //step3
Comparator<? super E> cmp = comparator;
if (cmp == null) //step4 如果指定了comparator,就按照指定的comparator来比较。否则就按照默认的
siftDownComparable(0, x, array, n);
else
siftDownUsingComparator(0, x, array, n, cmp);
size = n; //step5
return result; //step6
}
}
private static <T> void siftDownComparable(int k, T x, Object[] array, int n) {
if (n > 0) {
Comparable<? super T> key = (Comparable<? super T>)x;
int half = n >>> 1;
while (k < half) {
int child = (k << 1) + 1;
Object c = array[child];
int right = child + 1;
if (right < n && ((Comparable<? super T>) c).compareTo((T) array[right]) > 0)
c = array[child = right];
if (key.compareTo((T) c) <= 0)
break;
array[k] = c;
k = child;
}
array[k] = key;
}
}
private static <T> void siftDownUsingComparator(int k, T x, Object[] array, int n,
Comparator<? super T> cmp) {
if (n > 0) {
int half = n >>> 1;
while (k < half) {
int child = (k << 1) + 1;
Object c = array[child];
int right = child + 1;
if (right < n && cmp.compare((T) c, (T) array[right]) > 0)
c = array[child = right];
if (cmp.compare(x, (T) c) <= 0)
break;
array[k] = c;
k = child;
}
array[k] = x;
}
}

复现代码

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.PriorityBlockingQueue;
public class PriorityBlockingQueueTest {
static class Entity implements Comparable<Entity> {
private int id;
private String name;
private boolean flag;
public void setFlag(boolean flag) {
this.flag = flag;
}
public Entity(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public int compareTo(Entity entity) {
if(flag) {
throw new RuntimeException("Test Exception");
}
if (entity == null || this.id > entity.id) {
return 1;
}
return this.id == entity.id ? 0 : -1;
}
}
public static void main(String[] args) {
int num = 5;
PriorityBlockingQueue<Entity> priorityBlockingQueue = new PriorityBlockingQueue<>();
List<Entity> entities = new ArrayList<>();
for (int i = 0; i < num; i++) {
Entity entity = new Entity(i, "entity" + i);
entities.add(entity);
priorityBlockingQueue.offer(entity);
}
entities.get(num - 1).setFlag(true);
int size = entities.size();
for (int i = 0; i < size; i++) {
try {
priorityBlockingQueue.take();
} catch (Exception e) {
e.printStackTrace();
}
}
}

执行结果如下:

java.lang.RuntimeException: Test Exception
at PriorityBlockingQueueTest$Entity.compareTo(PriorityBlockingQueueTest.java:31)
at PriorityBlockingQueueTest$Entity.compareTo(PriorityBlockingQueueTest.java:8)
at java.util.concurrent.PriorityBlockingQueue.siftDownComparable(PriorityBlockingQueue.java:404)
at java.util.concurrent.PriorityBlockingQueue.dequeue(PriorityBlockingQueue.java:333)
at java.util.concurrent.PriorityBlockingQueue.take(PriorityBlockingQueue.java:548)
at PriorityBlockingQueueTest.main(PriorityBlockingQueueTest.java:71)
java.lang.NullPointerException
at java.util.concurrent.PriorityBlockingQueue.siftDownComparable(PriorityBlockingQueue.java:404)
at java.util.concurrent.PriorityBlockingQueue.dequeue(PriorityBlockingQueue.java:333)
at java.util.concurrent.PriorityBlockingQueue.take(PriorityBlockingQueue.java:548)
at PriorityBlockingQueueTest.main(PriorityBlockingQueueTest.java:71)

规避方案

可以通过以下两种方法规避:

  • 在take方法出现NPE时,清除队列元素,将未处理的元素重新进入队列;
  • 在 Comparable#compareTo 或 Comparator#compare 方法中做好异常处理,对异常情况进行默认操作;

建议使用后者。

案例引申

使用PriorityBlockingQueue作为缓存队列来创建线程池时,使用submit提交任务会出现 java.lang.ClassCastException: java.util.concurrent.FutureTask cannot be cast to 异常,而使用execute没有问题。

观察submit源码可以发现在submit内部代码会将Runable封装成RunnableFuture对象,然后调用execute提交任务。

public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}

以Comparable为例,任务入队列时,最终会调用siftUpComparable方法。该方法第一步将RunnableFuture强转为Comparable类型,而RunnableFuture类未实现Comparable接口,进而抛出ClassCastException异常。

public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
final ReentrantLock lock = this.lock;
lock.lock();
int n, cap;
Object[] array;
while ((n = size) >= (cap = (array = queue).length))
tryGrow(array, cap);
try {
Comparator<? super E> cmp = comparator;
if (cmp == null)
siftUpComparable(n, e, array);
else
siftUpUsingComparator(n, e, array, cmp);
size = n + 1;
notEmpty.signal();
} finally {
lock.unlock();
}
return true;
}
private static <T> void siftUpComparable(int k, T x, Object[] array) {
Comparable<? super T> key = (Comparable<? super T>) x;
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = array[parent];
if (key.compareTo((T) e) >= 0)
break;
array[k] = e;
k = parent;
}
array[k] = key;
}

这也是常见的比较器调用异常案例,本文不再赘述,可自行参考其他文章。

总结

在使用PriorityBlockingQueue时,注意在比较器中做好异常处理,避免出现类似问题。

后记

如果遇到相关技术问题(包括不限于毕昇 JDK),可以进入毕昇 JDK 社区查找相关资源(点击阅读原文进入官网),包括二进制下载、代码仓库、使用教学、安装、学习资料等。

点击关注,第一时间了解华为云新鲜技术~

案例复现,带你分析Priority Blocking Queue比较器异常导致的NPE问题的更多相关文章

  1. GIS空间分析案例教程——带背景和周围要素的逐要素导出地理

    GIS空间分析案例教程--带背景和周围要素的逐要素导出地理 商务合作,科技咨询,版权转让:向日葵,135-4855__4328,xiexiaokui#qq.com 目的:导出多边形要素类的每个要素 实 ...

  2. 老李案例分享:MAT分析应用程序服务出现内存溢出过程

    老李案例分享:MAT分析应用程序服务出现内存溢出过程   poptest是国内唯一一家培养测试开发工程师的培训机构,以学员能胜任自动化测试,性能测试,测试工具开发等工作为目标.在poptest的loa ...

  3. Android Priority Job Queue (Job Manager):多重不同Job并发执行并在前台获得返回结果(四)

     Android Priority Job Queue (Job Manager):多重不同Job并发执行并在前台获得返回结果(四) 在Android Priority Job Queue (Jo ...

  4. Java Blocking Queue

    //Listing 8-1. The Blocking Queue Equivalent of Chapter 3’s PC Application import java.util.concurre ...

  5. Android Priority Job Queue (Job Manager):后台线程任务结果传回前台(三)

     Android Priority Job Queue (Job Manager):后台线程任务结果传回前台(三) 在附录文章4,5的基础上改造MainActivity.java和MyJob.ja ...

  6. Android Priority Job Queue (Job Manager):线程任务的容错重启机制(二)

     Android Priority Job Queue (Job Manager):线程任务的容错重启机制(二) 附录文章4简单介绍了如何启动一个后台线程任务,Android Priority J ...

  7. Android Priority Job Queue (Job Manager)(一)

     Android Priority Job Queue (Job Manager)(一) 一.为什么要引入Android Priority Job Queue (Job Manager)?如今的A ...

  8. GIS案例学习笔记-水文分析河网提取地理建模

    GIS案例学习笔记-水文分析河网提取地理建模 联系方式:谢老师,135-4855-4328,xiexiaokui#qq.com 目的:针对数字高程模型,通过水文分析,提取河网 操作时间:25分钟 数据 ...

  9. C++ 如何用百行代码实现线程安全的并发队列 | concurrent queue or blocking queue implemented in cpp

    本文首发于个人博客https://kezunlin.me/post/cabccf5c/,欢迎阅读最新内容! concurrent queue or blocking queue implemented ...

随机推荐

  1. Fail2ban 命令详解 fail2ban-server

    Fail2ban的服务端操作命令,用于启动一个Fail2ban服务. root@local:~# fail2ban-server --help Usage: /usr/bin/fail2ban-ser ...

  2. TypeError: this.getOptions is not a function

    我在vue ui界面中安装版本依赖包后报这个错误 less-loader/sass-loader安装的版本过高 解决办法 删除原有的版本依赖包,安装更低版本的依赖包. 如 @6.0.1为选择安装的版本 ...

  3. uni-simple-router

    目录 uni-simple-router 一.快速上手 扩一:webpack插件之DefinePlugin 扩二:uni-read-pages 如何获取pages.json中的路由 二.H5模式 2. ...

  4. Code Forces 1367A Sorting Parts 题解

    (原题链接:CF传送门) 题目背景(我看不懂英文嘤嘤嘤) Sorting Parts You have an array a of length n. You can exactly once sel ...

  5. JAVA - 如何判断一个类是无用的类

    JAVA - 如何判断一个类是无用的类 方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢? 判定一个常量是否是"废弃常量"比较简单,而要判定一个类是否是"无用 ...

  6. 2 万字 + 20张图| 细说 Redis 九种数据类型和应用场景

    作者:小林coding 计算机八股文网(操作系统.计算机网络.计算机组成.MySQL.Redis):https://xiaolincoding.com 大家好,我是小林. 我们都知道 Redis 提供 ...

  7. Windows系统开启显示文件名后缀

    更新记录 2022年4月16日:本文迁移自Panda666原博客,原发布时间:2021年8月26日. 通常Windows系统根据文件名称的后缀来确定文件的类型.经常让朋友出现软件方面的问题,让其修改一 ...

  8. 讲一个linux服务启动报错问题排查

    例子 首先我们在/usr/lib/systemd/system目录下创建一个服务文件,写下服务启动任务配置.下面我以prometheus的node_exporter为例 vim /usr/lib/sy ...

  9. Kubernetes-23:详解如何将CPU Manager做到游刃有余

    k8s中为什么要用CPU Manager? 默认情况下,kubelet 使用CFS配额来执行 Pod 的 CPU 约束.Kubernetes的Node节点会运行多个Pod,其中会有部分的Pod属于CP ...

  10. RPA SAP财务内部对账机器人

    [简介] 本机器人用于使用SAP软件的集团公司间往来对账前台登录SAP账户和密码,需退出PC微信,输入法切换为英文半角状态. [详细流程] 1.清空Excel-VBA管理工具原始数据 2.输入对账时间 ...