一 测试 demo
我们先来写一个测试 demo
public class SimpleDateFormatTest {
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
static class ParseDate implements Runnable {
int i = 0;
ParseDate(int i) {
this.i = i;
}
@Override
public void run() {
try {
Date date = sdf.parse("2018-08-28 10:10:" + i % 60);
System.out.println(i + " ---- " + date);
} catch (ParseException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws ParseException {
ExecutorService es = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; ++ i) {
es.execute(new ParseDate(i));
}
}
}
测试结果如下
50 ---- Tue Aug 28 10:10:50 CST 2018
51 ---- Tue Aug 28 10:10:51 CST 2018
52 ---- Tue Aug 28 10:10:00 CST 2018
53 ---- Tue Aug 28 10:10:53 CST 2018
54 ---- Tue Aug 28 10:10:54 CST 2018
Exception in thread "pool-1-thread-6" Exception in thread "pool-1-thread-2" Exception in thread "pool-1-thread-20" 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:2051)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
二 为什么线程不安全
让我们看一看 SimpleDateFormat 的部分源码
public class SimpleDateFormat extends DateFormat {}
SimpleDateFormat 继承了 DateFormat,我们再来看一下 DateFormat 部分源码
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 源码可以看到,在 DateFormat 内部定义了一个私有变量 calendar,注释已经说明了用途,用于格式化或解析时间和日期。
我们再看一看 DateFormat 的实现类 SimpleDateFormat 的 format 方法源码
@Override
public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition pos){
pos.beginIndex = pos.endIndex = 0;
return format(date, toAppendTo, pos.getFieldDelegate());
}
// Called from Format after creating a FieldDelegate
private StringBuffer format(Date date, StringBuffer toAppendTo, FieldDelegate delegate) {
// Convert input date to time field list
calendar.setTime(date);
// ......
return toAppendTo;
}
从上面可以看到在 format 方法内对私有变量 calendar 的访问并没有做到线程安全(同步加锁),也就是说在多线程并发的情况下调用同一个 SimpleDateFormat 对象的 format 方法时,是一个不安全的操作。
所以说 SimpleDateFormat 的 format 方法并不是线程安全的。
让我们再看一下 SimpleDateFormat 的 parse 方法源码
@Override
public Date parse(String text, ParsePosition pos){
// ......
// At this point the fields of Calendar have been set. Calendar
// will fill in default values for missing fields when the time
// is computed.
Date parsedDate;
try {
parsedDate = calb.establish(calendar).getTime();
// ......
}
// ......
return parsedDate;
}
通过注释部分我们可以了解到,CalendarBuilder 对象 calb 在执行 calb.establish(calendar).getTime(); 获取时间时已经设置了值或分配了缺省的默认值,这一步也没有对 calendar 对象的访问操作做到线程安全(同步加锁),同 format 方法一样,parse 方法也不是线程安全的。
三 如何做到线程安全
2.1 在方法内 new SimpleDateFormat 对象,这样可以确保每个线程内部都有自己的 SimpleDateFormat 对象。
public Date parse(String dateStr) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
try {
return simpleDateFormat.parse(dateStr);
} catch (ParseException e) {
e.printStackTrace();
return null;
}
}
2.2 定义一个全局的 SimpleDateFormat 对象,在访问的时候进行同步加锁。
private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
private final Lock lock = new ReentrantLock();
public Date parse(String dateStr) {
try {
// 添加同步锁
lock.lockInterruptibly();
try {
return dateFormat.parse(dateStr);
} finally {
// 释放同步锁
lock.unlock();
}
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
2.3 使用 ThreadLocal 为每个线程都创建一个线程独享 SimpleDateFormat 变量。
private static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<>();
public Date parse(String dateStr) {
try {
if (dateFormatThreadLocal.get() == null) {
dateFormatThreadLocal.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}
return dateFormatThreadLocal.get().parse(dateStr);
} catch (ParseException e) {
e.printStackTrace();
return null;
}
}