问题介绍

在日常的开发过程中,我们不可避免地会使用到 JDK8 之前的 Date 类,在格式化日期或解析日期时就需要用到 SimpleDateFormat 类,但由于该类并不是线程安全的,所以我们常发现对该类的不恰当使用会导致日期解析异常,从而影响线上服务可用率。

以下是对 SimpleDateFormat 类不恰当使用的示例代码:

package com.jd.threadsafe;

import java.text.SimpleDateFormat;
import java.util.concurrent.atomic.AtomicBoolean; /**
* @Date: 2023/7/25 10:47
* @Desc: SimpleDateFormat 线程安全问题复现
* @Version: V1.0
**/
public class SimpleDateFormatTest {
private static final AtomicBoolean STOP = new AtomicBoolean();
private static final SimpleDateFormat FORMATTER = new SimpleDateFormat("yyyy-M-d"); // 非线程安全 public static void main(String[] args) {
Runnable runnable = () -> {
int count = 0;
while (!STOP.get()) {
try {
FORMATTER.parse("2023-7-15");
} catch (Exception e) {
e.printStackTrace();
if (++count > 3) {
STOP.set(true);
}
}
}
}; new Thread(runnable).start();
new Thread(runnable).start();
}
}

以上代码模拟了多线程并发使用 SimpleDateFormat 实例的场景,此时可观察到如下异常输出:

java.lang.NumberFormatException: For input string: ""
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:601)
at java.lang.Long.parseLong(Long.java:631)
at java.text.DigitList.getLong(DigitList.java:195)
at java.text.DecimalFormat.parse(DecimalFormat.java:2082)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at com.jd.threadsafe.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:21)
at java.lang.Thread.run(Thread.java:750)
java.lang.NumberFormatException: For input string: ""
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:601)
at java.lang.Long.parseLong(Long.java:631)
at java.text.DigitList.getLong(DigitList.java:195)
at java.text.DecimalFormat.parse(DecimalFormat.java:2082)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at com.jd.threadsafe.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:21)
at java.lang.Thread.run(Thread.java:750)
java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
at java.lang.Double.parseDouble(Double.java:538)
at java.text.DigitList.getDouble(DigitList.java:169)
at java.text.DecimalFormat.parse(DecimalFormat.java:2087)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at com.jd.threadsafe.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:21)
at java.lang.Thread.run(Thread.java:750)
java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
at java.lang.Double.parseDouble(Double.java:538)
at java.text.DigitList.getDouble(DigitList.java:169)
at java.text.DecimalFormat.parse(DecimalFormat.java:2087)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at com.jd.threadsafe.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:21)
at java.lang.Thread.run(Thread.java:750)
java.lang.NumberFormatException: For input string: ""
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:601)
at java.lang.Long.parseLong(Long.java:631)
at java.text.DigitList.getLong(DigitList.java:195)
at java.text.DecimalFormat.parse(DecimalFormat.java:2082)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at com.jd.threadsafe.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:21)
at java.lang.Thread.run(Thread.java:750)

以上异常的根本原因是因为 SimpleDateFormat 是有状态的,如 SimpleDateFormat 类中含有非线程安全的 NumberFormat 成员变量:

/**
* The number formatter that <code>DateFormat</code> uses to format numbers
* in dates and times. Subclasses should initialize this to a number format
* appropriate for the locale associated with this <code>DateFormat</code>.
* @serial
*/
protected NumberFormat numberFormat;

从 NumberFormat 的 Java Doc 中能看到如下描述:

Synchronization Number formats are generally not synchronized. It is recommended to create separate format instances for each thread. If multiple threads access a format concurrently, it must be synchronized externally.

从 SimpleDateFormat 的 Java Doc 中能看到如下描述:

Synchronization Date formats are not synchronized. It is recommended to create separate format instances for each thread. If multiple threads access a format concurrently, it must be synchronized externally.

修复方案一:加锁(不推荐)

package com.jd.threadsafe;

import java.text.SimpleDateFormat;
import java.util.concurrent.atomic.AtomicBoolean; /**
* @Date: 2023/7/25 10:47
* @Desc: SimpleDateFormat 线程安全修复方案:加锁
* @Version: V1.0
**/
public class SimpleDateFormatLockTest {
private static final AtomicBoolean STOP = new AtomicBoolean();
private static final SimpleDateFormat FORMATTER = new SimpleDateFormat("yyyy-M-d"); // 非线程安全 public static void main(String[] args) {
Runnable runnable = () -> {
int count = 0;
while (!STOP.get()) {
try {
synchronized (FORMATTER) {
FORMATTER.parse("2023-7-15");
}
} catch (Exception e) {
e.printStackTrace();
if (++count > 3) {
STOP.set(true);
}
}
}
}; new Thread(runnable).start();
new Thread(runnable).start();
} }

首先我们能想到的最简单的解决线程安全问题的修复方案即加锁,如以上修复方案,使用 synchronized 关键字对 FORMATTER 实例进行加锁,此时多线程进行日期格式化时退化为串行执行,保证了正确性牺牲了性能,不推荐。

修复方案二:栈封闭(不推荐)

如果按照文档中的推荐用法,可知推荐为每个线程创建独立的 SimpleDateFormat 实例,一种最简单的方式就是在方法调用时每次创建 SimpleDateFormat 实例,以实现栈封闭的效果,如以下示例代码:

package com.jd.threadsafe;

import java.text.SimpleDateFormat;
import java.util.concurrent.atomic.AtomicBoolean; /**
* @Date: 2023/7/25 10:47
* @Desc: SimpleDateFormat 线程安全修复方案:栈封闭
* @Version: V1.0
**/
public class SimpleDateFormatStackConfinementTest {
private static final AtomicBoolean STOP = new AtomicBoolean(); public static void main(String[] args) {
Runnable runnable = () -> {
int count = 0;
while (!STOP.get()) {
try {
new SimpleDateFormat("yyyy-M-d").parse("2023-7-15");
} catch (Exception e) {
e.printStackTrace();
if (++count > 3) {
STOP.set(true);
}
}
}
}; new Thread(runnable).start();
new Thread(runnable).start();
} }

即将共用的 SimpleDateFormat 实例调整为每次创建新的实例,该修复方案保证了正确性但每次方法调用需要创建 SimpleDateFormat 实例,并未复用 SimpleDateFormat 实例,存在 GC 损耗,所以并不推荐。

修复方案三:ThreadLocal(推荐)

如果日期格式化操作是应用里的高频操作,且需要优先保证性能,那么建议每个线程复用 SimpleDateFormat 实例,此时可引入 ThreadLocal 类来解决该问题:

package com.jd.threadsafe;

import java.text.SimpleDateFormat;
import java.util.concurrent.atomic.AtomicBoolean; /**
* @Date: 2023/7/25 10:47
* @Desc: SimpleDateFormat 线程安全修复方案:ThreadLocal
* @Version: V1.0
**/
public class SimpleDateFormatThreadLocalTest {
private static final AtomicBoolean STOP = new AtomicBoolean();
private static final ThreadLocal<SimpleDateFormat> SIMPLE_DATE_FORMAT_THREAD_LOCAL = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-M-d")); public static void main(String[] args) {
Runnable runnable = () -> {
int count = 0;
while (!STOP.get()) {
try {
SIMPLE_DATE_FORMAT_THREAD_LOCAL.get().parse("2023-7-15");
} catch (Exception e) {
e.printStackTrace();
if (++count > 3) {
STOP.set(true);
}
}
}
}; new Thread(runnable).start();
new Thread(runnable).start();
} }

执行上述代码,不会再观察到异常输出,因为已为每个线程创建了独立的 SimpleDateFormat 实例,即在线程维度复用了 SimpleDateFormat 实例,在线程池等池化场景下相比上方栈封闭的修复方案降低了 GC 损耗,同时也规避了线程安全问题。

以上使用 ThreadLocal 在线程维度复用非线程安全的实例可认为是一种通用的模式,可在 JDK 及不少开源项目中看到类似的模式实现,如在 JDK 最常见的 String 类中,对字符串进行编解码所需要用到的 StringDecoder 及 StringEncoder 即使用了 ThreadLocal 来规避线程安全问题:

/**
* Utility class for string encoding and decoding.
*/
class StringCoding { private StringCoding() { } /** The cached coders for each thread */
private final static ThreadLocal<SoftReference<StringDecoder>> decoder =
new ThreadLocal<>();
private final static ThreadLocal<SoftReference<StringEncoder>> encoder =
new ThreadLocal<>(); // ...
}

参考:JDK8 - StringCoding

在 Dubbo 的 ThreadLocalKryoFactory 类中,在对非线程安全类 Kryo 的使用中,也使用了 ThreadLocal 类来规避线程安全问题:

package org.apache.dubbo.common.serialize.kryo.utils;

import com.esotericsoftware.kryo.Kryo;

public class ThreadLocalKryoFactory extends AbstractKryoFactory {

    private final ThreadLocal<Kryo> holder = new ThreadLocal<Kryo>() {
@Override
protected Kryo initialValue() {
return create();
}
}; @Override
public void returnKryo(Kryo kryo) {
// do nothing
} @Override
public Kryo getKryo() {
return holder.get();
}
}

参考:Dubbo - ThreadLocalKryoFactory

类似地,在 HikariCP 的 ConcurrentBag 类中,也用到了 ThreadLocal 类来规避线程安全问题,此处不再进一步展开。

修复方案四:FastDateFormat(推荐)

针对 SimpleDateFormat 类的线程安全问题,apache commons-lang 提供了 FastDateFormat 类。其部分 Java Doc 如下:

FastDateFormat is a fast and thread-safe version ofSimpleDateFormat. To obtain an instance of FastDateFormat, use one of the static factory methods:getInstance(String, TimeZone, Locale),getDateInstance(int, TimeZone, Locale),getTimeInstance(int, TimeZone, Locale), orgetDateTimeInstance(int, int, TimeZone, Locale) Since FastDateFormat is thread safe, you can use a static member instance: private static final FastDateFormat DATE_FORMATTER = FastDateFormat.getDateTimeInstance(FastDateFormat.LONG, FastDateFormat.SHORT); This class can be used as a direct replacement toSimpleDateFormatin most formatting and parsing situations. This class is especially useful in multi-threaded server environments.SimpleDateFormatis not thread-safe in any JDK version, nor will it be as Sun have closed the bug/RFE. All patterns are compatible with SimpleDateFormat (except time zones and some year patterns - see below).

该修复方案相对来说代码改造最小,仅需在声明静态 SimpleDateFormat 实例代码处将 SimpleDateFormat 实例替换为 FastDateFormat 实例,示例代码如下:

package com.jd.threadsafe;

import org.apache.commons.lang3.time.FastDateFormat;

import java.util.concurrent.atomic.AtomicBoolean;

/**
* @Date: 2023/7/6 20:05
* @Desc: SimpleDateFormat 线程安全修复方案:FastDateFormat
* @Version: V1.0
**/
public class FastDateFormatTest {
private static final AtomicBoolean STOP = new AtomicBoolean();
private static final FastDateFormat FORMATTER = FastDateFormat.getInstance("yyyy-M-d"); public static void main(String[] args) {
Runnable runnable = () -> {
int count = 0;
while (!STOP.get()) {
try {
FORMATTER.parse("2023-7-15");
} catch (Exception e) {
e.printStackTrace();
if (++count > 3) {
STOP.set(true);
}
}
}
}; new Thread(runnable).start();
new Thread(runnable).start();
} }

执行上述代码,不会再观察到异常输出,因为 FastDateFormat 是线程安全的实现,支持多线程并发调用。

总结

无论使用哪种修复方案,都需要在修改后进行充分的测试,保证修复后不影响原有业务逻辑,如通过单元测试、流量回放等方式来保证本次修复的正确性。

思考

代码里使用 SimpleDateFormat 类的原因是因为日期使用了 Date 类,与 Date 相配套的 JDK 格式化类即 SimpleDateFormat 类,如果我们在处理日期时使用 JDK8 引入的 LocalDateTime 等不可变日期类,那么格式化将使用配套的线程安全的 DateTimeFormatter 类,从根源上规避掉对非线程安全类 SimpleDateFormat 类的使用。

作者:京东物流 刘建设 张九龙 田爽

来源:京东云开发者社区 自猿其说Tech 转载请注明来源

SimpleDateFormat 线程安全问题修复方案的更多相关文章

  1. SimpleDateFormat线程安全问题排查

    一. 问题现象 运营部门反馈使用小程序配置的拉新现金红包活动二维码,在扫码后跳转至404页面. 二. 原因排查 首先,检查扫码后的跳转链接地址不是对应二维码的实际URL,根据代码逻辑推测,可能是acc ...

  2. 解决SimpleDateFormat线程安全问题

    package com.tanlu.user.util; import java.text.DateFormat; import java.text.ParseException; import ja ...

  3. SimpleDateFormat线程安全问题

    今天线上出现了问题,从第三方获取的日期为 2019-12-12 11:11:11,通过SimpleDateFormat转换格式后,竟然出现完全不正常的日期数据,经百度,得知SimpleDateForm ...

  4. SimpleDateFormat类的线程安全问题和解决方案

    摘要:我们就一起看下在高并发下SimpleDateFormat类为何会出现安全问题,以及如何解决SimpleDateFormat类的安全问题. 本文分享自华为云社区<SimpleDateForm ...

  5. 关于 SimpleDateFormat 的非线程安全问题及其解决方案

    一直以来都是直接用SimpleDateFormat开发的,没想着考虑线程安全的问题,特记录下来(摘抄的): 1.问题: 先来看一段可能引起错误的代码: package test.date; impor ...

  6. SimpleDateFormat的线程安全问题

    做项目的时候查询的日期总是不对,花了很长时间才找到异常的根源,原来SimpleDateFormat是非线程安全的,当我把这个类放到多线程的环境下转换日期就会出现莫名奇妙的结果,这种异常找出来可真不容易 ...

  7. SimpleDateFormat 的线程安全问题与解决方式

    SimpleDateFormat 的线程安全问题 SimpleDateFormat 是一个以国别敏感的方式格式化和分析数据的详细类. 它同意格式化 (date -> text).语法分析 (te ...

  8. SimpleDateFormat使用和线程安全问题

    SimpleDateFormat 是一个以国别敏感的方式格式化和分析数据的具体类. 它允许格式化 (date -> text).语法分析 (text -> date)和标准化. Simpl ...

  9. SimpleDateFormat时间格式化存在线程安全问题

    想必大家对SimpleDateFormat并不陌生.SimpleDateFormat 是 Java 中一个非常常用的类,该类用来对日期字符串进行解析和格式化输出,但如果使用不小心会导致非常微妙和难以调 ...

  10. 转:浅谈SimpleDateFormat的线程安全问题

    转自:https://blog.csdn.net/weixin_38810239/article/details/79941964 在实际项目中,我们经常需要将日期在String和Date之间做转化, ...

随机推荐

  1. PostgreSQL一站式插件推荐 -- pg_enterprise_views

    近日发现PG官方插件列表中新收录了一款插件 pg_enterprise_views,因为官方已经数年未添新的插件了很是新奇,找了台设备测试过后果断上了生产,得空分享给大家. 该插件提供了数十张系统表及 ...

  2. Node.js出现‘Cannot find module init’ 解决方法

    1. 首先查看当前根目录是否有node_module文件夹,如果有,请删除 2. 输入 npm clean cache 3. 再次输入 node init -y 大功告成

  3. 非 root 用户手动编译安装 GCC

    我们知道,关于 GCC 在 CentOS 下通过 yum 安装默认版本号,CentOS 5 是 4.1.2:CentOS 6 是 4.4.7:CentOS 7 是 4.8.3.很多时候在编译安装软件都 ...

  4. 一分钟学一个 Linux 命令 - mkdir 和 touch

    前言 大家好,我是god23bin.欢迎来到<一分钟学一个 Linux 命令>系列,今天需要你花两分钟时间来学习下,因为今天要讲的是两个命令,mkdir 和 touch 命令.前一个命令是 ...

  5. Vue3从入门到精通(二)

    vue3 侦听器 在Vue3中,侦听器的使用方式与Vue2相同,可以使用watch选项或$watch方法来创建侦听器.不同之处在于,Vue3中取消了immediate选项,同时提供了新的选项和API. ...

  6. 【LeetCode专题#基本计算器】基本计算器I,图解中序表达式转逆波兰表达式,太难了

    基本计算器 https://leetcode.cn/problems/basic-calculator/?envType=list&envId=cKNEfNsF 给你一个字符串表达式 s ,请 ...

  7. Linux 线程传递参数

    1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <pthread.h> 4 #include <u ...

  8. Docker 中的 .NET 异常了怎么抓 Dump

    一:背景 1. 讲故事 有很多朋友跟我说,在 Windows 上看过你文章知道了怎么抓 Crash, CPU爆高,内存暴涨 等各种Dump,为什么你没有写在 Docker 中如何抓的相关文章呢?瞧不上 ...

  9. 【.Net/C#之ChatGPT开发系列】四、ChatGPT多KEY动态轮询,自动删除无效KEY

    ChatGPT是一种基于Token数量计费的语言模型,它可以生成高质量的文本.然而,每个新账号只有一个有限的初始配额,用完后就需要付费才能继续使用.为此,我们可能存在使用多KEY的情况,并在每个KEY ...

  10. AI技术在软件测试中的应用和实践

    随着人工智能(AI)技术的快速发展,它在各个领域都展现出了巨大的潜力和影响力.在软件测试领域,AI技术也越来越得到重视和应用.本文将探讨AI技术在软件测试中的应用和实践,重点关注chatGPT如何根据 ...