java在做日期轉(zhuǎn)換時(shí)我們會(huì)使用SimpleDateFormat做時(shí)間轉(zhuǎn)換焰檩,但其實(shí)SimpleDateFormat不是線程安全的,如果SimpleDateFormat用static聲明或只實(shí)例化一次被多個(gè)線程使用,并發(fā)度高時(shí)就會(huì)出并發(fā)異常,看如下例子
public static void main(String[] args) {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
List<String> lists = new ArrayList<String>() {
{
add("2018-11-22 01:11:11");
add("2018-11-22 02:22:22");
add("2018-11-22 03:33:33");
add("2018-11-22 04:44:44");
add("2018-11-22 03:55:55");
add("2018-11-22 04:55:56");
}
};
ExecutorService executorService = Executors.newCachedThreadPool();
//用CountDownLatch增加并發(fā)度
CountDownLatch countDownLatch = new CountDownLatch(lists.size());
for (String list : lists) {
executorService.submit(() -> {
try {
countDownLatch.await();
Date parse = format.parse(list);
System.out.println(parse.toString());
} catch (Exception e) {
e.printStackTrace();
}
});
countDownLatch.countDown();
}
}
運(yùn)行日志如下诬滩,可能每次報(bào)錯(cuò)不一樣
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:2056)
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 thread.aqs.SimpleDateFormateTest.lambda$main$0(SimpleDateFormateTest.java:42)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
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: For input string: "E2E2."
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:2056)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at thread.aqs.SimpleDateFormateTest.lambda$main$0(SimpleDateFormateTest.java:42)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
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: For input string: "E2E"
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:2056)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at thread.aqs.SimpleDateFormateTest.lambda$main$0(SimpleDateFormateTest.java:42)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
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: For input string: "E.4220118E4"
或者日期錯(cuò)亂的結(jié)果
Thu Nov 22 04:55:56 CST 2018
Sat Nov 30 02:22:22 CST 2024
Tue Nov 30 01:11:11 CST 1
Thu Nov 22 04:55:56 CST 2018
原因
有兩個(gè)地方存在并發(fā)問(wèn)題
1、Calendar的共享
SimpleDateFormat的父類(lèi)持有一個(gè)對(duì)象Calendar
parse方法流程是這樣的:
//解析字符串
...
//清空Calendar
Calendar.clear();
//循環(huán)設(shè)置每個(gè)field
...
所以在多線程中會(huì)出現(xiàn)A線程設(shè)置Calendar值后被B線程修改灭将,
或者A和B線程同時(shí)設(shè)置Calendar的值疼鸟,都會(huì)出現(xiàn)錯(cuò)誤
看一下源碼
SimpleDateFormat繼承DateFormat
public class SimpleDateFormat extends DateFormat
入口
public Date parse(String source) throws ParseException
{
ParsePosition pos = new ParsePosition(0);
Date result = parse(source, pos);
if (pos.index == 0)
throw new ParseException("Unparseable date: \"" + source + "\"" ,
pos.errorIndex);
return result;
}
parse中解析字符串后的一部分
Date parsedDate;
try {
//出問(wèn)題的方法
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();
}
}
}
// An IllegalArgumentException will be thrown by Calendar.getTime()
// if any fields are out of range, e.g., MONTH == 17.
catch (IllegalArgumentException e) {
pos.errorIndex = start;
pos.index = oldStart;
return null;
}
calb.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();
for (int stamp = MINIMUM_USER_STAMP; stamp < nextStamp; stamp++) {
for (int index = 0; index <= maxFieldIndex; index++) {
if (field[index] == stamp) {
//設(shè)置field
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;
}
2、DecimalFormat對(duì)象不是線程安全的
SimpleDateFormat持有對(duì)象DecimalFormat庙曙,而DecimalFormat也不是線程安全的空镜,所以會(huì)報(bào)上面的那些錯(cuò)
parse方法
DecimalFormat對(duì)象持有一個(gè)DigitList對(duì)象,用來(lái)標(biāo)記解析狀態(tài)
private transient DigitList digitList = new DigitList();
@Override
public Number parse(String text, ParsePosition pos) {
// special case NaN
if (text.regionMatches(pos.index, symbols.getNaN(), 0, symbols.getNaN().length())) {
pos.index = pos.index + symbols.getNaN().length();
return new Double(Double.NaN);
}
boolean[] status = new boolean[STATUS_LENGTH];
//這里subparse方法清空和組裝digitList,多線程并發(fā)會(huì)出問(wèn)題
if (!subparse(text, pos, positivePrefix, negativePrefix, digitList, false, status)) {
return null;
}
...
如何解決
每次使用時(shí)都new一個(gè)SimpleDateFormat當(dāng)然可以解決這個(gè)問(wèn)題捌朴,但頻繁創(chuàng)建銷(xiāo)毀對(duì)象效能不高吴攒,方法上加鎖又會(huì)降低并發(fā)度
因?yàn)槊總€(gè)線程自己執(zhí)行肯定是按順序執(zhí)行,所以可以利用ThreadLocal
public class DateUtil {
private static ThreadLocal<DateFormat> threadLocal = ThreadLocal.withInitial(()->{
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
});
public static Date parse(String dateStr) throws ParseException {
return threadLocal.get().parse(dateStr);
}
public static String format(Date date) {
return threadLocal.get().format(date);
}
}
每個(gè)線程持有自己的SimpleDateFormat砂蔽,再跑測(cè)試代碼洼怔,發(fā)現(xiàn)不報(bào)錯(cuò)了
//輸出
Thu Nov 22 01:11:11 CST 2018
Thu Nov 22 04:55:56 CST 2018
Thu Nov 22 03:33:33 CST 2018
Thu Nov 22 03:55:55 CST 2018
Thu Nov 22 02:22:22 CST 2018
Thu Nov 22 04:44:44 CST 2018
使用jodatime
String date = "2018-11-22 02:22:22";
DateTime dateTime = DateTime.parse(date, DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss"));
或者使用java8的LocalDateTime
String str = "1986-04-08 12:30:22";
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime dateTime = LocalDateTime.parse(str, formatter);