并发场景下,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是线程安全的