浅谈关于Java中TimeZone锁竞争引发的问题解决

 更新时间:2026年02月06日 09:04:42   作者:桦说编程  
本文主要介绍了浅谈关于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)        │   │
│  └──────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────┘

核心问题

  1. 每次时间转换都触发 toTimeZone() 调用
  2. TimeZone.getTimeZone() 是同步方法(JDK 8)
  3. 高并发下大量线程竞争同一把类锁
  4. 实际业务中时区种类非常有限(如仅 Asia/ShanghaiUTC 等)

原始实现

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.computeIfAbsentJDK 原生,无依赖首次加载时仍有锁竞争;长 key 可能 hash 冲突
Caffeine高性能、自动驱逐、统计监控引入额外依赖
Guava Cache功能完善性能略逊于 Caffeine

选择 Caffeine 的原因:

  1. 高性能: 基于 W-TinyLFU 算法,近乎 O(1) 的读写性能
  2. 无锁读取: 读操作几乎无锁竞争
  3. 自动管理: 支持容量限制和自动驱逐
  4. 项目已有依赖: 无需引入新的 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/ShanghaiUTCAsia/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文件与Base64之间的转化方式

    Java文件与Base64之间的转化方式

    这篇文章介绍了如何使用Java将文件(如图片、视频)转换为Base64编码,以及如何将Base64编码转换回文件,通过提供具体的工具类实现,作者希望帮助读者更好地理解和应用这一过程
    2025-02-02
  • java反编译工具jd-gui-osx for mac M1芯片无法使用的问题及解决

    java反编译工具jd-gui-osx for mac M1芯片无法使用的问题及解决

    这篇文章主要介绍了java反编译工具jd-gui-osx for mac M1芯片无法使用的问题及解决方案,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-01-01
  • Spring Shell 命令行实现交互式Shell应用开发

    Spring Shell 命令行实现交互式Shell应用开发

    本文主要介绍了Spring Shell 命令行实现交互式Shell应用开发,能够帮助开发者快速构建功能丰富的命令行应用程序,具有一定的参考价值,感兴趣的可以了解一下
    2025-04-04
  • 浅谈list.removeAll()删除失败的原因及解决

    浅谈list.removeAll()删除失败的原因及解决

    这篇文章主要介绍了浅谈list.removeAll()删除失败的原因及解决方案,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-12-12
  • java Spring MVC4环境搭建实例详解(步骤)

    java Spring MVC4环境搭建实例详解(步骤)

    spring WEB MVC框架提供了一个MVC(model-view-controller)模型-视图-控制器的结构和组件,利用它可以开发更灵活、松耦合的web应用。MVC模式使得整个服务应用的各部分(控制逻辑、业务逻辑、UI界面展示)分离开来,使它们之间的耦合性更低
    2017-08-08
  • SpringBoot整合Zuul全过程

    SpringBoot整合Zuul全过程

    Zuul网关是微服务架构中的重要组件,具备统一入口、鉴权校验、动态路由等功能,它通过配置文件进行灵活的路由和过滤器设置,支持Hystrix进行容错处理,还提供了限流保护和性能调优
    2025-12-12
  • Mac Maven环境搭建安装和配置超详细步骤

    Mac Maven环境搭建安装和配置超详细步骤

    这篇文章主要给大家介绍了关于Mac Maven环境搭建安装和配置的超详细步骤,Maven是一种常用的Java构建工具,它可以自动化构建、测试和打包Java项目,文中通过图文介绍的非常详细,需要的朋友可以参考下
    2023-10-10
  • 如何用java程序(JSch)运行远程linux主机上的shell脚本

    如何用java程序(JSch)运行远程linux主机上的shell脚本

    这篇文章主要介绍了如何用java程序(JSch)运行远程linux主机上的shell脚本,帮助大家更好的理解和学习,感兴趣的朋友可以了解下
    2020-08-08
  • Java如何实现树的同构?

    Java如何实现树的同构?

    今天给大家带来的是关于Java的相关知识,文章围绕着Java如何实现树的同构展开,文中有非常详细的介绍及代码示例,需要的朋友可以参考下
    2021-06-06
  • Java利用AlphaComposite类合并图像

    Java利用AlphaComposite类合并图像

    这篇文章主要介绍了Java利用AlphaComposite类合并图像,帮助大家更好的利用Java处理图像,感兴趣的朋友可以了解下
    2020-10-10

最新评论