SimpleDateFormat是Java中的日期转换类,面试中也会经常问到为什么有线程安全问题,最近真的在运维项目中遇到这个问题,可能之前并发量很少没有发生,最近频繁出现才发现系统中很多处使用都有问题,简单说明下。
测试代码:
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SimpleDateFormatTest {
//SimpleDateFormat对象
private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
executorService.execute(() -> {
try {
try {
simpleDateFormat.parse("2022-02-02");
System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期成功!!!");
} catch (ParseException e) {
System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败;ParseException");
e.printStackTrace();
System.exit(1);
} catch (NumberFormatException e) {
System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败;NumberFormatException");
e.printStackTrace();
System.exit(1);
}
} catch (Exception e) {
e.printStackTrace();
}
});
}
executorService.shutdown();
}
}
线程:pool-1-thread-1 格式化日期失败;NumberFormatException
线程:pool-1-thread-10 格式化日期成功!!!
线程:pool-1-thread-6 格式化日期失败;NumberFormatException
线程:pool-1-thread-2 格式化日期失败;NumberFormatException
线程:pool-1-thread-8 格式化日期失败;NumberFormatException
线程:pool-1-thread-5 格式化日期失败;NumberFormatException
线程:pool-1-thread-3 格式化日期失败;NumberFormatException
线程:pool-1-thread-4 格式化日期失败;NumberFormatException
线程:pool-1-thread-9 格式化日期成功!!!
线程:pool-1-thread-7 格式化日期成功!!!
java.lang.NumberFormatException: For input string: "20222022022222022E42022022222022E4"
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2043)
at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
at java.lang.Double.parseDouble(Double.java:538)
at java.text.DigitList.getDouble(DigitList.java:169)
at java.text.DecimalFormat.parse(DecimalFormat.java:2089)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at com.danjiu.runtime.thread.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:22)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
at java.lang.Double.parseDouble(Double.java:538)
at java.text.DigitList.getDouble(DigitList.java:169)
at java.text.DecimalFormat.parse(DecimalFormat.java:2089)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
Parse最终的调用方法在CalendarBuilder.establish()方法中,先后调用了cal.clear()与cal.set(),先清除cal对象中设置的值,再重新设置新的值。由于Calendar内部并没有线程安全机制,并且这两个操作也都不是原子性的,所以当多个线程同时操作一个SimpleDateFormat时就会引起cal的值混乱。简单说SimpleDateFormat类不是线程安全的根本原因,DateFormat类中的Calendar对象被多线程共享,而Calendar对象本身不支持线程安全。
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;
}
}
}
if (weekDate) {
int weekOfYear = isSet(WEEK_OF_YEAR) ? field[MAX_FIELD + WEEK_OF_YEAR] : 1;
int dayOfWeek = isSet(DAY_OF_WEEK) ?
field[MAX_FIELD + DAY_OF_WEEK] : cal.getFirstDayOfWeek();
if (!isValidDayOfWeek(dayOfWeek) && cal.isLenient()) {
if (dayOfWeek >= 8) {
dayOfWeek--;
weekOfYear += dayOfWeek / 7;
dayOfWeek = (dayOfWeek % 7) + 1;
} else {
while (dayOfWeek <= 0) {
dayOfWeek += 7;
weekOfYear--;
}
}
dayOfWeek = toCalendarDayOfWeek(dayOfWeek);
}
cal.setWeekDate(field[MAX_FIELD + WEEK_YEAR], weekOfYear, dayOfWeek);
}
return cal;
}
解决办法:
1.通过ThreadLocal解决SimpleDateFormat类的线程安全问题
private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>(){
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
2.使用joda-time的DateTimeFormatter类解决线程安全问题
private static DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("yyyy-MM-dd");
DateTime.parse("2020-01-01", dateTimeFormatter).toDate();
3.其它方法
局部变量;
synchronized/lock锁;
因为性能等原因不建议使用,推荐方法1
|