前言
提示:在SimpleDateFormat类中官方其实已经标注了,该类非线程安全:
* 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.
* 日期格式不同步。建议为每个线程创建单独的格式实例。如果多个线程同时访问一种格式,则必须对其进行 同步。
public class SimpleDateFormat extends DateFormat {
}
一、SimpleDateFormat 是什么?
SimpleDateFormat 是java中非常常用的类,可以对日期字符串进行解析,但是如果写法有问题,将会出现线程安全问题,因为SimpleDateFormat 类不是线程安全的,在多线程环境下会出现数据脏读甚至报错的情况。
二、示例
1.自定义DateUtils.java
public class DateUtils {
private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
private static ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
return sdf;
}
};
// 错误示范
public static Date parseString(String datetime) throws Exception {
return simpleDateFormat.parse(datetime);
}
// 通过同步synchronized解决该问题,缺陷影响执行速率
public static Date parseString1(String datetime) throws Exception {
synchronized (simpleDateFormat) {
return simpleDateFormat.parse(datetime);
}
}
// 每次都创建新的SimpleDateFormat实例,缺陷:开销大
public static Date parseString2(String datetime) throws Exception {
return new SimpleDateFormat("yyyy-MM-dd").parse(datetime);
}
// 通过ThreadLocal解决,最佳方式
public static Date parseString3(String datetime) throws Exception {
return threadLocal.get().parse(datetime);
}
// 第三方工具:hutool类
public static Date parseString4(String datetime) throws Exception {
DateTime result = DateUtil.parse(datetime, "yyyy-MM-dd");
return result;
}
}
2.多线程测试异常情况
public class Test1 {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(100);
for (int i = 0; i < 50; i++) {
executorService.execute(() -> {
try {
System.out.println(DateUtils.parseString("2020-05-01"));
} catch (Exception e) {
e.printStackTrace();
}
});
}
executorService.shutdown();
try {
executorService.awaitTermination(100, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
报错:类型异常、数据脏读等问题!
三、非线程安全问题分析
提示:一般为了方便使用把SimpleDateFormat定义成一个static变量,而静态变量是线程共享的(即核心Calendar也是共享,无论是format还是parse方法),所以线程a就会访问到线程b的时间,从而导致时间的误差。
从上面代码看1,2,3,4,5操作不是原子性,当多个线程调用parse方法适合,比如A执行了(3)(4),也就是设置了cal对象,在执行代码(5) 前线程B 执行了代码(3) 清空了cal对象,由于多个线程使用的是一个Calendar对象,所以线程A执行(5) 的时候返回的是被线程B清空后的对象。
public Date parse(String text, ParsePosition pos){
//(1)解析日期字符串text放入CalendarBuilder的实例calb中,
.....
Date parsedDate;
try {
//(2)使用calb中把日期数据设置到calendar
parsedDate = calb.establish(calendar).getTime();
...
}
return parsedDate;
}
class CalendarBuilder {
Calendar establish(Calendar cal) {
...
//(3)重置日期对象cal的属性值
cal.clear();
//(4) 使用calb中中属性设置cal
...
//(5)返回设置好的cal对象
return cal;
}
}
问题原因与parse方法一样,由于多线程公用一个calendar对象。
public class SimpleDateFormat extends DateFormat {
private StringBuffer format(Date date, StringBuffer toAppendTo,
FieldDelegate delegate) {
calendar.setTime(date);
boolean useDateFormatSymbols = useDateFormatSymbols();
for (int i = 0; i < compiledPattern.length; ) {
int tag = compiledPattern[i] >>> 8;
int count = compiledPattern[i++] & 0xff;
if (count == 255) {
count = compiledPattern[i++] << 16;
count |= compiledPattern[i++];
}
switch (tag) {
case TAG_QUOTE_ASCII_CHAR:
toAppendTo.append((char)count);
break;
case TAG_QUOTE_CHARS:
toAppendTo.append(compiledPattern, i, count);
i += count;
break;
default:
//核心:将calendar进行解析获取的字符串拼接到toAppendTo中
subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
break;
}
}
return toAppendTo;
}
}
四、通过以下方式可解决
提示:以下4中方法,在DateUtils.java已标注优点和缺点。
- 通过synchronized同步参考parseString1方法)
- 每个线程执行时都创建新的SimpleDateFormat实例(参考parseString2方法)
- 通过ThreadLocal解决,最佳方式(参考parseString3方法)
- 通过第三方工具,测试中我以hutool为例(参考parseString4方法)