jdk- 线程不安全的 SimpleDateFormat

博客围绕SimpleDateFormat展开,先给出测试demo,接着分析其线程不安全的原因,从DateFormat和SimpleDateFormat的源码可知,format和parse方法对私有变量calendar的访问未同步加锁。最后提出三种实现线程安全的方法,如在方法内创建对象、同步加锁、使用ThreadLocal。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一 测试 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;
    }
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值