前言
最近在查资料的时候,偶然看到了SimpleDateFormat
不是线程安全的类的相关资料,说实话,一开始看的时候还是惊讶了一把的,从来没想过这个类居然不是线程安全的。今天就来看看这个类的线程安全问题。
SimpleDateFormat线程安全分析
先看一个很简单的日期处理工具类
public class DateUtil {
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static String formatDate(Date date) throws ParseException {
return sdf.format(date);
}
public static Date parse(String strDate) throws ParseException {
return sdf.parse(strDate);
}
}
看上去没有任何问题,我们测试一下。
package com.wangcc.springbootexample.dateformat;
import java.text.ParseException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import com.wangcc.springbootexample.utils.DateUtil;
/**
* @ClassName: DateFormatTest
* @Description: TODO
* @author BryantCong
* @date 2018年12月23日 下午11:37:13
*
*/
public class DateFormatTest {
public static void main(String[] args) throws InterruptedException {
ExecutorService es = Executors.newFixedThreadPool(3);
for (int i = 0; i < 30; i++) {
es.submit(() -> {
try {
System.out.println(Thread.currentThread().getName() + ":" + DateUtil.parse("2018-12-23 06:02:20"));
} catch (ParseException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
});
}
Thread.sleep(2000L);
es.shutdown();
if (es.awaitTermination(10, TimeUnit.SECONDS)) {
System.out.println("线程池已关闭");
} else {
System.out.println("线程池未正常关闭");
}
}
}
pool-1-thread-1:Sat Apr 12 18:22:00 CST 2228
pool-1-thread-2:Sun Dec 23 06:02:20 CST 2018
pool-1-thread-3:Sat Apr 12 18:22:00 CST 2228
pool-1-thread-3:Sun Dec 23 06:02:20 CST 2018
pool-1-thread-1:Sun Dec 23 06:02:20 CST 2018
pool-1-thread-3:Sat Dec 23 06:02:20 CST 2017
pool-1-thread-2:Sun Dec 23 06:20:20 CST 2018
pool-1-thread-3:Sun Dec 23 06:02:20 CST 2018
pool-1-thread-1:Sun Dec 23 06:02:20 CST 2018
pool-1-thread-1:Wed Dec 23 06:02:20 CST 223180618
pool-1-thread-1:Sun Dec 23 06:02:20 CST 2018
pool-1-thread-2:Sun Dec 23 06:02:20 CST 2018
pool-1-thread-1:Sat Dec 23 06:02:20 CST 2017
pool-1-thread-2:Sun Oct 08 19:00:43 CST 2023
pool-1-thread-1:Sun Feb 08 06:02:20 CST 42026
pool-1-thread-3:Sun Feb 08 06:02:20 CST 1
线程池已关闭
发现并发的时候会有问题,并不是每次输出的时间都是我们预期的,而且大多都不是我们预期的时间。我们在设计日期工具类的时候,复用了SimpleDateFormat
对象,而且我们想当然的以为调用他的parse()
方法和format()
方法是线程安全的,其实,真的只是我们相当然而已,这两个方法都不是线程安全的,正因为不是线程安全的,所以这两个方法才不是静态方法,否则这两个方法直接是静态方法就好了(要保证线程安全,代表方法不会改变该类的状态)。
在SimpleDateFormat转换日期是通过Calendar对象来操作的,SimpleDateFormat继承DateFormat类,DateFormat类中维护一个Calendar对象
public abstract class DateFormat extends Format {
/**
* The {@link Calendar} instance used for calculating the date-time fields
* and the instant of time. This field is used for both formatting and
* parsing.
*
* <p>Subclasses should initialize this field to a {@link Calendar}
* appropriate for the {@link Locale} associated with this
* <code>DateFormat</code>.
* @serial
*/
protected Calendar calendar;
//DateFormat
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;
}
//SimpleDateFormat
@Override
public Date parse(String text, ParsePosition pos){
//省略大段代码
try {
parsedDate = calb.establish(calendar).getTime();
// If the year value is ambiguous,
// then the two-digit year == the default start year
if (ambiguousYear[0]) {
if (parsedDate.before(defaultCenturyStart)) {
parsedDate = calb.addYear(100).establish(calendar).getTime();
}
}
}
//省略大段代码
}
在parse方法的最后,会调用CalendarBuilder的establish方法,入参就是SimpleDateFormat维护的Calendar实例,在establish方法中会调用calendar的clear方法,如下:
Calendar establish(Calendar cal) {
boolean weekDate = isSet(WEEK_YEAR)
&& field[WEEK_YEAR] > field[YEAR];
if (weekDate && !cal.isWeekDateSupported()) {
// Use YEAR instead
if (!isSet(YEAR)) {
set(YEAR, field[MAX_FIELD + WEEK_YEAR]);
}
weekDate = false;
}
//导致线程安全问题
cal.clear();
// 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;
}
}
}
//省略大段代码
}
可知SimpleDateFormat维护的用parse方法计算日期-时间的calendar被清空了,如果此时线程A将calendar清空且没有设置新值,线程B也进入parse方法用到了SimpleDateFormat对象中的calendar对象,会产生线程安全问题。接着的for循环设置calendar的属性时,也会出现线程安全问题,所以我们会看到输出的时间在并发情况下会变得各种各样。
解决方法:
public class ConcurrentDateUtil {
private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
public static Date parse(String dateStr) throws ParseException {
return threadLocal.get().parse(dateStr);
}
public static String format(Date date) {
return threadLocal.get().format(date);
}
}
每个线程分配一个SimpleDateFormat
对象,这样就不会出现线程安全问题。
问题:format方法会不会也有线程安全问题?
ExecutorService es = Executors.newFixedThreadPool(3);
for (int i = 0; i < 30; i++) {
es.submit(() -> {
try {
System.out.println(Thread.currentThread().getName() + ":" + DateUtil.formatDate(new Date()));
} catch (ParseException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
});
}
Thread.sleep(2000L);
es.shutdown();
if (es.awaitTermination(10, TimeUnit.SECONDS)) {
System.out.println("线程池已关闭");
} else {
System.out.println("线程池未正常关闭");
}
将上面的测试方法换成format
输出如下
pool-1-thread-2:2018-12-24 14:48:43
pool-1-thread-2:2018-12-24 14:48:43
pool-1-thread-3:2018-12-24 14:48:43
pool-1-thread-2:2018-12-24 14:48:43
pool-1-thread-2:2018-12-24 14:48:43
pool-1-thread-1:2018-12-24 14:48:43
pool-1-thread-2:2018-12-24 14:48:43
pool-1-thread-3:2018-12-24 14:48:43
pool-1-thread-2:2018-12-24 14:48:43
pool-1-thread-2:2018-12-24 14:48:43
pool-1-thread-2:2018-12-24 14:48:43
pool-1-thread-2:2018-12-24 14:48:43
pool-1-thread-2:2018-12-24 14:48:43
pool-1-thread-1:2018-12-24 14:48:43
pool-1-thread-2:2018-12-24 14:48:43
pool-1-thread-3:2018-12-24 14:48:43
pool-1-thread-2:2018-12-24 14:48:43
pool-1-thread-1:2018-12-24 14:48:43
pool-1-thread-2:2018-12-24 14:48:43
pool-1-thread-2:2018-12-24 14:48:43
pool-1-thread-3:2018-12-24 14:48:43
pool-1-thread-3:2018-12-24 14:48:43
pool-1-thread-2:2018-12-24 14:48:43
pool-1-thread-1:2018-12-24 14:48:43
pool-1-thread-2:2018-12-24 14:48:43
pool-1-thread-3:2018-12-24 14:48:43
pool-1-thread-2:2018-12-24 14:48:43
pool-1-thread-1:2018-12-24 14:48:43
pool-1-thread-2:2018-12-24 14:48:43
pool-1-thread-3:2018-12-24 14:48:43
运行多次的结果都是一样的,看不出有任何异常。
我在网上看到有些人说format方法是线程安全的,因为多次执行这个测试都没有发现任何异常。
其实,format方法与parse方法一样,都是非线程安全的。
我们看源码
//SimpleDateFormat
private StringBuffer format(Date date, StringBuffer toAppendTo,
FieldDelegate delegate) {
// Convert input date to time field list
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:
subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
break;
}
}
return toAppendTo;
}
很明显calendar.setTime(date);
这一行代码是肯定会导致线程不安全的。
你设置好的时间随时有可能被另一个线程的date覆盖,导致最后的值与期望值是不一致。而上面的测试方法为什么不会报错,很简单,就算被另外的线程覆盖,也还是原来的值(毫秒级的改变,我们无法察觉到)。所以我们不能用上述的测试方法来测试。
正确的测试方法
ExecutorService es = Executors.newFixedThreadPool(5);
for (int i = 0; i < 50; i++) {
es.submit(() -> {
try {
int random = new Random().nextInt();
Date date = new Date();
if (random % 2 == 0) {
date = ConcurrentDateUtil.parse("2018-02-19 06:02:20");
}
if (!ConcurrentDateUtil.format(date).equals(DateUtil.formatDate(date))) {
System.out.println("证明非线程安全");
} else {
System.out.println("====");
}
// System.out.println(Thread.currentThread().getName() + "同步:" + date +
// "format:" + DateUtil.formatDate(date));
} catch (ParseException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
});
}
es.shutdown();
if (es.awaitTermination(10, TimeUnit.SECONDS)) {
System.out.println("线程池已关闭");
} else {
System.out.println("线程池未正常关闭");
}
输出如下
证明非线程安全
====
====
====
证明非线程安全
====
====
====
====
====
====
证明在多线程的情况下,会出现date覆盖的情况,非线程安全。
思考
说实话,一直以来,我都以为SimpleDateFormat这个类的所有操作都是线程安全的,像这种JDK提供的工具类,真的从来没想过他存在线程安全问题,但是他却偏偏就是个非线程安全的类。作为一个程序员,一定不能想当然,实践是检验真理的唯一标准。
另一方面,今天分析的这个问题也告诉了我们编写代码的需要注意的一些地方。
- 我们最初提供的
DateUtil
类,他并不是一个无状态的类,这是很可怕的,我们绝对不能允许我们的Util类是一个有状态的类,这是非常非常可怕的,在我们对一些全局变量或者实例属性进行操作的时候,一定要考虑到对他们的操作会不会带来线程安全的问题。 - 对于util这种公用类,我们一定要对其做并发测试!一定要做并发测试!一定要做并发测试!保证代码的正确性。
- 我们在设计一个类的时候,尽量把他设计为无状态的。在Spring中,默认的Bean模式也是单例的,这要求我们的Bean要是无状态的,否则就会存在线程安全问题。如果是有状态的Bean,那么Bean模式必须要改为原型模式。