浅谈关于Java中TimeZone锁竞争引发的问题解决
背景
在高并发服务的性能排查中,我们通过线程 dump 发现了大量线程阻塞在同一把锁上。本文将详细分析问题根因,并介绍我的优化方案。
问题发现
在生产环境进行 thread dump 时,发现多个工作线程(20+)处于 BLOCKED 状态,等待同一个 Class 对象锁:
at java.util.TimeZone.getTimeZone(TimeZone.java:549)
- waiting on java.lang.Class@37b99a71
at org.joda.time.DateTimeZone.toTimeZone(DateTimeZone.java:1250)
at org.joda.time.base.AbstractDateTime.toGregorianCalendar(AbstractDateTime.java:296)
at xxx.common.DateTimeUtils.toCalendar(DateTimeUtils.java:469)
at xxx.business.RefundEndorseBusiness.doSetRefundEndorseFee(...)
...
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
关键信息:
- 阻塞位置:
java.util.TimeZone.getTimeZone - 等待对象:
java.lang.Class@37b99a71(类锁) - 调用来源: Joda-Time 的
DateTimeZone.toTimeZone()方法
问题分析
JDK TimeZone.getTimeZone 的锁机制
TimeZone.getTimeZone(String ID) 方法内部实现使用了 synchronized 关键字:
public static synchronized TimeZone getTimeZone(String ID) {
// 从缓存或文件加载时区信息
...
}
这意味着所有调用该方法的线程都需要竞争同一把类锁。
Joda-Time 的调用链
分析 Joda-Time 源码,DateTime.toGregorianCalendar() 的实现如下:
// org.joda.time.base.AbstractDateTime
public GregorianCalendar toGregorianCalendar() {
DateTimeZone zone = getZone();
GregorianCalendar cal = new GregorianCalendar(zone.toTimeZone());
cal.setTime(toDate());
return cal;
}
每次调用都会执行 zone.toTimeZone(),最终触发 TimeZone.getTimeZone()。
问题根因
┌─────────────────────────────────────────────────────────────────┐
│ 高并发请求 │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Thread-1 Thread-2 Thread-3 ... Thread-N │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ toCalendar() toCalendar() toCalendar() toCalendar() │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ toGregorianCalendar() ──────────────────────────────────── │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ toTimeZone() ───────────────────────────────────────────── │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ TimeZone.getTimeZone() - synchronized 类锁 │ │
│ │ │ │
│ │ Thread-1: 获取锁,执行中... │ │
│ │ Thread-2: BLOCKED (waiting on java.lang.Class) │ │
│ │ Thread-3: BLOCKED (waiting on java.lang.Class) │ │
│ │ ... │ │
│ │ Thread-N: BLOCKED (waiting on java.lang.Class) │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
核心问题:
- 每次时间转换都触发
toTimeZone()调用 TimeZone.getTimeZone()是同步方法(JDK 8)- 高并发下大量线程竞争同一把类锁
- 实际业务中时区种类非常有限(如仅
Asia/Shanghai、UTC等)
原始实现
public static Calendar toCalendar(DateTime dateTime) {
Calendar result;
if (dateTime == null || DateTimeUtils.isLogicMin(dateTime)) {
result = new GregorianCalendar(1, 0, 1, 0, 0, 0);
result.setTimeZone(TimeZone.getTimeZone("UTC"));
return result;
}
if (DateTimeUtils.isLogicMax(dateTime)) {
result = new GregorianCalendar(9999, Calendar.DECEMBER, 31, 0, 0, 0);
result.setTimeZone(TimeZone.getTimeZone("UTC"));
return result;
}
// 问题所在:每次都调用 toGregorianCalendar()
result = dateTime.toGregorianCalendar();
return result;
}
优化方案
设计思路
由于底层API在项目中频繁使用,全部改动风险比较大,我也不想因为出错而背锅,所以考虑消除锁竞争的方案。
既然时区数量有限,我们可以缓存 DateTimeZone 到 TimeZone 的映射,避免重复调用 toTimeZone()。
技术选型:为什么选择 Caffeine
| 方案 | 优点 | 缺点 |
|---|---|---|
| HashMap + synchronized | 实现简单 | 锁粒度粗,与原问题类似 |
| ConcurrentHashMap.computeIfAbsent | JDK 原生,无依赖 | 首次加载时仍有锁竞争;长 key 可能 hash 冲突 |
| Caffeine | 高性能、自动驱逐、统计监控 | 引入额外依赖 |
| Guava Cache | 功能完善 | 性能略逊于 Caffeine |
选择 Caffeine 的原因:
- 高性能: 基于 W-TinyLFU 算法,近乎 O(1) 的读写性能
- 无锁读取: 读操作几乎无锁竞争
- 自动管理: 支持容量限制和自动驱逐
- 项目已有依赖: 无需引入新的 jar 包
优化后的实现
/**
* 缓存开关,驱逐时降级为直接调用
*/
static volatile boolean zoneCacheEnable = true;
/**
* DateTimeZone -> TimeZone 缓存
* 生产环境时区种类有限(通常 < 5 种),设置 maximumSize = 24 足够
*/
static LoadingCache<DateTimeZone, TimeZone> zoneCache = Caffeine.newBuilder()
.maximumSize(24)
.evictionListener((k, v, c) -> {
// 正常情况不应该触发驱逐,如果触发说明时区种类异常多
LOGGER.error("zone cache evicted k={}, v={}, cause={}", k, v, c);
zoneCacheEnable = false;
})
.build(DateTimeZone::toTimeZone);
/**
* 从缓存获取 TimeZone,带 fallback
*/
static TimeZone getTimeZoneFromCache(DateTimeZone zone) {
return zoneCacheEnable ? zoneCache.get(zone) : zone.toTimeZone();
}
/**
* 优化后的 toCalendar 方法
*/
public static Calendar toCalendar(DateTime dateTime) {
Calendar result;
if (dateTime == null || DateTimeUtils.isLogicMin(dateTime)) {
result = new GregorianCalendar(1, 0, 1, 0, 0, 0);
result.setTimeZone(TimeZone.getTimeZone("UTC"));
return result;
}
if (DateTimeUtils.isLogicMax(dateTime)) {
result = new GregorianCalendar(9999, Calendar.DECEMBER, 31, 0, 0, 0);
result.setTimeZone(TimeZone.getTimeZone("UTC"));
return result;
}
// 优化:从缓存获取 TimeZone,避免锁竞争
DateTimeZone zone = dateTime.getZone();
TimeZone timeZone = getTimeZoneFromCache(zone);
GregorianCalendar cal = new GregorianCalendar(timeZone);
cal.setTime(dateTime.toDate());
return cal;
}
优化前后对比
优化前: ┌─────────────────────────────────────────────────────────┐ │ N 个线程同时调用 toCalendar() │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────┐ │ │ │ TimeZone.getTimeZone() - synchronized 类锁 │ │ │ │ 所有线程串行等待 │ │ │ └─────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────┘ 优化后: ┌─────────────────────────────────────────────────────────┐ │ N 个线程同时调用 toCalendar() │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────┐ │ │ │ Caffeine Cache - 无锁读取 │ │ │ │ 所有线程并行获取缓存的 TimeZone │ │ │ └─────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────┘
关键设计决策
1. 为什么设置 maximumSize = 24
- 国内业务主要涉及
Asia/Shanghai、UTC、Asia/Hong_Kong等少数几个时区 - 如果超过 24 个不同时区,说明数据异常,触发告警
2. evictionListener 的作用
.evictionListener((k, v, c) -> {
LOGGER.error("zone cache evicted k={}, v={}, cause={}", k, v, c);
zoneCacheEnable = false;
})
- 正常情况下不应该触发驱逐(时区数量 < 8)
- 如果触发,说明:
- 存在异常数据导致时区种类过多
- 或者缓存配置需要调整
- 记录错误日志便于排查
- 禁用缓存作为 fallback,避免频繁驱逐带来的性能损耗
3. volatile 关键字的使用
static volatile boolean zoneCacheEnable = true;
- 保证多线程间的可见性
- 允许在发现异常时快速降级
总结
通过引入 Caffeine 缓存,我们成功消除了 TimeZone.getTimeZone() 的锁竞争问题。这个优化的核心思想是:对于数量有限、创建成本高的对象,使用缓存来换取并发性能。
不要使用过时的API。
到此这篇关于浅谈关于Java中TimeZone锁竞争引发的问题解决的文章就介绍到这了,更多相关Java TimeZone锁竞争内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
java反编译工具jd-gui-osx for mac M1芯片无法使用的问题及解决
这篇文章主要介绍了java反编译工具jd-gui-osx for mac M1芯片无法使用的问题及解决方案,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教2024-01-01
Spring Shell 命令行实现交互式Shell应用开发
本文主要介绍了Spring Shell 命令行实现交互式Shell应用开发,能够帮助开发者快速构建功能丰富的命令行应用程序,具有一定的参考价值,感兴趣的可以了解一下2025-04-04
如何用java程序(JSch)运行远程linux主机上的shell脚本
这篇文章主要介绍了如何用java程序(JSch)运行远程linux主机上的shell脚本,帮助大家更好的理解和学习,感兴趣的朋友可以了解下2020-08-08


最新评论