SimpleDateFormat 线程安全问题修复方案
问题介绍
在日常的开发过程中,我们不可避免地会使用到 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<>();
// ...
}
在 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 of
SimpleDateFormat. 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 线程安全问题修复方案的更多相关文章
- SimpleDateFormat线程安全问题排查
一. 问题现象 运营部门反馈使用小程序配置的拉新现金红包活动二维码,在扫码后跳转至404页面. 二. 原因排查 首先,检查扫码后的跳转链接地址不是对应二维码的实际URL,根据代码逻辑推测,可能是acc ...
- 解决SimpleDateFormat线程安全问题
package com.tanlu.user.util; import java.text.DateFormat; import java.text.ParseException; import ja ...
- SimpleDateFormat线程安全问题
今天线上出现了问题,从第三方获取的日期为 2019-12-12 11:11:11,通过SimpleDateFormat转换格式后,竟然出现完全不正常的日期数据,经百度,得知SimpleDateForm ...
- SimpleDateFormat类的线程安全问题和解决方案
摘要:我们就一起看下在高并发下SimpleDateFormat类为何会出现安全问题,以及如何解决SimpleDateFormat类的安全问题. 本文分享自华为云社区<SimpleDateForm ...
- 关于 SimpleDateFormat 的非线程安全问题及其解决方案
一直以来都是直接用SimpleDateFormat开发的,没想着考虑线程安全的问题,特记录下来(摘抄的): 1.问题: 先来看一段可能引起错误的代码: package test.date; impor ...
- SimpleDateFormat的线程安全问题
做项目的时候查询的日期总是不对,花了很长时间才找到异常的根源,原来SimpleDateFormat是非线程安全的,当我把这个类放到多线程的环境下转换日期就会出现莫名奇妙的结果,这种异常找出来可真不容易 ...
- SimpleDateFormat 的线程安全问题与解决方式
SimpleDateFormat 的线程安全问题 SimpleDateFormat 是一个以国别敏感的方式格式化和分析数据的详细类. 它同意格式化 (date -> text).语法分析 (te ...
- SimpleDateFormat使用和线程安全问题
SimpleDateFormat 是一个以国别敏感的方式格式化和分析数据的具体类. 它允许格式化 (date -> text).语法分析 (text -> date)和标准化. Simpl ...
- SimpleDateFormat时间格式化存在线程安全问题
想必大家对SimpleDateFormat并不陌生.SimpleDateFormat 是 Java 中一个非常常用的类,该类用来对日期字符串进行解析和格式化输出,但如果使用不小心会导致非常微妙和难以调 ...
- 转:浅谈SimpleDateFormat的线程安全问题
转自:https://blog.csdn.net/weixin_38810239/article/details/79941964 在实际项目中,我们经常需要将日期在String和Date之间做转化, ...
随机推荐
- 2020-10-29:使用redis实现分布式限流组件,要求高并发场景同一IP一分钟内只能访问100次,超过限制返回异常,写出实现思路或伪代码均可。
福哥答案2020-10-29: 简单回答:固定窗口:string.key存ip,value存次数.滑动窗口:list.key存ip,value=list,存每次访问的时间. 中级回答:固定窗口:用re ...
- golang调用sdl2,键盘和鼠标事件
golang调用sdl2,键盘和鼠标事件 win10 x64下测试成功,其他操作系统下不保证成功. 采用的是syscall方式,不是cgo方式. 见地址 代码用golang编写.代码如下: packa ...
- 深入理解 python 虚拟机:破解核心魔法——反序列化 pyc 文件
深入理解 python 虚拟机:破解核心魔法--反序列化 pyc 文件 在前面的文章当中我们详细的对于 pyc 文件的结构进行了分析,pyc 文件主要有下面的四个部分组成:魔术. Bite Filed ...
- 这可能是最全面的Java面试八股文了
Java的特点 Java是一门面向对象的编程语言.面向对象和面向过程的区别参考下一个问题. Java具有平台独立性和移植性. Java有一句口号:Write once, run anywhere,一次 ...
- 有JSDoc还需要TypeScript吗
这听起来是不是很耳熟:你想写一个小型脚本,不管是为页面.命令行工具,还是其他什么类型.你从JavaScript开始,直到你想起写代码时没有类型是多么痛苦.所以你把文件从.js重命名为.ts.然后意识到 ...
- Python Excel 操作 | xlrd+xlwt 模块笔记
Python 的pandas模块使用xlrd作为读取 excel 文件的默认引擎.但是,xlrd在其最新版本(从 2.0.1 版本开始)中删除了对 xls 文件以外的任何文件的支持. xlsx fil ...
- Kafka关键原理
日志分段切分条件 日志分段文件切分包含以下4个条件,满足其一即可: 当前日志分段文件的大小超过了broker端参数 log.segment.bytes 配置的值.log.segment.bytes参数 ...
- 给你的 Discord 接入一个既能联网又能画画的 ChatGPT
如果有这样一款 Discord 机器人,它既能访问互联网,又能绘画,还能给 YouTube 视频提供摘要.最重要的是,它是完全免费的,不需要提供 OpenAI 的 API Key,我就问你香不香? 现 ...
- 程序包xx不存在解决方案:java: 程序包org.aspectj.lang.annotation
java: 程序包org.aspectj.lang.annotation不存在 #我自己已经配置好pom.xml文件,但是一直报错这个 aspectj 不存在 下面是我的pom.xml配置 可以看出来 ...
- 2023-06-21:redis中什么是BigKey?该如何解决?
2023-06-21:redis中什么是BigKey?该如何解决? 答案2023-06-21: 什么是bigkey bigkey是指存储在Key-Value数据库中的键对应的值所占用的内存空间较大.举 ...