最近工作中有接触到DelayQueue,网上搜索资料的时候发现一篇文章谈到DelayQueue的坑。点击打开链接

文中已经总结了遇到坑的地方,还有解决方案。不过我第一眼看一下没弄明白为什么,所以翻了翻源码深究了一下,下面把这个坑的原因以及原理分析一下。

首先是DelayQueue的take()方法:

     public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
for (;;) {
E first = q.peek();
if (first == null)
available.await();
else {
long delay = first.getDelay(NANOSECONDS); //
if (delay <= 0)
return q.poll();
first = null; // don't retain ref while waiting
if (leader != null)
available.await();
else {
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
available.awaitNanos(delay); //
} finally {
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
if (leader == null && q.peek() != null)
available.signal();
lock.unlock();
}
}

首先看到注释2,这是一个带时间的await方法,时间单位是纳秒,传入的参数delay是从注释1通过调用first对象的getDelay方法获取的。first对象是E类型的,E是一个实现了Delayed接口的泛型。

这里看看接口Delayed的源码:

 public interface Delayed extends Comparable<Delayed> {

     /**
* Returns the remaining delay associated with this object, in the
* given time unit.
*
* @param unit the time unit
* @return the remaining delay; zero or negative values indicate
* that the delay has already elapsed
*/
long getDelay(TimeUnit unit);
}

就只有一个getDelay(TimeUnit)方法,它返回的指定的TimeUnit的时间长度。显然,具体的实现类要实现该方法才行。

那么来看一下具体的getDelay(TimeUnit)方法的实现吧,我看了几篇文章,基本上大同小异,都是如下这般实现的:

     public long getDelay(TimeUnit unit) {
return unit.convert(this.expire - System.currentTimeMillis() , TimeUnit.MILLISECONDS);
}

原博主很贴心的提醒了,这个地方convert方法的第二个参数,应该要使用TimeUnit.MILLISECONDS而不是TimeUnit.NANOSECONDS(虽然不管使用什么时间单位都不会导致程序出现错误的结果,但是用错了时间单位的话,CPU可就遭殃了)。那么为什么会一定要强调要使用MILLISECONDS这个单位呢?

继续看看convert方法的源码吧,在TimeUnit枚举类中,定义了若干时间单位,他们有各自的convert方法的实现,先来看看TimeUnit.NANOSECONDS的:

     NANOSECONDS {
public long toNanos(long d) { return d; }
public long toMicros(long d) { return d/(C1/C0); }
public long toMillis(long d) { return d/(C2/C0); }
public long toSeconds(long d) { return d/(C3/C0); }
public long toMinutes(long d) { return d/(C4/C0); }
public long toHours(long d) { return d/(C5/C0); }
public long toDays(long d) { return d/(C6/C0); }
public long convert(long d, TimeUnit u) { return u.toNanos(d); }
int excessNanos(long d, long m) { return (int)(d - (m*C2)); }
},

可以看到,convert方法又直接调用了TimeUnit.toNanos方法,直接就把第一个参数d当做一个纳秒的时间长度给返回了。

同理看看TimeUnit.MILLISECONDS定义的方法:

     MILLISECONDS {
public long toNanos(long d) { return x(d, C2/C0, MAX/(C2/C0)); } //static final long C0 = 1L; static final long C1 = C0 * 1000L;static final long C2 = C1 * 1000L;
public long toMicros(long d) { return x(d, C2/C1, MAX/(C2/C1)); }
public long toMillis(long d) { return d; }
public long toSeconds(long d) { return d/(C3/C2); }
public long toMinutes(long d) { return d/(C4/C2); }
public long toHours(long d) { return d/(C5/C2); }
public long toDays(long d) { return d/(C6/C2); }
public long convert(long d, TimeUnit u) { return u.toMillis(d); }
int excessNanos(long d, long m) { return 0; }
},

回到我们的实际使用场景,take方法中long delay = first.getDelay(NANOSECONDS);  ->  NANOSECONDS.convert(long d, TimeUnit u)  ->  u.toNanos(d)。如果我们在getDelay方法实现中,convert方法第二个参数传入的是NANOSECONDS,那么就直接返回d;如果convert方法第二个参数传入的是MILLISECONDS,那么返回就是MILLISECONDS.toNanos(d),得到的结果就是1000*1000*d。

可以发现,convert方法的第二个参数TimeUnit,实际上是跟着第一个参数d的时间单位走的。如果实现时候直接使用time - System.currentTimeMillis()作为第一个参数,实际上它的时间单位确实应该是MILLISECONDS,那么如果第二个参数传错了为NANOSECONDS,那就导致take方法中的awaitNanos方法等待时间缩短了1000*1000倍,这样带来的cpu空转压力是巨大的。

分析了这么多,其实看看jdk中TimeUnit类对convert方法的注释,很容易就理解了:

    /**
* Converts the given time duration in the given unit to this unit.
* Conversions from finer to coarser granularities truncate, so
* lose precision. For example, converting {@code 999} milliseconds
* to seconds results in {@code 0}. Conversions from coarser to
* finer granularities with arguments that would numerically
* overflow saturate to {@code Long.MIN_VALUE} if negative or
* {@code Long.MAX_VALUE} if positive.
*
* <p>For example, to convert 10 minutes to milliseconds, use:
* {@code TimeUnit.MILLISECONDS.convert(10L, TimeUnit.MINUTES)}
*
* @param sourceDuration the time duration in the given {@code sourceUnit}
* @param sourceUnit the unit of the {@code sourceDuration} argument
* @return the converted duration in this unit,
* or {@code Long.MIN_VALUE} if conversion would negatively
* overflow, or {@code Long.MAX_VALUE} if it would positively overflow.
*/
public long convert(long sourceDuration, TimeUnit sourceUnit) {
throw new AbstractMethodError();
}

这里很明确的指出了,convert方法的第二个参数sourceUnit(@param sourceUnit the unit of the {@code sourceDuration} argument)应该是第一个参数sourceDuration的时间单位。会产生原链接中提到的那样的错误使用,应该就是理解错了这个convert方法参数的含义,以为第二个参数的时间单位是要转换到的时间单位。

不过这个陷阱确实有点绕,在getDelay(TimeUnit unit)方法里面,调用unit.convert(long sourceDuration, TimeUnit sourceUnit)方法,一下出来了两个TimeUnit变量,不仔细一点的话真是容易被坑啊。当然,要是自身的getDelay方法实现不用unit.convert方法或许就避免了该问题了。

java中DelayQueue的一个使用陷阱分析的更多相关文章

  1. Java中Comparable和Comparator接口区别分析

    Java中Comparable和Comparator接口区别分析 来源:码农网 | 时间:2015-03-16 10:25:20 | 阅读数:8902 [导读] 本文要来详细分析一下Java中Comp ...

  2. Java中直接输出一个类的对象

    例如 package com.atguigu.java.fanshe; public class Person { String name; private int age; public Strin ...

  3. java 中 “文件” 和 “流” 的简单分析

    java 中 FIle 和 流的简单分析 File类 简单File 常用方法 创建一个File 对象,检验文件是否存在,若不存在就创建,然后对File的类的这部分操作进行演示,如文件的名称.大小等 / ...

  4. Java中如何查看一个类依赖的包

    Java中如何查看一个类依赖的包 如图, 我如何知道JSONArray是依赖的哪一个包呢,这里有两个json-lib包?   测试语句:   public static void main(Strin ...

  5. Java中的Enum的使用与分析

    使用name()方法和valueOf(String)方法可以在枚举类型对象和字符串之间方便得转换.如果valueOf(String)方法的参数不是该枚举类型合法的字符串,则会抛出IllegalArgu ...

  6. java中只能有一个实例的类的创建

    Java中,如果我们创建一个类,想让这个类只有一个对象,那么我们可以 1:把该类的构造方法设计为private 2:在该类中定义一个static方法,在该方法中创建对象 package test; / ...

  7. 七、如何在Java中高效检查一个数组是否含有一个值

    如何检查一个数组(非排序的)是否包含特定的值.这是个非常有用或经常被在Java中使用.这是个在Stack Overflow中高得票的问题.在已经高得票的答案中,有许多不同的处理方法,但是时间的复杂度非 ...

  8. Java中参数传递时值传递的机制分析

    参数传递是什么?      在C的函数或是JAVA的方法中,向一个函数或方法内部传递一个参数,比如:   void fun( int num ){     num+=2 ; }   int a = 3 ...

  9. Java中Final修饰一个变量时,是引用不能变还是引用的对象不能变

    Java中,使用Final修饰一个变量,是引用不能变,还是引用对象不能变? 是引用对象的地址不能变,引用变量所指的对象的内容可以改变. final变量永远指向这个对象,是一个常量指针,而不是指向常量的 ...

随机推荐

  1. centos6.5下 hdp-2.4.2安装

    (1)准备工作 /usr/sbin/sestatus -v getenforce1./usr/sbin/sestatus -v ##如果SELinux status参数为enabled即为开启状态SE ...

  2. C#开发Open-Webkit-Sharp浏览器并支持前端alert显示

    看了网上的很多教程,但是总是总是只言片语的,可能不同的人遇到的问题不一样,他们就只列举了自己的问题,那么这里我来做一下总结吧,跟大家分享一下我的完整的开发过程 首先你需要准备Visual Studio ...

  3. 题目1010:A + B

    题目描述: 读入两个小于100的正整数A和B,计算A+B. 需要注意的是:A和B的每一位数字由对应的英文单词给出. 输入: 测试输入包含若干测试用例,每个测试用例占一行,格式为"A + B ...

  4. HIVE和HADOOP的一些东西

    今天刚上班就要更新一个hive表(新年好呀我想说...),由于建立的外表直接替换hdfs文件就行了,但是替换完发现少了二行数据,原来之前做了关联,这就要用到hive的insert了! 先来说一下hiv ...

  5. 对thinkphp的命名空间的理解

    tp的命名空间其实就是虚拟目录,目的是为了自动加载类(不是管理文件) tp命名空间包含两部分: (1)初始命名空间:Library (2)根命名空间: a)Library文件下的所有文件夹,只含一级文 ...

  6. vue中$on与$emit的实际应用

    $on常用于监听自定义事件,触发后可传入参数 //监听event1事件 vm.$on('event1',function(msg){ console.log(msg) }) //触发event1自定义 ...

  7. 笔记:Struts2 国际化

    视图页面的国际化 在JSP页面中指定国际化资源需要借助<s:i18n-/>标签,如果把<s:i18n-/>标签作为<s:test-/>标签的父标签,则会直接加载父标 ...

  8. poj 1154 letters (dfs回溯)

    http://poj.org/problem?id=1154 #include<iostream> using namespace std; ]={},s,r,sum=,s1=; ][]; ...

  9. Redis相关命令

    一.命令示例 1. KEYS/RENAME/DEL/EXISTS/MOVE/RENAMENX: #在Shell命令行下启动Redis客户端工具. /> redis-cli #清空当前选择的数据库 ...

  10. 基于PLC-C#串口通讯,温度检测和转速监控的c#/.Net实现。

    我司为五金加工企业,其中有一条喷涂车间和流水线,客户要求能实时监控炉温温度.流水线速,并设置上下限值,达到上下限时报警. 开始考虑过USB的温度采集器,但是却没有找到带USB的光电开关,并且线路长度受 ...