Java学习者论坛

 找回密码
 立即注册

QQ登录

只需一步,快速开始

手机号码,快捷登录

恭喜Java学习者论坛(https://www.javaxxz.com)已经为数万Java学习者服务超过8年了!积累会员资料超过10000G+
成为本站VIP会员,下载本站10000G+会员资源,购买链接:点击进入购买VIP会员
JAVA高级面试进阶视频教程Java架构师系统进阶VIP课程

分布式高可用全栈开发微服务教程

Go语言视频零基础入门到精通

Java架构师3期(课件+源码)

Java开发全终端实战租房项目视频教程

SpringBoot2.X入门到高级使用教程

大数据培训第六期全套视频教程

深度学习(CNN RNN GAN)算法原理

Java亿级流量电商系统视频教程

互联网架构师视频教程

年薪50万Spark2.0从入门到精通

年薪50万!人工智能学习路线教程

年薪50万!大数据从入门到精通学习路线年薪50万!机器学习入门到精通视频教程
仿小米商城类app和小程序视频教程深度学习数据分析基础到实战最新黑马javaEE2.1就业课程从 0到JVM实战高手教程 MySQL入门到精通教程
查看: 452|回复: 0

[默认分类] java中DelayQueue的一个使用陷阱分析

[复制链接]
  • TA的每日心情
    开心
    2021-12-13 21:45
  • 签到天数: 15 天

    [LV.4]偶尔看看III

    发表于 2018-3-27 09:27:54 | 显示全部楼层 |阅读模式
    最近工作中有接触到DelayQueue,网上搜索资料的时候发现一篇文章谈到DelayQueue的坑。点击打开链接
    文中已经总结了遇到坑的地方,还有解决方案。不过我第一眼看一下没弄明白为什么,所以翻了翻源码深究了一下,下面把这个坑的原因以及原理分析一下。
     
    首先是DelayQueue的take()方法:

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


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

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


    &nbsp;
    就只有一个getDelay(TimeUnit)方法,它返回的指定的TimeUnit的时间长度。显然,具体的实现类要实现该方法才行。
    &nbsp;
    那么来看一下具体的getDelay(TimeUnit)方法的实现吧,我看了几篇文章,基本上大同小异,都是如下这般实现的:

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


    &nbsp;
    原博主很贴心的提醒了,这个地方convert方法的第二个参数,应该要使用TimeUnit.MILLISECONDS而不是TimeUnit.NANOSECONDS(虽然不管使用什么时间单位都不会导致程序出现错误的结果,但是用错了时间单位的话,CPU可就遭殃了)。那么为什么会一定要强调要使用MILLISECONDS这个单位呢?
    继续看看convert方法的源码吧,在TimeUnit枚举类中,定义了若干时间单位,他们有各自的convert方法的实现,先来看看TimeUnit.NANOSECONDS的:

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


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

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


    &nbsp;
    回到我们的实际使用场景,take方法中long delay = first.getDelay(NANOSECONDS); &nbsp;-> &nbsp;NANOSECONDS.convert(long d, TimeUnit u) &nbsp;-> &nbsp;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空转压力是巨大的。
    &nbsp;
    分析了这么多,其实看看jdk中TimeUnit类对convert方法的注释,很容易就理解了:
    1.     /**
    2.      * Converts the given time duration in the given unit to this unit.
    3.      * Conversions from finer to coarser granularities truncate, so
    4.      * lose precision. For example, converting {@code 999} milliseconds
    5.      * to seconds results in {@code 0}. Conversions from coarser to
    6.      * finer granularities with arguments that would numerically
    7.      * overflow saturate to {@code Long.MIN_VALUE} if negative or
    8.      * {@code Long.MAX_VALUE} if positive.
    9.      *
    10.      * <p>For example, to convert 10 minutes to milliseconds, use:
    11.      * {@code TimeUnit.MILLISECONDS.convert(10L, TimeUnit.MINUTES)}
    12.      *
    13.      * @param sourceDuration the time duration in the given {@code sourceUnit}
    14.      * @param sourceUnit the unit of the {@code sourceDuration} argument
    15.      * @return the converted duration in this unit,
    16.      * or {@code Long.MIN_VALUE} if conversion would negatively
    17.      * overflow, or {@code Long.MAX_VALUE} if it would positively overflow.
    18.      */
    19.     public long convert(long sourceDuration, TimeUnit sourceUnit) {
    20.         throw new AbstractMethodError();
    21.     }
    复制代码

    这里很明确的指出了,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方法或许就避免了该问题了。
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    QQ|手机版|Java学习者论坛 ( 声明:本站资料整理自互联网,用于Java学习者交流学习使用,对资料版权不负任何法律责任,若有侵权请及时联系客服屏蔽删除 )

    GMT+8, 2024-5-21 08:30 , Processed in 0.371212 second(s), 37 queries .

    Powered by Discuz! X3.4

    © 2001-2017 Comsenz Inc.

    快速回复 返回顶部 返回列表