原文:https://www.jianshu.com/p/b212afa16f1f
1.SimpleDateFormat為什么不是線程安全的?
- 如果我們把SimpleDateFormat定義成static成員變量,那么多個thread之間會共享這個SimpleDateFormat對象, 所以Calendar對象也會共享。
public static SimpleDateFormat formater = new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss"); System.out.println(formater.format(new Date())+" Exception made...");
- DateFormat.java
public final String format(Date date) { return format(date, new StringBuffer(), DontCareFieldPosition.INSTANCE).toString(); } public abstract StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition fieldPosition);
- SimpleDateFormat.java
@Override public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition pos) { // 如此輕易地使用內部變量,肯定不能線程安全 // 線程都對pos進行寫操作,必然會影響其他線程的讀操作 pos.beginIndex = pos.endIndex = 0; return format(date, toAppendTo, pos.getFieldDelegate()); } private StringBuffer format(Date date, StringBuffer toAppendTo, FieldDelegate delegate) { // Convert input date to time field list // 這里已經徹底毀壞線程的安全性 calendar.setTime(date); boolean useDateFormatSymbols = useDateFormatSymbols(); for (int i = 0; i < compiledPattern.length; ) { int tag = compiledPattern[i] >>> 8; int count = compiledPattern[i++] & 0xff; if (count == 255) { count = compiledPattern[i++] << 16; count |= compiledPattern[i++]; } switch (tag) { case TAG_QUOTE_ASCII_CHAR: toAppendTo.append((char)count); break; case TAG_QUOTE_CHARS: toAppendTo.append(compiledPattern, i, count); i += count; break; default: subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols); break; } } return toAppendTo; }
1.1 復現錯誤
public class DateFormatTest { private static SimpleDateFormat sdf = new SimpleDateFormat("dd-MMM-yyyy", Locale.US); private static String date[] = { "01-Jan-1999", "09-Jan-2000", "08-Jan-2001" , "07-Jan-2002" , "06-Jan-2003" , "05-Jan-2004" , "04-Jan-2005" , "03-Jan-2006" , "02-Jan-2007" }; public static void main(String[] args) { for (int i = 0; i < date.length; i++) { final int temp = i; new Thread(new Runnable() { @Override public void run() { try { while (true) { String str1 = date[temp]; String str2 = sdf.format(sdf.parse(str1)); System.out.println(Thread.currentThread().getName() + ", " + str1 + "," + str2); if(!str1.equals(str2)){ throw new RuntimeException(Thread.currentThread().getName() + ", Expected " + str1 + " but got " + str2); } } } catch (Exception e) { throw new RuntimeException("parse failed", e); } } }).start(); } } }

2.SimpleDateFormat線程不安全的解決方法
2.1 將SimpleDateFormat定義成局部變量:
SimpleDateFormat sdf = new SimpleDateFormat("dd-MMM-yyyy", Locale.US); String str1 = "01-Jan-2010"; String str2 = sdf.format(sdf.parse(str1));
缺點:每調用一次方法就會創建一個SimpleDateFormat對象,方法結束又要作為垃圾回收。
2.2 加一把線程同步鎖:synchronized(lock)
public class SyncDateFormatTest { private static SimpleDateFormat sdf = new SimpleDateFormat("dd-MMM-yyyy", Locale.US); private static String date[] = { "01-Jan-1999", "01-Jan-2000", "01-Jan-2001" }; public static void main(String[] args) { for (int i = 0; i < date.length; i++) { final int temp = i; new Thread(new Runnable() { @Override public void run() { try { while (true) { synchronized (sdf) { String str1 = date[temp]; Date date = sdf.parse(str1); String str2 = sdf.format(date); System.out.println(Thread.currentThread().getName() + ", " + str1 + "," + str2); if(!str1.equals(str2)){ throw new RuntimeException(Thread.currentThread().getName() + ", Expected " + str1 + " but got " + str2); } } } } catch (Exception e) { throw new RuntimeException("parse failed", e); } } }).start(); } } }
缺點:性能較差,每次都要等待鎖釋放后其他線程才能進入
2.3 使用ThreadLocal
每個線程都將擁有自己的SimpleDateFormat對象副本。
public class DateUtil { private static ThreadLocal<SimpleDateFormat> local = new ThreadLocal<SimpleDateFormat>(); public static Date parse(String str) throws Exception { SimpleDateFormat sdf = local.get(); if (sdf == null) { sdf = new SimpleDateFormat("dd-MMM-yyyy", Locale.US); local.set(sdf); } return sdf.parse(str); } public static String format(Date date) throws Exception { SimpleDateFormat sdf = local.get(); if (sdf == null) { sdf = new SimpleDateFormat("dd-MMM-yyyy", Locale.US); local.set(sdf); } return sdf.format(date); } }
public class ThreadLocalDateFormatTest { private static String date[] = { "01-Jan-1999", "01-Jan-2000", "01-Jan-2001" }; public static void main(String[] args) { for (int i = 0; i < date.length; i++) { final int temp = i; new Thread(new Runnable() { @Override public void run() { try { while (true) { String str1 = date[temp]; Date date = DateUtil.parse(str1); String str2 = DateUtil.format(date); System.out.println(str1 + "," + str2); if(!str1.equals(str2)){ throw new RuntimeException(Thread.currentThread().getName() + ", Expected " + str1 + " but got " + str2); } } } catch (Exception e) { throw new RuntimeException("parse failed", e); } } }).start(); } } }
3.使用DateTimeFormatter代替SimpleDateFormat
代碼參考DateTimeFormatterTest.java
jdk1.8中新增了 LocalDate 與 LocalDateTime等類來解決日期處理方法,同時引入了一個新的類DateTimeFormatter來解決日期格式化問題。
LocalDateTime,DateTimeFormatter兩個類都沒有線程問題,只要你自己不把它們創建為共享變量就沒有線程問題。
可以使用Instant代替 Date,LocalDateTime代替 Calendar,DateTimeFormatter 代替 SimpleDateFormat。
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy MM dd"); LocalDate date = LocalDate.parse("2017 06 17", formatter); System.out.println(formatter.format(date));
自測類
import java.text.SimpleDateFormat; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.Locale; public class DateFormatTest { private static SimpleDateFormat sdf = new SimpleDateFormat("dd-MM-yyyy", Locale.US); private static String date[] = { "01-01-1999", "09-01-2000", "08-01-2001" , "07-01-2002" , "06-01-2003" , "05-01-2004" , "04-01-2005" , "03-01-2006" , "02-01-2007" }; private static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy"); public static void main(String[] args) { for (int i = 0; i < date.length; i++) { final int temp = i; new Thread(new Runnable() { @Override public void run() { try { while (true) { String str1 = date[temp]; //線程安全 LocalDate date = LocalDate.parse(str1, formatter); String str2 = formatter.format(date); //線程不安全 // String str2 = sdf.format(sdf.parse(str1)); System.out.println(Thread.currentThread().getName() + ", " + str1 + "," + str2); if(!str1.equals(str2)){ throw new RuntimeException(Thread.currentThread().getName() + ", Expected " + str1 + " but got " + str2); } } } catch (Exception e) { throw new RuntimeException("parse failed", e); } } }).start(); } } }