TA的每日心情 | 开心 2021-12-13 21:45 |
---|
签到天数: 15 天 [LV.4]偶尔看看III
|
最近工作中有接触到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方法或许就避免了该问题了。 |
|