Java Review - SimpleDateFormat线程不安全原因的源码分析及解决办法
文章目录
概述
SimpleDateFormat是Java提供的一个格式化和解析日期的工具类,在日常开发中经常会用到,但是由于它是线程不安全的,所以多线程共用一个SimpleDateFormat实例对日期进行解析或者格式化会导致程序出错。
这里来揭示它为何是线程不安全的,以及如何避免该问题。
复现问题
import java.text.ParseException;
import java.text.SimpleDateFormat;
/**
* @author 小工匠
* @version 1.0
* @description: TODO
* @date 2021/11/21 14:56
* @mark: show me the code , change the world
*/
public class SimpleDateFormatTest {
// 1 创建单例实例
private static SimpleDateFormat sdf = new SimpleDateFormat("YYYY-MM-dd HH:mm:ss");
public static void main(String[] args) {
// 2 开启多个线程,并且欧东
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(() -> {
try {
// 3 使用单例日期解析文本
System.out.println(sdf.parse("2021-11-19 15:15:00"));
} catch (ParseException e) {
e.printStackTrace();
}
});
thread.start();
}
}
}
代码(1)创建了SimpleDateFormat
的一个实例
代码(2)创建10个线程,每个线程都共用同一个sdf对象对文本日期进行解析。
多运行几次代码就会抛出java.lang.NumberFormatException
异常,增加线程的个数有利于复现该问题
Exception in thread "Thread-0" Exception in thread "Thread-2" Exception in thread "Thread-1" Exception in thread "Thread-6" Exception in thread "Thread-4" Exception in thread "Thread-8" Exception in thread "Thread-9" Exception in thread "Thread-5" Exception in thread "Thread-7" 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:2084)
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.artisan.bfzm.chapter11.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:26)
at java.lang.Thread.run(Thread.java:748)
java.lang.ArrayIndexOutOfBoundsException: 20
at java.text.DigitList.append(DigitList.java:151)
at java.text.DecimalFormat.subparse(DecimalFormat.java:2278)
at java.text.DecimalFormat.parse(DecimalFormat.java:2036)
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.artisan.bfzm.chapter11.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:26)
at java.lang.Thread.run(Thread.java:748)
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:2089)
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.artisan.bfzm.chapter11.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:26)
at java.lang.Thread.run(Thread.java:748)
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:2084)
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.artisan.bfzm.chapter11.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:26)
at java.lang.Thread.run(Thread.java:748)
java.lang.NumberFormatException: empty String
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1842)
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:2089)
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.artisan.bfzm.chapter11.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:26)
at java.lang.Thread.run(Thread.java:748)
java.lang.NumberFormatException: empty String
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1842)
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:2089)
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.artisan.bfzm.chapter11.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:26)
at java.lang.Thread.run(Thread.java:748)
java.lang.ArrayIndexOutOfBoundsException: 19
at java.text.DigitList.append(DigitList.java:151)
at java.text.DecimalFormat.subparse(DecimalFormat.java:2278)
at java.text.DecimalFormat.parse(DecimalFormat.java:2036)
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.artisan.bfzm.chapter11.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:26)
at java.lang.Thread.run(Thread.java:748)
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:2089)
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
.....
源码分析
为了便于分析,首先来看SimpleDateFormat的类图结构
可以看到,每个SimpleDateFormat实例里面都有一个Calendar对象,后面我们就会知道,SimpleDateFormat之所以是线程不安全的,就是因为Calendar是线程不安全的。
Calendar之所以是线程不安全的,是因为其中存放日期数据的变量都是线程不安全的,比如fields、time等。
下面从代码层面来看下parse方法做了什么事情。
public Date parse(String source) throws ParseException
{
ParsePosition pos = new ParsePosition(0);
Date result = parse(source, pos);
if (pos.index == 0)
throw new ParseException("Unparseable date: "" + source + """ ,
pos.errorIndex);
return result;
}
@Override
public Date parse(String text, ParsePosition pos)
{
.......
// 1 解析日期字符串,将解析好的数据放入CalendarBuilder对象中
CalendarBuilder calb = new CalendarBuilder();
.......
.......
Date parsedDate;
try {
// 2 使用calb中解析好的日期数据设置calendar
parsedDate = calb.establish(calendar).getTime();
}
catch (IllegalArgumentException e) {
.......
.......
return null;
}
.......
.......
return parsedDate;
}
- 代码(1)的主要作用是解析日期字符串并把解析好的数据放入 CalendarBuilder的实例calb中。CalendarBuilder是一个建造者模式,用来存放后面需要的数据。
- 代码(2)使用calb中解析好的日期数据设置calendar。
calb.establish的代码如下
Calendar establish(Calendar cal) {
.....
// 3 重置日期对象cal的属性值
cal.clear();
// 4 使用calb中的属性设置cal
// Set the fields from the min stamp to the max stamp so that
// the field resolution works in the Calendar.
for (int stamp = MINIMUM_USER_STAMP; stamp < nextStamp; stamp++) {
for (int index = 0; index <= maxFieldIndex; index++) {
if (field[index] == stamp) {
cal.set(index, field[MAX_FIELD + index]);
break;
}
}
}
if (weekDate) {
int weekOfYear = isSet(WEEK_OF_YEAR) ? field[MAX_FIELD + WEEK_OF_YEAR] : 1;
int dayOfWeek = isSet(DAY_OF_WEEK) ?
field[MAX_FIELD + DAY_OF_WEEK] : cal.getFirstDayOfWeek();
if (!isValidDayOfWeek(dayOfWeek) && cal.isLenient()) {
if (dayOfWeek >= 8) {
dayOfWeek--;
weekOfYear += dayOfWeek / 7;
dayOfWeek = (dayOfWeek % 7) + 1;
} else {
while (dayOfWeek <= 0) {
dayOfWeek += 7;
weekOfYear--;
}
}
dayOfWeek = toCalendarDayOfWeek(dayOfWeek);
}
cal.setWeekDate(field[MAX_FIELD + WEEK_YEAR], weekOfYear, dayOfWeek);
}
// 5 返回cal
return cal;
}
- 代码(3)重置Calendar对象里面的属性值,如下所示。
public final void clear()
{
for (int i = 0; i < fields.length; ) {
stamp[i] = fields[i] = 0; // UNSET == 0
isSet[i++] = false;
}
areAllFieldsSet = areFieldsSet = false;
isTimeSet = false;
}
- 代码(4)使用calb中解析好的日期数据设置cal对象。
- 代码(5) 返回设置好的cal对象。
从以上代码可以看出,代码(3)、代码(4)和代码(5)并不是原子性操作。
当多个线程调用parse 方法时,比如线程A执行了代码(3)和代码(4),也就是设置好了cal对象,但是在执行代码(5)之前,线程B执行了代码(3),清空了cal对象。
由于多个线程使用的是一个cal对象,所以线程A执行代码(5)返回的可能就是被线程B清空的对象,当然也有可能线程B执行了代码(4),设置被线程A修改的cal对象,从而导致程序出现错误。
How to Fix ?
每次使用时new一个SimpleDateFormat的实例
每次使用时new一个SimpleDateFormat的实例,这样可以保证每个实例使用自己的Calendar实例,但是每次使用都需要new一个对象,并且使用后由于没有其他引用,又需要回收,开销会很大。
加锁
出错的根本原因是因为多线程下代码(3)、代码(4)和代码(5)三个步骤不是一个原子性操作,那么容易想到的是对它们进行同步,让代码(3)、代码(4)和代码(5)成为原子性操作。
可以使用synchronized进行同步,具体如下。
进行同步意味着多个线程要竞争锁,在高并发场景下这会导致系统响应性能下降。
使用ThreadLocal
使用ThreadLocal,这样每个线程只需要使用一个SimpleDateFormat实例,这相比第一种方式大大节省了对象的创建销毁开销,并且不需要使多个线程同步。
使用ThreadLocal方式的代码如下
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
/**
* @author 小工匠
* @version 1.0
* @description: TODO
* @date 2021/11/21 14:56
* @mark: show me the code , change the world
*/
public class SimpleDateFormatTest {
// 1 创建ThreadLocal实例
static ThreadLocal<DateFormat> threadLocal= ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
// static ThreadLocal threadLocal2 = new ThreadLocal(){
// @Override
// protected DateFormat initialValue() {
// return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// }
// };
public static void main(String[] args) {
// 2 开启多个线程,并且欧东
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(() -> {
try {
// 3 使用单例日期解析文本
System.out.println(threadLocal.get().parse("2021-11-19 15:15:00"));
} catch (ParseException e) {
e.printStackTrace();
}finally {
// 4 使用完毕,一定要remove
threadLocal.remove();
}
});
thread.start();
}
}
}
- 代码(1)创建了一个线程安全的SimpleDateFormat实例
- 代码(3)首先使用get()方法获取当前线程下SimpleDateFormat的实例。在第一次调用ThreadLocal的get()方法时,会触发其initialValue方法创建当前线程所需要的SimpleDateFormat对象。
- 另外需要注意的是,在代码(4)中,使用完线程变量后,要进行清理,以避免内存泄漏。
换API - JodaTime or JDK1.8的时间类
Java 8的日期和时间类包含LocalDate、LocalTime、Instant、Duration以及Period,这些类都包含在java.time包中.
新的日期API中提供了一个DateTimeFormatter
类用于处理日期格式化操作,它被包含在java.time.format包中,Java 8的日期类有一个format()
方法用于将日期格式化为字符串,该方法接收一个DateTimeFormatter
类型参数.
public static void main(String[] args) {
// 2 开启多个线程,
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(() -> {
// 3 使用单例日期解析文本
System.out.println(LocalDateTime.parse("2021-11-19 15:15:00", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
});
thread.start();
}
}
小结
我们这里简单介绍SimpleDateFormat的原理解释了为何SimpleDateFormat是线程不安全的,应该避免在多线程下使用SimpleDateFormat的单个实例。
相关文章
- 金融服务领域的大数据:即时分析
- 影响大数据、机器学习和人工智能未来发展的8个因素
- 从0开始构建一个属于你自己的PHP框架
- 如何将Hadoop集成到工作流程中?这6个优秀实践必看
- SEO公司使用大数据优化其模型的5种方法
- 关于Web Workers你需要了解的七件事
- 深入理解HTTPS原理、过程与实践
- 增强分析:数据和分析的未来
- PHP协程实现过程详解
- AI专家:大数据知识图谱——实战经验总结
- 关于PHP的错误机制总结
- 利用数据分析量化协同过滤算法的两大常见难题
- 怎么做大数据工作流调度系统?大厂架构师一语点破!
- 2019大数据处理必备的十大工具,从Linux到架构师必修
- OpenCV中的KMeans算法介绍与应用
- 教大家如果搭建一套phpstorm+wamp+xdebug调试PHP的环境
- CentOS下三种PHP拓展安装方法
- Go语言HTTP Server源码分析
- Go语言HTTP Server源码分析
- 2017年4月编程语言排行榜:Hack首次进入前五十