这是一个完整的 TimeUnit 枚举类源码。它是 Java 并发编程中的基础设施,用类型安全的方式来表示时间单位,并提供跨单位转换、超时控制、线程等待/睡眠等工具方法。

下面我们逐层分解这段代码,解释它的设计思路和每个部分的作用。


一、类的作用与设计理念(类注释)

/**
 * A {@code TimeUnit} represents time durations at a given unit of
 * granularity and provides utility methods to convert across units,
 * and to perform timing and delay operations in these units.
 ...
 */
public enum TimeUnit { ... }

TimeUnit 是一个枚举,它的核心作用有三个:

  1. 表示时间粒度:纳秒、微秒、毫秒、秒、分、时、天。
  2. 在不同单位间转换数值(如 10 分钟是多少毫秒)。
  3. 提供基于这些单位的定时等待操作waitsleepjoin)。

注释中强调了几个关键点:

  • TimeUnit 不保存时间值本身,它只是“单位标尺”,帮助我们解释一个数值的含义。
  • 例如 lock.tryLock(50L, TimeUnit.MILLISECONDS) 表示 50 毫秒,而同样的 50L, TimeUnit.SECONDS 就表示 50 秒。
  • 实际等待精度不一定能到达纳秒级,具体由底层实现决定。

二、枚举常量与基准比例常量

NANOSECONDS(TimeUnit.NANO_SCALE),
MICROSECONDS(TimeUnit.MICRO_SCALE),
MILLISECONDS(TimeUnit.MILLI_SCALE),
SECONDS(TimeUnit.SECOND_SCALE),
MINUTES(TimeUnit.MINUTE_SCALE),
HOURS(TimeUnit.HOUR_SCALE),
DAYS(TimeUnit.DAY_SCALE);

枚举实例分别代表 7 种时间单位。每个实例在构造时传入一个比例因子 scale,表示该单位包含多少纳秒。

比例常量定义如下:

private static final long NANO_SCALE   = 1L;
private static final long MICRO_SCALE  = 1000L * NANO_SCALE;      // 1_000
private static final long MILLI_SCALE  = 1000L * MICRO_SCALE;     // 1_000_000
private static final long SECOND_SCALE = 1000L * MILLI_SCALE;     // 1_000_000_000
private static final long MINUTE_SCALE = 60L * SECOND_SCALE;
private static final long HOUR_SCALE   = 60L * MINUTE_SCALE;
private static final long DAY_SCALE    = 24L * HOUR_SCALE;

这样,所有比例都以纳秒为最小基准,转换时可以统一用纳秒计算。


三、预计算的转换缓存字段

private final long scale;
private final long maxNanos;
private final long maxMicros;
private final long maxMillis;
private final long maxSecs;
private final long microRatio;
private final int milliRatio;
private final int secRatio;

字段含义

  • scale – 该单位对应的纳秒数(构造参数 s)。
  • maxNanos – 该单位可安全转为纳秒的最大值:Long.MAX_VALUE / scale
    如果传入值 d > maxNanos,则 d * scale 会溢出,此时应饱和到 Long.MAX_VALUE
  • microRatio – 该单位与微秒之间的转换比率(若 s >= MICRO_SCALE,则是 s / MICRO_SCALE;否则是 MICRO_SCALE / s,用于向下除法)。
  • maxMicros – 该单位转换到微秒的安全上界。
  • milliRatio – 与毫秒的转换比率(同样是 s / MILLI_SCALEMILLI_SCALE / s),存为 int,因为比率不会超过 int 范围。
  • maxMillis – 转换到毫秒的安全上界。
  • secRatio – 与秒的转换比率(存为 int)。
  • maxSecs – 转换到秒的安全上界。

为什么只缓存到秒?

源码注释:

/*
 * Instances cache conversion ratios and saturation cutoffs for
 * the units up through SECONDS. Other cases compute them, in
 * method cvt.
 */

秒及以下单位是最常用的(sleepwaitjoin 都用到毫秒/纳秒),缓存这些比率可以加速 toNanostoMicrostoMillistoSeconds 等方法。对于分、时、天,转换频率低,直接调用 cvt 通用方法即可。


四、构造方法 TimeUnit(long s)

private TimeUnit(long s) {
    this.scale = s;
    this.maxNanos = Long.MAX_VALUE / s;
    long ur = (s >= MICRO_SCALE) ? (s / MICRO_SCALE) : (MICRO_SCALE / s);
    this.microRatio = ur;
    this.maxMicros = Long.MAX_VALUE / ur;
    long mr = (s >= MILLI_SCALE) ? (s / MILLI_SCALE) : (MILLI_SCALE / s);
    this.milliRatio = (int)mr;
    this.maxMillis = Long.MAX_VALUE / mr;
    long sr = (s >= SECOND_SCALE) ? (s / SECOND_SCALE) : (SECOND_SCALE / s);
    this.secRatio = (int)sr;
    this.maxSecs = Long.MAX_VALUE / sr;
}
  • 传入 s 就是该单位的 scale(纳秒数)。
  • 计算各个 ratio 时,要根据当前单位是大于还是小于目标单位来决定用法:
    • 如果当前单位 ≥ 目标单位,ratio = scale / 目标scale(1 个当前单位 = 多少个目标单位)。
    • 如果当前单位 < 目标单位,ratio = 目标scale / scale(需用除法才能转为更大单位)。
  • 同时根据 Long.MAX_VALUE / ratio 算出安全上界,避免乘法溢出。

这些值在枚举实例初始化时就固定好了,后续方法可以零开销地直接使用。


五、通用转换方法 cvt(long d, long dst, long src)

private static long cvt(long d, long dst, long src) {
    long r, m;
    if (src == dst)
        return d;
    else if (src < dst)
        return d / (dst / src);
    else if (d > (m = Long.MAX_VALUE / (r = src / dst)))
        return Long.MAX_VALUE;
    else if (d < -m)
        return Long.MIN_VALUE;
    else
        return d * r;
}

这是最基础的转换逻辑:

  • dst – 目标单位的 scale(纳秒值)
  • src – 源单位的 scale
  • 如果 src == dst,直接返回。
  • 如果 src < dst(源单位比目标单位更精细),则需要除法
    d / (dst / src) (先将目标转成源单位的倍数,再除)。这样 d 除以一个整数,向下取整。
  • 如果 src > dst(源单位比目标单位更大),则需要乘法:先算 r = src / dst,然后检查 d * r 是否会溢出。
    Long.MAX_VALUE / r 算出安全边界 m,如果 d > m 正向溢出返回 Long.MAX_VALUE;如果 d < -m 负向溢出返回 Long.MIN_VALUE;否则安全返回 d * r

这种方法保证了任何转换都不会抛出异常,而是采用饱和策略


六、convert 方法(长整型版本)

public long convert(long sourceDuration, TimeUnit sourceUnit) {
    switch (this) {
    case NANOSECONDS:  return sourceUnit.toNanos(sourceDuration);
    case MICROSECONDS: return sourceUnit.toMicros(sourceDuration);
    case MILLISECONDS: return sourceUnit.toMillis(sourceDuration);
    case SECONDS:      return sourceUnit.toSeconds(sourceDuration);
    default: return cvt(sourceDuration, scale, sourceUnit.scale);
    }
}
  • 根据目标单位(即 this,当前枚举实例)进行 switch。
  • 对于纳秒、微秒、毫秒、秒这四种常用单位,直接委托给对应的 toXxx 方法,它们内部有预计算的缓存,效率更高。
  • 对于分、时、天等较大单位,退而使用 cvt 通用方法。

七、convert(Duration duration) (Java 11 新增)

public long convert(Duration duration) { ... }

这是为了支持 java.time.Duration 的转换。

  • Duration 内部用秒 + 纳秒调整来表示时间量。
  • 处理了 secs < 0 && nano > 0 的特殊情况:
    比如 Duration.ofSeconds(-1, 500_000_000) 表示 -0.5 秒。
    duration.getSeconds() 返回 -1,nano 返回 500_000_000。
    为了与整数除法语义对齐,代码调整 secs++nano -= SECOND_SCALE,变为 secs = 0nano = -500_000_000
  • 然后根据不同目标单位进行优化:
    • 如果是 NANOSECONDS,直接计算 secs * secRatio + nano,其中 secRatio = 1_000_000_000
    • 如果目标单位 < 秒,先将 nano 除以该单位的 scale
    • 如果目标单位是秒,直接返回 secs
    • 更大单位:secs / secRatio
  • 最后做溢出检查:如果 secs 在安全范围内直接返回,否则根据符号返回 Long.MAX_VALUELong.MIN_VALUE

这个方法同样不抛异常,采用饱和策略,与 Duration.toNanos() 不同。


八、toNanostoMicrostoMillistoSeconds

这些方法是转换为具体单位的高频入口,它们的共同模式是:

  • 检查 scale 与目标常量的关系,决定直接返回、除法还是乘法。
  • 乘法时使用构造时缓存好的 maxXxx 边界来避免溢出,溢出则饱和到 MAX_VALUEMIN_VALUE

toMillis 为例:

public long toMillis(long duration) {
    long s, m;
    if ((s = scale) <= MILLI_SCALE)
        return (s == MILLI_SCALE) ? duration : duration / milliRatio;
    else if (duration > (m = maxMillis))
        return Long.MAX_VALUE;
    else if (duration < -m)
        return Long.MIN_VALUE;
    else
        return duration * milliRatio;
}
  • 若当前单位 ≤ 毫秒(即更精细),则要么相等直接返回,要么用除法(milliRatio = MILLI_SCALE / scale,向上取整的分母)。
  • 若当前单位 > 毫秒,则需要乘法,并用 maxMillis 做溢出检查。

toMinutestoHourstoDays 则直接调用 cvt,没有单独缓存。


九、excessNanos:计算不满 1 毫秒的纳秒部分

private int excessNanos(long d, long m) { ... }
  • d 是总的 timeout(当前单位数值),m 是已经转成毫秒的部分(通过 toMillis 得到)。
  • 此方法计算需要额外传递给 wait(millis, nanos) 的纳秒部分。
  • 如果当前单位是纳秒:d - (m * MILLI_SCALE) 算出剩余纳秒。
  • 如果当前单位是微秒:d * 1000 - m * MILLI_SCALE
  • 其他单位(毫秒及以上),纳秒部分为 0。

十、定时等待方法:timedWaittimedJoinsleep

这三个方法将 TimeUnit 的数值转换成 Object.wait(long ms, int ns)Thread.join(long ms, int ns)Thread.sleep(long ms, int ns) 所需的参数。

sleep 为例:

public void sleep(long timeout) throws InterruptedException {
    if (timeout > 0) {
        long ms = toMillis(timeout);
        int ns = excessNanos(timeout, ms);
        Thread.sleep(ms, ns);
    }
}
  • 如果 timeout <= 0,什么都不做(与 Thread.sleep(0) 语义不同,但这是并发工具类的常见处理,避免负值异常)。
  • 转换为毫秒和剩余纳秒,调用底层方法。这样用户可以写 TimeUnit.SECONDS.sleep(1) 来睡眠 1 秒,代码更具可读性。

十一、与 java.time.temporal.ChronoUnit 互相转换(Java 9+)

public ChronoUnit toChronoUnit() { ... }
public static TimeUnit of(ChronoUnit chronoUnit) { ... }

这两个方法实现了 TimeUnitChronoUnit 的双向映射,让新旧 API 能互操作。例如:

  • TimeUnit.SECONDS.toChronoUnit()ChronoUnit.SECONDS
  • TimeUnit.of(ChronoUnit.HOURS)TimeUnit.HOURS

对于 ChronoUnit 中没有直接对应的单位(如 WEEKSMONTHS),of 会抛出 IllegalArgumentException


总结

整个 TimeUnit 的设计体现了高性能、类型安全、防御式编程的理念:

  1. 枚举保证类型安全:不会传递错误的整数当作时间单位。
  2. 预计算与缓存:构造时就把常用转换的比率和溢出边界算好,运行时几乎无额外开销。
  3. 饱和策略:所有转换不会抛 ArithmeticException,而是返回 Long.MAX/MIN_VALUE,避免程序因溢出中断(但也需要调用者自己注意)。
  4. 便捷的定时方法:让 sleepwaitjoin 代码更可读,且内部正确分解毫秒和纳秒参数。
  5. java.time 兼容:通过 Duration 转换和 ChronoUnit 互转,桥接新旧 API。

这就是 TimeUnit 的全部核心含义与实现细节。
结合给出的源码和注释,我逐一回答你的四个问题。


1. TimeUnit 和 Lock 的关系是什么?

TimeUnitLock 之间是 “参数解释器”与“使用者” 的关系。

  • Lock 接口中定义了带超时的方法,例如 tryLock(long time, TimeUnit unit)
  • 这里的 time 只是一个数值,本身没有单位含义TimeUnit 的作用就是告诉 Lock:“这个 time 数值应该按照什么时间粒度来解读”。

从注释中的例子可以清楚看到这一点:

// 50 毫秒后超时
if (lock.tryLock(50L, TimeUnit.MILLISECONDS)) ...

// 50 秒后超时
if (lock.tryLock(50L, TimeUnit.SECONDS)) ...

同一个数值 50L,因为配合的 TimeUnit 不同,一个解释为 50 毫秒,一个解释为 50 秒。
Lock 的实现内部通常会使用 TimeUnit 提供的方法(比如 toMillistoNanos)将任意单位的时间统一转换为自己需要的精度,进而进行底层的定时等待或自旋。

另外,TimeUnit 还提供 timedWaittimedJoinsleep 等便利方法,它们都可以直接用于基于 synchronized 的等待或线程控制,这和 Lock 中的超时机制属于同一类并发原语的辅助。比如你可以用 unit.timedWait(lock, timeout) 来实现一个阻塞队列的 poll 方法,这在注释中也有示例:

public E poll(long timeout, TimeUnit unit) throws InterruptedException {
    synchronized (lock) {
        while (isEmpty()) {
            unit.timedWait(lock, timeout);
            ...
        }
    }
}

总结: TimeUnit 不拥有锁,也不实现锁机制,它是作为参数传给锁的方法,用于定义超时时间的单位,从而让锁的接口在时间表达上既灵活又类型安全。


2. TimeUnit 怎么处理并发问题?

需要从两个层面理解:

(1)TimeUnit 自身的线程安全性

TimeUnit 是一个枚举,所有实例(如 NANOSECONDSSECONDS 等)都是在类加载时创建的常量。
它的所有字段(scalemaxNanossecRatio 等)都是 private final 的,在构造完成后就不可变
所有方法(converttoNanossleep 等)都是无状态的纯计算,只依赖方法参数和自身不可变字段,不修改任何共享状态。

因此,TimeUnit天然线程安全的,多个线程可以任意并发调用同一个 TimeUnit 实例的方法,而不需要任何外部同步。

(2)TimeUnit 在并发场景中的作用

TimeUnit 并不自己去“处理并发问题”(如加锁、协调线程),而是为并发工具提供时间解释服务。它和并发工具的关系是:

  • LockConditionFutureSemaphore 等提供超时参数的单位,使代码可读性更强且不易出错。
  • 内置 timedWaittimedJoinsleep 方法,这些方法内部将当前 TimeUnit 的时间值转换为毫秒和纳秒,再调用 Object.wait(ms, ns)Thread.join(ms, ns)Thread.sleep(ms, ns) 来实现等待。它们本质上是在调用底层操作系统的定时等待机制,并发控制仍然由 JVM 和操作系统负责,TimeUnit 只是做了数值转换。

例如 timedWait 的实现:

public void timedWait(Object obj, long timeout) throws InterruptedException {
    if (timeout > 0) {
        long ms = toMillis(timeout);
        int ns = excessNanos(timeout, ms);
        obj.wait(ms, ns);
    }
}

它只是算出毫秒和剩余纳秒,然后调用 obj.wait()。这种设计让并发代码不用再手动拆分时间值,减少了出错几率。

结论: TimeUnit 自身是不可变枚举,线程安全;它通过提供单位转换和便利方法,间接支持了并发编程中对超时、等待、睡眠等操作的精确控制,但实际的并发互斥由 Locksynchronized 等机制处理。


3. TimeUnit 内部最大的单位和最小的单位是什么?

根据枚举常量定义,结合比例常量:

NANOSECONDS(TimeUnit.NANO_SCALE),     // NANO_SCALE = 1
MICROSECONDS(TimeUnit.MICRO_SCALE),   // MICRO_SCALE = 1000
MILLISECONDS(TimeUnit.MILLI_SCALE),   // MILLI_SCALE = 1000 * 1000
SECONDS(TimeUnit.SECOND_SCALE),       // SECOND_SCALE = 1000 * 1000 * 1000
MINUTES(TimeUnit.MINUTE_SCALE),       // 60 * SECOND_SCALE
HOURS(TimeUnit.HOUR_SCALE),           // 60 * MINUTE_SCALE
DAYS(TimeUnit.DAY_SCALE)              // 24 * HOUR_SCALE
  • 最小单位NANOSECONDS(纳秒),其基准比例 NANO_SCALE = 1
  • 最大单位DAYS(天),其比例 DAY_SCALE = 24 * HOUR_SCALE,即 86,400 秒的纳秒数。

注释中也明确说明:

A nanosecond is defined as one thousandth of a microsecond, a microsecond as one thousandth of a millisecond, a millisecond as one thousandth of a second, a minute as sixty seconds, an hour as sixty minutes, and a day as twenty four hours.

所以时间粒度从纳秒一直到天,覆盖了绝大多数业务和系统编程的需要。


4. 如何处理单位之间的转化?

单位转化是 TimeUnit 的核心能力,主要通过以下方式实现,并始终伴随溢出保护

(1)核心通用方法 cvt
private static long cvt(long d, long dst, long src) {
    long r, m;
    if (src == dst)
        return d;
    else if (src < dst)
        return d / (dst / src);
    else if (d > (m = Long.MAX_VALUE / (r = src / dst)))
        return Long.MAX_VALUE;
    else if (d < -m)
        return Long.MIN_VALUE;
    else
        return d * r;
}

逻辑如下:

  • 同单位:直接返回原值。
  • 源单位小于目标单位(例如纳秒 → 秒,越精细 → 越粗):
    整数除法 截断,d / (dst / src)
    注释也注明:转换从细粒度到粗粒度会 truncate(截断),丢失精度。例如 999 毫秒转秒 → 0
  • 源单位大于目标单位(例如秒 → 纳秒,越粗 → 越细):
    需要乘法。乘法前检查溢出:
    r = src / dst(一个 src 单位等于多少个 dst 单位),
    m = Long.MAX_VALUE / r(安全上限)。
    d > m 则饱和为 Long.MAX_VALUE;若 d < -m 则饱和为 Long.MIN_VALUE;否则安全相乘。

这样,任何转换都不会抛出 ArithmeticException,而是采用饱和策略,和 Duration.toNanos() 的行为不同(Duration.toNanos() 会抛异常)。

(2)高频单位的快速通道:toNanostoMicrostoMillistoSeconds

构造时为秒及以下单位预先计算了 转换比率microRatio, milliRatio, secRatio)和 安全边界maxNanos, maxMicros, maxMillis, maxSecs)。这些 toXxx() 方法直接利用缓存值,避免重复计算,性能极高。

toMillis 为例:

public long toMillis(long duration) {
    long s, m;
    if ((s = scale) <= MILLI_SCALE)
        return (s == MILLI_SCALE) ? duration : duration / milliRatio;
    else if (duration > (m = maxMillis))
        return Long.MAX_VALUE;
    else if (duration < -m)
        return Long.MIN_VALUE;
    else
        return duration * milliRatio;
}
  • 若当前单位 ≤ 毫秒,则返回原值(相等时)或除以 milliRatio(更细单位向毫秒转换的除数)。
  • 若当前单位 > 毫秒,则乘以 milliRatio,并用预存的 maxMillis 做溢出检测。
(3)convert 方法(分派)
public long convert(long sourceDuration, TimeUnit sourceUnit) {
    switch (this) {
    case NANOSECONDS:  return sourceUnit.toNanos(sourceDuration);
    case MICROSECONDS: return sourceUnit.toMicros(sourceDuration);
    case MILLISECONDS: return sourceUnit.toMillis(sourceDuration);
    case SECONDS:      return sourceUnit.toSeconds(sourceDuration);
    default: return cvt(sourceDuration, scale, sourceUnit.scale);
    }
}

convert 将源单位的值转成当前单位(this)。它把最常用的四种目标单位(纳秒、微秒、毫秒、秒)直接分派到对应的快速方法;对于分、时、天则退回到 cvt

(4)与 Duration 的转换

Java 11 后加入 convert(Duration duration),它提取 Duration 的秒和纳秒部分,经过调整(处理负数的正纳秒表示)后,再根据不同目标单位用不同路径计算,同样做溢出饱和保护。

(5)定时等待中的特殊转化:excessNanos

sleepwaitjoin 中,需要把时间拆成“毫秒部分”和“不足 1 毫秒的纳秒部分”:

private int excessNanos(long d, long m) {
    long s;
    if ((s = scale) == NANO_SCALE)
        return (int)(d - (m * MILLI_SCALE));
    else if (s == MICRO_SCALE)
        return (int)((d * 1000L) - (m * MILLI_SCALE));
    else
        return 0;
}

这里 d 是原数值,m 是已经转化出的毫秒数,该方法算出剩余纳秒,精确传递给底层 wait(ms, ns) 等方法。


整体来说,TimeUnit 的单位转换设计兼顾了:

  • 正确性:整数除法截断、乘法溢出饱和,避免静默错误。
  • 性能:常用单位缓存比率和边界,高频调用零开销。
  • 可读性converttoXxx 等方法让调用代码清晰表达意图。
  • 安全性:所有转换都不抛异常,采用最安全的长整型饱和策略。

作用

这个 TimeSource 接口很好地展示了 TimeUnit解耦时间来源与时间单位上的设计价值。下面我们先从 Minecraft 的设计意图切入,再看企业开发里的经典场景,让你对 TimeUnit 的作用有立体的认识。


一、从 Minecraft 的 TimeSource 理解 TimeUnit 的作用

源码回顾

@FunctionalInterface
public interface TimeSource {
    long get(TimeUnit unit);

    interface NanoTimeSource extends TimeSource, LongSupplier {
        default long get(TimeUnit unit) {
            return unit.convert(this.getAsLong(), TimeUnit.NANOSECONDS);
        }
    }
}

设计意图

  • TimeSource 是一个时间源抽象,不规定具体来源(系统时间、游戏刻、自定义时钟等)。它只有一个方法 long get(TimeUnit unit),意为“以你指定的单位返回当前时间”。
  • NanoTimeSource 是一种基于纳秒的实现。它同时继承了 LongSupplier(提供 long getAsLong()),在 get(TimeUnit) 中调用 unit.convert(this.getAsLong(), TimeUnit.NANOSECONDS),将底层纳秒值转换成任意单位。

为什么需要 TimeUnit 作为参数?

如果 TimeSource 只是固定返回纳秒或毫秒,那么:

  1. 调用方必须自己转换单位,代码里会散落大量除法/乘法,容易出错。
  2. 不同时间源的精度不同:游戏刻(tick)可能用“刻数”表示时间,Minecraft 的 20 tick = 1 秒。若有一个基于 tick 的 TimeSource,它能通过 TimeUnit 直接把内部 tick 数转成秒、毫秒等,调用方无需关心内部单位。

TimeUnit 在这里提供了一个统一的转换协议:底层实现只需用自己最自然的单位记录时间,然后在 get(unit) 中使用 unit.convert(...)sourceUnit.toXxx(...) 输出给调用方。调用方可以自由选择自己关心的精度(性能分析用纳秒,业务超时用秒),而不破坏底层封装。

Minecraft 中的实际用法举例

  • 性能分析MinecraftServer 的 tick 循环里可能有 TimeSource.NanoTimeSource,用于测量单 tick 耗时。
    例如:long elapsed = timeSource.get(TimeUnit.MILLISECONDS) 直接得到毫秒数。
  • 异步任务调度:计划任务可能接受 TimeUnit 参数,让配置更可读:schedule(task, 5, TimeUnit.SECONDS)
  • 多平台移植:不同平台系统时间精度不同,通过 TimeSource 抽象,可替换实现而不影响上层逻辑。

二、企业实际开发中 TimeUnit 的核心作用与示例

TimeUnit 在 Java 企业开发中几乎无处不在,主要有以下几个作用:

  1. 让时间间隔的语义清晰,消除魔法数字
  2. 统一单位转换,避免手工计算和溢出风险
  3. 配合并发工具类,简化超时、延迟、等待的代码
  4. 实现性能计时、限流、缓存过期等逻辑
  5. 与监控、日志、接口设计结合,提升可读性和可维护性

1. 消除魔法数字,提高代码可读性

没有 TimeUnit 时,你可能会写:

// 超时 3 秒——这是毫秒还是秒?容易混淆
future.get(3000, TimeUnit.MILLISECONDS);  // 不好

更好的写法直接使用 TimeUnit 常量作为参数:

// 清晰表达“3 秒超时”
future.get(3, TimeUnit.SECONDS);

这在 API 设计中尤其重要,例如自定义服务的超时配置:

public class ServiceConfig {
    private long timeout;
    private TimeUnit timeoutUnit = TimeUnit.MILLISECONDS;

    public void setTimeout(long timeout, TimeUnit unit) {
        this.timeout = timeout;
        this.timeoutUnit = unit;
    }

    public long getTimeoutMillis() {
        return timeoutUnit.toMillis(timeout);
    }
}

调用方可以自由设置:config.setTimeout(5, TimeUnit.SECONDS),而内部统一转为毫秒,既灵活又安全。


2. 配合并发工具,简化超时/延迟控制

JUC 下的 LockConditionFutureCompletableFutureSemaphore 等很多方法的超时参数都直接使用 TimeUnit

// 可重入锁尝试获取锁,最多等 500 毫秒
Lock lock = new ReentrantLock();
if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
    try {
        // 处理逻辑
    } finally {
        lock.unlock();
    }
} else {
    // 获取失败降级
}

线程池的 awaitTermination 同样依赖 TimeUnit

ExecutorService executor = Executors.newFixedThreadPool(4);
// ... 提交任务
executor.shutdown();
if (!executor.awaitTermination(10, TimeUnit.SECONDS)) {
    executor.shutdownNow(); // 强制关闭
}

sleepwaitjoin 也可以通过 TimeUnit 得到更优雅的表达:

TimeUnit.SECONDS.sleep(1);   // 代替 Thread.sleep(1000)
TimeUnit.MINUTES.timedJoin(workerThread, 2); // 最多等 2 分钟

3. 性能监控与代码计时

System.nanoTime() 结合 TimeUnit 可以方便地实现高精度计时,并直接输出你想要的时间单位:

long start = System.nanoTime();
// 执行业务逻辑
doSomething();
long elapsedNanos = System.nanoTime() - start;

// 直接输出为毫秒
long elapsedMillis = TimeUnit.NANOSECONDS.toMillis(elapsedNanos);
logger.info("执行耗时:{} ms", elapsedMillis);

// 或者用 Duration(内部也依赖 TimeUnit/ChronoUnit)
Duration duration = Duration.ofNanos(elapsedNanos);
System.out.println(duration.toMillis() + " ms");

更优雅的封装(类似 TimeSource):

public class Stopwatch {
    private long startNanos;

    public Stopwatch start() {
        this.startNanos = System.nanoTime();
        return this;
    }

    public long elapsed(TimeUnit unit) {
        return unit.convert(System.nanoTime() - startNanos, TimeUnit.NANOSECONDS);
    }
}

// 使用
Stopwatch sw = new Stopwatch().start();
// ... 操作
System.out.println("耗时:" + sw.elapsed(TimeUnit.MICROSECONDS) + " µs");

4. 限流与速率控制

Guava 的 RateLimiter 内部就重度依赖 TimeUnit,但我们可以用 TimeUnit 自己实现简单的令牌桶或固定窗口限流:

public class SimpleRateLimiter {
    private final long intervalNanos;
    private long nextAllowedNanos = System.nanoTime();

    public SimpleRateLimiter(long permitsPerSecond) {
        this.intervalNanos = TimeUnit.SECONDS.toNanos(1) / permitsPerSecond;
    }

    public synchronized boolean tryAcquire() {
        long now = System.nanoTime();
        if (now >= nextAllowedNanos) {
            nextAllowedNanos = now + intervalNanos;
            return true;
        }
        return false;
    }

    // 带超时的等待获取
    public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException {
        long deadline = System.nanoTime() + unit.toNanos(timeout);
        while (System.nanoTime() < deadline) {
            if (tryAcquire()) return true;
            TimeUnit.MICROSECONDS.sleep(100); // 短暂休眠避免忙等
        }
        return false;
    }
}

调用:

SimpleRateLimiter limiter = new SimpleRateLimiter(10); // 每秒 10 个
if (limiter.tryAcquire(500, TimeUnit.MILLISECONDS)) {
    // 允许请求
}

5. 缓存过期与定时任务

缓存库(如 Caffeine、Guava Cache)都使用 TimeUnit 指定过期时间。我们可以写一个简单的缓存过期实现:

public class TimedCache<K, V> {
    private final ConcurrentHashMap<K, V> cache = new ConcurrentHashMap<>();
    private final ConcurrentHashMap<K, Long> expiryMap = new ConcurrentHashMap<>();
    private final long ttlNanos;

    public TimedCache(long ttl, TimeUnit unit) {
        this.ttlNanos = unit.toNanos(ttl);
    }

    public void put(K key, V value) {
        cache.put(key, value);
        expiryMap.put(key, System.nanoTime() + ttlNanos);
    }

    public V get(K key) {
        Long expiry = expiryMap.get(key);
        if (expiry == null || System.nanoTime() > expiry) {
            cache.remove(key);
            expiryMap.remove(key);
            return null;
        }
        return cache.get(key);
    }
}

// 使用:条目 10 秒后过期
TimedCache<String, User> sessionCache = new TimedCache<>(10, TimeUnit.SECONDS);

在调度任务时,ScheduledExecutorService 也直接使用 TimeUnit

ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
// 延迟 5 秒执行,之后每隔 2 秒执行一次
scheduler.scheduleAtFixedRate(
    () -> System.out.println("清理临时文件..."),
    5, 2, TimeUnit.SECONDS
);

6. 与 java.time.Duration 互转,桥接新旧 API

企业开发中经常需要将配置中的 Duration 转换为并发库所需的 (long, TimeUnit) 参数:

Duration timeout = Duration.ofSeconds(30);
// 转换为 TimeUnit 调用
future.get(timeout.toMillis(), TimeUnit.MILLISECONDS);

// 或直接用 TimeUnit 的 convert 方法
long seconds = TimeUnit.MILLISECONDS.convert(timeout.toMillis(), TimeUnit.MILLISECONDS);

TimeUnit 本身也提供了 toChronoUnit()TimeUnit.of(ChronoUnit) 实现与 ChronoUnit 互转,使得 Spring、Guava 等框架能无缝衔接。


三、总结

TimeUnit 作为一个枚举工具类,在企业开发中的核心价值是:

  • 类型安全:避免整数参数含义不明。
  • 转换安全:内部处理溢出并采用饱和策略,避免 ArithmeticException
  • 代码自文档化:看到 5, TimeUnit.SECONDS 就知道是 5 秒。
  • 性能优化:通过预缓存缩放因子和边界值,转换开销极低。

结合 Minecraft 的 TimeSource,我们看到了 TimeUnit 如何帮助抽象时间源:底层可以是纳秒、毫秒甚至游戏刻,上层只需通过 TimeUnit 获取任意单位的值,既保持底层精度,又让上层逻辑单位无关,这是多精度时间系统里非常典型的设计范式。

如果你正在设计自己的框架或工具类,凡是需要传递“时间长度”的地方,建议都用 (long duration, TimeUnit unit) 这样的方法签名,这会大幅度提升你 API 的易用性和稳健性。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐