SimpleDateFormat定義
SimpleDateFormat 是一個以與語言環境有關的方式來格式化和解析日期的具體類。它允許進行格式化(日期 -> 文本)、解析(文本 -> 日期)和規范化。 SimpleDateFormat 使得可以選擇任何用戶定義的日期-時間格式的模式。但是,仍然建議通過 DateFormat 中的 getTimeInstance、getDateInstance 或 getDateTimeInstance
來創建日期-時間格式器。每一個這樣的類方法都能夠返回一個以默認格式模式初始化的日期/時間格式器。可以根據需要使用 applyPattern 方法來修改格式模式。
官網同步建議
同步
日期格式是不同步的。建議為每個線程創建獨立的格式實例。如果多個線程同時訪問一個格式,則它必須是外部同步的。
為什么線程不安全
上圖中,SimpleDateFormat類中,有個對象calendar
calendar
DateFormat
使用 calendar 來生成實現日期和時間格式化所需的時間字段值。
當SimpleDateFormat用static申明,多個線程共享SimpleDateFormat對象是,也共享該對象的calendar對象。而當調用parse方法時,會clear所有日歷字段和值。當線程A正在調用parse,線程B調用clear,這樣解析后的數據就會出現偏差
//parse方法 @Override public Date parse(String text, ParsePosition pos) { try { parsedDate = calb.establish(calendar).getTime(); ... } }
//establish方法 Calendar establish(Calendar cal) { ... //將此 Calendar 的所有日歷字段值和時間值(從歷元至現在的毫秒偏移量)設置成未定義 cal.clear(); }
同樣 formart中也用到了calendar對象,將date設置到日歷的時間字段中
private StringBuffer format(Date date, StringBuffer toAppendTo, FieldDelegate delegate) { // Convert input date to time field list calendar.setTime(date); ... }
當線程A調用setTime,而線程B也調用setTime,這時候線程A最后得到的時間是 最后線程B的時間。也會導致數據偏差
不安全示例:
public static void main(String[] args) throws InterruptedException { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); String dateStr = "1111-11-11 11:11:11"; ExecutorService executorService = Executors.newFixedThreadPool(100); for(int i=0;i<100;i++) { executorService.submit(new Runnable() { @Override public void run() { try { //多個線程操作同一個sdf對象 System.out.println(sdf.format(sdf.parse(dateStr)) + "---" + Thread.currentThread().getName()); } catch (ParseException e) { System.out.println("--------------> error, " + e.getMessage()); } } }); } executorService.shutdown(); }
執行結果:
... 1111-11-11 11:11:11---pool-1-thread-69 0011-11-11 11:11:11---pool-1-thread-72 0011-11-11 11:11:11---pool-1-thread-71 1111-11-11 11:11:11---pool-1-thread-73 1111-11-11 11:11:11---pool-1-thread-75 1111-11-11 11:11:11---pool-1-thread-76 1111-11-11 11:11:11---pool-1-thread-89 1111-11-11 00:11:11---pool-1-thread-93 1111-11-11 11:11:11---pool-1-thread-96 ...
可以看到數據出現偏差
解決方案
1.為每個實例創建一個單獨的SimpleDateFormat對象
public static void main(String[] args) throws InterruptedException { //SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); String dateStr = "1111-11-11 11:11:11"; ExecutorService executorService = Executors.newFixedThreadPool(100); for(int i=0;i<100;i++) { executorService.submit(new Runnable() { //為每個線程創建自己的sdf對象 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); @Override public void run() { try { System.out.println(sdf.format(sdf.parse(dateStr)) + "---" + Thread.currentThread().getName()); } catch (ParseException e) { System.out.println("--------------> error, " + e.getMessage()); } } }); } executorService.shutdown(); }
缺點:每次new一個實例,都會new一個format對象,虛擬機內存消耗大,垃圾回收頻繁
2.給靜態SimpleDateFormat對象加鎖,使用Lock或者synchronized修飾
public static void main(String[] args) throws InterruptedException { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); String dateStr = "1111-11-11 11:11:11"; ExecutorService executorService = Executors.newFixedThreadPool(100); for(int i=0;i<100;i++) { executorService.submit(new Runnable() { @Override public void run() { //加同步鎖 synchronized (sdf) { try { System.out.println(sdf.format(sdf.parse(dateStr)) + "---" + Thread.currentThread().getName()); } catch (ParseException e) { System.out.println("--------------> error, " + e.getMessage()); } } } }); } executorService.shutdown(); }
缺點:性能差,其他線程要等待鎖釋放
3.使用ThreadLocal為每個線程創建一個SimpleDateFormat對象副本,有線程隔離性,各自的副本對象也不會被其他線程影響
public static void main(String[] args) throws InterruptedException { //SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); //初始化threadLocal並設置值 ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<SimpleDateFormat>(){ @Override protected SimpleDateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); } }; String dateStr = "1111-11-11 11:11:11"; ExecutorService executorService = Executors.newFixedThreadPool(100); for(int i=0;i<100;i++) { executorService.submit(new Runnable() { @Override public void run() { try { System.out.println(threadLocal.get().format(threadLocal.get().parse(dateStr)) + "---" + Thread.currentThread().getName()); } catch (ParseException e) { System.out.println("--------------> error, " + e.getMessage()); } } }); } executorService.shutdown();
//清理threadLocal,生產環境不清理容易導致內存溢出
threadLocal.remove();
}
ThreadLocal原理分析