淺談關(guān)于Java中TimeZone鎖競爭引發(fā)的問題解決
背景
在高并發(fā)服務(wù)的性能排查中,我們通過線程 dump 發(fā)現(xiàn)了大量線程阻塞在同一把鎖上。本文將詳細(xì)分析問題根因,并介紹我的優(yōu)化方案。
問題發(fā)現(xiàn)
在生產(chǎn)環(huán)境進(jìn)行 thread dump 時(shí),發(fā)現(xiàn)多個(gè)工作線程(20+)處于 BLOCKED 狀態(tài),等待同一個(gè) 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)
關(guān)鍵信息:
- 阻塞位置:
java.util.TimeZone.getTimeZone - 等待對象:
java.lang.Class@37b99a71(類鎖) - 調(diào)用來源: Joda-Time 的
DateTimeZone.toTimeZone()方法
問題分析
JDK TimeZone.getTimeZone 的鎖機(jī)制
TimeZone.getTimeZone(String ID) 方法內(nèi)部實(shí)現(xiàn)使用了 synchronized 關(guān)鍵字:
public static synchronized TimeZone getTimeZone(String ID) {
// 從緩存或文件加載時(shí)區(qū)信息
...
}
這意味著所有調(diào)用該方法的線程都需要競爭同一把類鎖。
Joda-Time 的調(diào)用鏈
分析 Joda-Time 源碼,DateTime.toGregorianCalendar() 的實(shí)現(xiàn)如下:
// org.joda.time.base.AbstractDateTime
public GregorianCalendar toGregorianCalendar() {
DateTimeZone zone = getZone();
GregorianCalendar cal = new GregorianCalendar(zone.toTimeZone());
cal.setTime(toDate());
return cal;
}
每次調(diào)用都會(huì)執(zhí)行 zone.toTimeZone(),最終觸發(fā) TimeZone.getTimeZone()。
問題根因
┌─────────────────────────────────────────────────────────────────┐
│ 高并發(fā)請求 │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Thread-1 Thread-2 Thread-3 ... Thread-N │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ toCalendar() toCalendar() toCalendar() toCalendar() │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ toGregorianCalendar() ──────────────────────────────────── │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ toTimeZone() ───────────────────────────────────────────── │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ TimeZone.getTimeZone() - synchronized 類鎖 │ │
│ │ │ │
│ │ Thread-1: 獲取鎖,執(zhí)行中... │ │
│ │ Thread-2: BLOCKED (waiting on java.lang.Class) │ │
│ │ Thread-3: BLOCKED (waiting on java.lang.Class) │ │
│ │ ... │ │
│ │ Thread-N: BLOCKED (waiting on java.lang.Class) │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
核心問題:
- 每次時(shí)間轉(zhuǎn)換都觸發(fā)
toTimeZone()調(diào)用 TimeZone.getTimeZone()是同步方法(JDK 8)- 高并發(fā)下大量線程競爭同一把類鎖
- 實(shí)際業(yè)務(wù)中時(shí)區(qū)種類非常有限(如僅
Asia/Shanghai、UTC等)
原始實(shí)現(xiàn)
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;
}
// 問題所在:每次都調(diào)用 toGregorianCalendar()
result = dateTime.toGregorianCalendar();
return result;
}
優(yōu)化方案
設(shè)計(jì)思路
由于底層API在項(xiàng)目中頻繁使用,全部改動(dòng)風(fēng)險(xiǎn)比較大,我也不想因?yàn)槌鲥e(cuò)而背鍋,所以考慮消除鎖競爭的方案。
既然時(shí)區(qū)數(shù)量有限,我們可以緩存 DateTimeZone 到 TimeZone 的映射,避免重復(fù)調(diào)用 toTimeZone()。
技術(shù)選型:為什么選擇 Caffeine
| 方案 | 優(yōu)點(diǎn) | 缺點(diǎn) |
|---|---|---|
| HashMap + synchronized | 實(shí)現(xiàn)簡單 | 鎖粒度粗,與原問題類似 |
| ConcurrentHashMap.computeIfAbsent | JDK 原生,無依賴 | 首次加載時(shí)仍有鎖競爭;長 key 可能 hash 沖突 |
| Caffeine | 高性能、自動(dòng)驅(qū)逐、統(tǒng)計(jì)監(jiān)控 | 引入額外依賴 |
| Guava Cache | 功能完善 | 性能略遜于 Caffeine |
選擇 Caffeine 的原因:
- 高性能: 基于 W-TinyLFU 算法,近乎 O(1) 的讀寫性能
- 無鎖讀取: 讀操作幾乎無鎖競爭
- 自動(dòng)管理: 支持容量限制和自動(dòng)驅(qū)逐
- 項(xiàng)目已有依賴: 無需引入新的 jar 包
優(yōu)化后的實(shí)現(xiàn)
/**
* 緩存開關(guān),驅(qū)逐時(shí)降級為直接調(diào)用
*/
static volatile boolean zoneCacheEnable = true;
/**
* DateTimeZone -> TimeZone 緩存
* 生產(chǎn)環(huán)境時(shí)區(qū)種類有限(通常 < 5 種),設(shè)置 maximumSize = 24 足夠
*/
static LoadingCache<DateTimeZone, TimeZone> zoneCache = Caffeine.newBuilder()
.maximumSize(24)
.evictionListener((k, v, c) -> {
// 正常情況不應(yīng)該觸發(fā)驅(qū)逐,如果觸發(fā)說明時(shí)區(qū)種類異常多
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();
}
/**
* 優(yōu)化后的 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;
}
// 優(yōu)化:從緩存獲取 TimeZone,避免鎖競爭
DateTimeZone zone = dateTime.getZone();
TimeZone timeZone = getTimeZoneFromCache(zone);
GregorianCalendar cal = new GregorianCalendar(timeZone);
cal.setTime(dateTime.toDate());
return cal;
}
優(yōu)化前后對比
優(yōu)化前: ┌─────────────────────────────────────────────────────────┐ │ N 個(gè)線程同時(shí)調(diào)用 toCalendar() │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────┐ │ │ │ TimeZone.getTimeZone() - synchronized 類鎖 │ │ │ │ 所有線程串行等待 │ │ │ └─────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────┘ 優(yōu)化后: ┌─────────────────────────────────────────────────────────┐ │ N 個(gè)線程同時(shí)調(diào)用 toCalendar() │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────┐ │ │ │ Caffeine Cache - 無鎖讀取 │ │ │ │ 所有線程并行獲取緩存的 TimeZone │ │ │ └─────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────┘
關(guān)鍵設(shè)計(jì)決策
1. 為什么設(shè)置 maximumSize = 24
- 國內(nèi)業(yè)務(wù)主要涉及
Asia/Shanghai、UTC、Asia/Hong_Kong等少數(shù)幾個(gè)時(shí)區(qū) - 如果超過 24 個(gè)不同時(shí)區(qū),說明數(shù)據(jù)異常,觸發(fā)告警
2. evictionListener 的作用
.evictionListener((k, v, c) -> {
LOGGER.error("zone cache evicted k={}, v={}, cause={}", k, v, c);
zoneCacheEnable = false;
})
- 正常情況下不應(yīng)該觸發(fā)驅(qū)逐(時(shí)區(qū)數(shù)量 < 8)
- 如果觸發(fā),說明:
- 存在異常數(shù)據(jù)導(dǎo)致時(shí)區(qū)種類過多
- 或者緩存配置需要調(diào)整
- 記錄錯(cuò)誤日志便于排查
- 禁用緩存作為 fallback,避免頻繁驅(qū)逐帶來的性能損耗
3. volatile 關(guān)鍵字的使用
static volatile boolean zoneCacheEnable = true;
- 保證多線程間的可見性
- 允許在發(fā)現(xiàn)異常時(shí)快速降級
總結(jié)
通過引入 Caffeine 緩存,我們成功消除了 TimeZone.getTimeZone() 的鎖競爭問題。這個(gè)優(yōu)化的核心思想是:對于數(shù)量有限、創(chuàng)建成本高的對象,使用緩存來換取并發(fā)性能。
不要使用過時(shí)的API。
到此這篇關(guān)于淺談關(guān)于Java中TimeZone鎖競爭引發(fā)的問題解決的文章就介紹到這了,更多相關(guān)Java TimeZone鎖競爭內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java反編譯工具jd-gui-osx?for?mac?M1芯片無法使用的問題及解決
這篇文章主要介紹了java反編譯工具jd-gui-osx?for?mac?M1芯片無法使用的問題及解決方案,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-01-01
Spring Shell 命令行實(shí)現(xiàn)交互式Shell應(yīng)用開發(fā)
本文主要介紹了Spring Shell 命令行實(shí)現(xiàn)交互式Shell應(yīng)用開發(fā),能夠幫助開發(fā)者快速構(gòu)建功能豐富的命令行應(yīng)用程序,具有一定的參考價(jià)值,感興趣的可以了解一下2025-04-04
java Spring MVC4環(huán)境搭建實(shí)例詳解(步驟)
spring WEB MVC框架提供了一個(gè)MVC(model-view-controller)模型-視圖-控制器的結(jié)構(gòu)和組件,利用它可以開發(fā)更靈活、松耦合的web應(yīng)用。MVC模式使得整個(gè)服務(wù)應(yīng)用的各部分(控制邏輯、業(yè)務(wù)邏輯、UI界面展示)分離開來,使它們之間的耦合性更低2017-08-08
Mac?Maven環(huán)境搭建安裝和配置超詳細(xì)步驟
這篇文章主要給大家介紹了關(guān)于Mac?Maven環(huán)境搭建安裝和配置的超詳細(xì)步驟,Maven是一種常用的Java構(gòu)建工具,它可以自動(dòng)化構(gòu)建、測試和打包Java項(xiàng)目,文中通過圖文介紹的非常詳細(xì),需要的朋友可以參考下2023-10-10
如何用java程序(JSch)運(yùn)行遠(yuǎn)程linux主機(jī)上的shell腳本
這篇文章主要介紹了如何用java程序(JSch)運(yùn)行遠(yuǎn)程linux主機(jī)上的shell腳本,幫助大家更好的理解和學(xué)習(xí),感興趣的朋友可以了解下2020-08-08

