线程不安全的SimpleDateFormat

日期:2018-09-16       浏览:563

一 测试 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;
    }
}
扫码关注有惊喜

(转载本站文章请注明作者和出处 qbian)

暂无评论

Copyright 2016 qbian. All Rights Reserved.

文章目录