SimpleDateFormat类的线程不安全

文章详细解释了在并发环境下,SimpleDateFormat由于其内部使用的Calendar对象非线程安全导致的问题,并通过代码示例展示了这个问题。解决方案包括使用ThreadLocal存储每个线程的SimpleDateFormat副本或切换到线程安全的DateTimeFormatter。

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

并发场景下,SimpleDateFormat类是线程不安全的,有的小伙伴会有疑问:我们一直使用SimpleDateFormat类来解析和格式化时间,也没发现问题啊。那是因为并发量还没有达到出现问题。

模拟SimpleDateFormat类的线程安全问题

使用多个线程执行时间解析,即可出现该问题,简单写了个代码:

public class SimpleDateFormatTest {
    private static final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newCachedThreadPool();
        CountDownLatch countDownLatch = new CountDownLatch(100);
        for (int i = 0; i < 100; i++) {
            executorService.execute(()-> {
                try {
                    simpleDateFormat.parse("2023-04-04");
                } catch (Exception e) {
                    System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
                    System.exit(1);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        System.out.println("所有线程格式化日期成功");
    }
}

执行结果如下:

SimpleDateFormat为何线程不安全

查看SimpleDateFormat类源码,SimpleDateFormat继承DateFormat。

public class SimpleDateFormat extends 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;

这个Calendar对象既用于格式化也用于解析日期时间。查看parse方法。

public Date parse(String text, ParsePosition pos) {
    ......
    Date parsedDate;
    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();
            }
        }
    }
    ......
    return parsedDate;
}

最后的返回值是通过调用CalendarBuilder.establish()方法获得的,而这个方法的参数正好就是前面的Calendar对象。接下来再看看establish这个方法。

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;
            }
        }
    }
    ......
}

establish方法中先后调用了cal.clear()与cal.set(),也就是先清除cal对象中设置的值,再重新设置新的值。由于Calendar内部并没有线程安全机制,并且这两个操作也都不是原子性的,所以当多个线程同时操作一个SimpleDateFormat时就会引起cal的值混乱。

因此,SimpleDateFormat类不是线程安全的。

解决SimpleDateFormat类的线程安全问题

  • 将SimpleDateFormat类对象定义成局部变量

  • 加锁(synchronized或者ReentrantLock)

  • 使用ThreadLocal存储每个线程拥有的SimpleDateFormat对象副本,可以有效避免多线程问题

    private static final ThreadLocal<SimpleDateFormat> threadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
    ​
    threadLocal.get().parse("2023-04-04");
  • 使用DateTimeFormatter,DateTimeFormatter是线程安全的

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

傻蛋800

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值