文章目录
前言
在日常开发中,任何涉及超时、延迟、定时任务、性能统计的场景都会遇到时间单位转换问题。
你可能会写出 Thread.sleep(1000) 这样的代码,但1000代表的是毫秒吗?还是秒?如果需求变成3秒,你可能需要心算一下换算。
为了解决可读性和时间单位转换的痛点,Java从1.5版本引入的 java.util.concurrent.TimeUnit 枚举工具。
它不仅让代码更具可读性,还封装了许多实用的时间操作方法,并且**java.util.concurrent 并发包**中引入各种工具类集成。
一、TimeUnit 基础
1.1 基础定义
TimeUnit 是 Java 1.5 引入的一个枚举类(Enum)。顾名思义,它代表了时间单位。
它的设计初衷是为了在多线程并发编程中提供一种清晰、跨平台的时间表示方式,同时解决时间单位换算时的易错问题。
TimeUnit 涵盖了从纳秒到天的所有常用时间维度。打开源码,你可以看到它定义了以下 7 个枚举常量:
NANOSECONDS:纳秒(千分之一微秒)MICROSECONDS:微秒(千分之一毫秒)MILLISECONDS:毫秒(千分之一秒)SECONDS:秒MINUTES:分钟HOURS:小时DAYS:天
1.2 时间转换
TimeUnit 提供了两种转换时间维度的方式:
- 直接转换法 (
toXxx):将当前枚举表示的时间,转换为指定单位的数值。 - 灵活转换法 (
convert):将给定数值和给定单位,转换为当前枚举代表的单位。
convert 方法
将一个给定单位的时长转换为当前枚举单位的值:
java
long millis = TimeUnit.SECONDS.convert(1, TimeUnit.MINUTES); // 1分钟转成秒 = 60
long hours = TimeUnit.HOURS.convert(3600000, TimeUnit.MILLISECONDS); // 3600000毫秒 = 1小时
toXxx 方法
将当前 TimeUnit 实例代表的时间值转换为其他单位,但不改变原值:
java
// 示例 1:1 小时等于多少分钟?
long minutes = TimeUnit.HOURS.toMinutes(1);
System.out.println("1小时 = " + minutes + " 分钟"); // 输出: 60
// 示例 2:3 天等于多少秒?
long seconds = TimeUnit.DAYS.toSeconds(3);
System.out.println("3天 = " + seconds + " 秒"); // 输出: 259200
// 示例 3:把 120 分钟转换为小时 (使用 convert)
// 语境:我想得到 HOURS,所以用 TimeUnit.HOURS.convert,传入的数据是 120,单位是 MINUTES
long hours = TimeUnit.HOURS.convert(120, TimeUnit.MINUTES);
System.out.println("120分钟 = " + hours + " 小时"); // 输出: 2
1.3 增强的可读性休眠与等待
如果你在编写底层的多线程同步代码,TimeUnit 提供了对 Object.wait() 和 Thread.join() 的优雅封装,避免手动计算毫秒和纳秒。
TimeUnit 直接提供了线程控制方法:
java
// 传统写法:可读性差,容易弄错单位
Thread.sleep(2000); // 是2秒还是2毫秒?
// 使用 TimeUnit,一目了然
TimeUnit.SECONDS.sleep(2);
TimeUnit.MILLISECONDS.sleep(500);
TimeUnit.MINUTES.sleep(1);
// 类似的还有 timedJoin 和 timedWait
Thread t = new Thread(() -> {});
t.start();
TimeUnit.SECONDS.timedJoin(t, 5); // 等待最多5秒
Object lock = new Object();
synchronized (lock) {
TimeUnit.SECONDS.timedWait(lock, 3); // 在锁上等待3秒
}
这些方法内部会调用 Thread.sleep 等底层方法,但会处理好中断异常,并对负数或零值进行直接返回的优化。
二、使用场景
TimeUnit 在涉及到缓存、网络、并发锁等需要超时控制的场景出场率很高。
2.1 缓存过期时间 (TTL) 设置
无论是操作 Redis 还是本地缓存(如 Guava Cache、Caffeine),在设置 key 的存活时间时,规范的 API 都会要求传入 TimeUnit。
Java
// 伪代码:向 Redis 写入数据,过期时间为 2 小时
redisTemplate.opsForValue().set("user:token:1001", "abcdefg", 2, TimeUnit.HOURS);
// 本地缓存 Caffeine 设置过期时间
Cache<String, String> cache = Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES) // 写入后 10 分钟过期
.build();
2.2 并发锁的超时获取
在使用 java.util.concurrent.locks.Lock(如 ReentrantLock)时,为了防止死锁,我们通常会使用带有超时机制的加锁方式。
Java
Lock lock = new ReentrantLock();
try {
// 尝试获取锁,最多等待 3 秒。如果 3 秒还没拿到,就返回 false,不继续死等。
boolean isLocked = lock.tryLock(3, TimeUnit.SECONDS);
if (isLocked) {
try {
// 执行业务逻辑
System.out.println("拿到锁,开始处理...");
} finally {
lock.unlock(); // 务必在 finally 中释放锁
}
} else {
System.out.println("获取锁超时,放弃处理或执行降级逻辑。");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
2.3 线程池与并发工具的等待控制
在线程池管理或者使用 CountDownLatch 等并发协调工具时,TimeUnit 也是标配。
Java
CountDownLatch latch = new CountDownLatch(3);
// 主线程等待子线程完成,最多只等 10 秒
boolean completed = latch.await(10, TimeUnit.SECONDS);
if (!completed) {
System.out.println("子线程执行太慢,主线程不等了,直接返回!");
}
// 线程池优雅关闭,等待已提交任务执行完毕,最多等 1 分钟
executorService.shutdown();
executorService.awaitTermination(1, TimeUnit.MINUTES);
总结
TimeUnit 本质上是 Java 并发编程中用于统一时间单位表达与转换的工具类。
相比直接写 1000、5000 这样的"魔法数字",它能让代码的可读性、可维护性和安全性大幅提升。
在实际开发中,TimeUnit 几乎贯穿所有涉及超时控制、线程等待、缓存过期、锁机制、线程池管理 的场景,是 java.util.concurrent 并发体系中的基础组件之一。
核心记忆点
- 使用
SECONDS.sleep(2)比Thread.sleep(2000)更清晰 convert()用于不同单位之间灵活换算toXxx()用于当前单位直接转换- 在并发开发中,
TimeUnit几乎是所有超时 API 的"标准搭档"