java中DelayQueue的一个使用陷阱分析

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

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

首先是DelayQueue的take()方法:

 1     public E take() throws InterruptedException {
 2         final ReentrantLock lock = this.lock;
 3         lock.lockInterruptibly();
 4         try {
 5             for (;;) {
 6                 E first = q.peek();
 7                 if (first == null)
 8                     available.await();
 9                 else {
10                     long delay = first.getDelay(NANOSECONDS);    // 1
11                     if (delay <= 0)
12                         return q.poll();
13                     first = null; // don‘t retain ref while waiting
14                     if (leader != null)
15                         available.await();
16                     else {
17                         Thread thisThread = Thread.currentThread();
18                         leader = thisThread;
19                         try {
20                             available.awaitNanos(delay);    // 2
21                         } finally {
22                             if (leader == thisThread)
23                                 leader = null;
24                         }
25                     }
26                 }
27             }
28         } finally {
29             if (leader == null && q.peek() != null)
30                 available.signal();
31             lock.unlock();
32         }
33     }

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

这里看看接口Delayed的源码:

 1 public interface Delayed extends Comparable<Delayed> {
 2
 3     /**
 4      * Returns the remaining delay associated with this object, in the
 5      * given time unit.
 6      *
 7      * @param unit the time unit
 8      * @return the remaining delay; zero or negative values indicate
 9      * that the delay has already elapsed
10      */
11     long getDelay(TimeUnit unit);
12 }

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

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

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

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

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

 1     NANOSECONDS {
 2         public long toNanos(long d)   { return d; }
 3         public long toMicros(long d)  { return d/(C1/C0); }
 4         public long toMillis(long d)  { return d/(C2/C0); }
 5         public long toSeconds(long d) { return d/(C3/C0); }
 6         public long toMinutes(long d) { return d/(C4/C0); }
 7         public long toHours(long d)   { return d/(C5/C0); }
 8         public long toDays(long d)    { return d/(C6/C0); }
 9         public long convert(long d, TimeUnit u) { return u.toNanos(d); }
10         int excessNanos(long d, long m) { return (int)(d - (m*C2)); }
11     },

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

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

 1     MILLISECONDS {
 2         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;
 3         public long toMicros(long d)  { return x(d, C2/C1, MAX/(C2/C1)); }
 4         public long toMillis(long d)  { return d; }
 5         public long toSeconds(long d) { return d/(C3/C2); }
 6         public long toMinutes(long d) { return d/(C4/C2); }
 7         public long toHours(long d)   { return d/(C5/C2); }
 8         public long toDays(long d)    { return d/(C6/C2); }
 9         public long convert(long d, TimeUnit u) { return u.toMillis(d); }
10         int excessNanos(long d, long m) { return 0; }
11     },

回到我们的实际使用场景,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方法或许就避免了该问题了。

原文地址:https://www.cnblogs.com/noodleprince/p/8647582.html

时间: 2024-11-08 14:44:00

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

Java中的Enum的使用与分析(转)

示例: public enum EnumTest { FRANK("The given name of me"), LIU("The family name of me");//两个实例 private String context; private String getContext(){ return this.context; } private EnumTest(String context){ this.context = context; } publi

关于java中ReentrantLock类的源码分析以及总结与例子

一,官方描述 关于ReentrantLock的官方描述,英文的就不贴出来了,这里我只贴出我自己翻译的描述: reentrant是一个跟synchronized具有相同行为和语义的持有锁来访问方法和语句的互斥锁,但是reentrant还拥有被扩展的能力. ReentrantLock会被线程拥有并且持续锁定,不会解锁.线程调用lock()方法返回后,则成功持有锁,否则这个锁正在被另一个线程所持有,只能等待另一个线程释放锁,如果当前线程拥有了锁,则调用lock()方法会立即返回,这个状态可以通过isH

Java中Comparable和Comparator接口区别分析

Java中Comparable和Comparator接口区别分析 来源:码农网 | 时间:2015-03-16 10:25:20 | 阅读数:8902 [导读] 本文要来详细分析一下Java中Comparable和Comparator接口的区别,两者都有比较的功能,那么究竟有什么区别呢,感兴趣的Java开发者继续看下去吧.Comparable 简介Comparable 是排序接口.若一个类实现了Comparab 本文要来详细分析一下Java中Comparable和Comparator接口的区别,

Java中arraylist和linkedlist源码分析与性能比较

Java中arraylist和linkedlist源码分析与性能比较 1,简介 在java开发中比较常用的数据结构是arraylist和linkedlist,本文主要从源码角度分析arraylist和linkedlist的性能. 2,arraylist源码分析 Arraylist底层的数据结构是一个对象数组,有一个size的成员变量标记数组中元素的个数,如下图: * The array buffer into which the elements of the ArrayList are sto

关于java中文件删除失败的原因分析

最近在做一个文档管理系统,结果在删除文件的时候,一直提示我文件删除失败,当然啦,是我在jsp里面写的一个alert("文件删除失败!"),然后我就纳闷儿了,为什么删不掉呢?后来打开windows,找到相应的文件,用管理员权限去删除也删不掉!然后就给我报错,java TM...正在使用这个文件,我顿时就凌乱了,因为我使用的是MyEcplise,所以我又回去检查代码,后来终于找到元凶了,是一个警告导致的错误!警告啊!下面我贴上代码: /** * 获取单个文件的大小 * @param fil

java中instanceof和getClass()的区别分析

本篇文章介绍了,在java中instanceof和getClass()的区别分析.需要的朋友参考下 class A { } class B extends A { } Object o1 = new A();  Object o2 = new B(); o1 instanceof A => true  o1 instanceof B => false  o2 instanceof A => true // <================ HERE  o2 instanceof

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

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

Java中如何判断一个double类型的数据为0?

Java中如何判断一个double类型的数据为0 其实这个问题很简单,只是很多时候考虑复杂了,直接用==判断即可.下面给出测试例子: /**  * 如何判断一个double类型的数据为0  *  * @author leizhimin 2014/8/27 10:31  */ public class Test4 {     public static void main(String[] args) {         double x = 0.00000000000000000;       

在java中如何创建一个内存泄露

今天访问java 并发编程网,看到一个翻译征集令,并发编程网的作者从stackoverflow 网站上选取了一些经典问答,遂决定翻译几篇 征集令地址:http://ifeve.com/stackoverflow-assembly/ 翻译系列文章: 1.Java 核心类库中的一些设计模式 2. hashMap 与hashTable之间的区别 3.  在java中如何创建一个内存泄露 译文: 在java中如何创建一个内存泄露 问题: 我之前参加了一个面试, 被问到在java中如何创建一个内存泄露.不