SimpleDateFormat是線程不安全的,不能多個線程公用。而FastDateFormat和Joda-Time都是線程安全的,可以放心使用。
SimpleDateFormat是JDK提供的,不需要依賴第三方jar包,而其他兩種都得依賴第三方jar包。
FastDateFormat是apache的commons-lang3包提供的
Joda-Time需要依賴以下maven的配置(現在最新版本就是2.10.1)
<!-- https://mvnrepository.com/artifact/joda-time/joda-time -->
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.10.1</version>
</dependency>
一、SimpleDateFormat,線程不安全
SimpleDateFormat和FastDateFormat主要都是對時間的格式化
SimpleDateFormat在對時間進行格式化的方法format中,會先對calendar對象進行setTime的賦值,若是有多個線程同時操作一個SimpleDateFormat實例的話,就會對calendar的賦值進行覆蓋,進而產生問題。
在format方法里,有這樣一段代碼:
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; }
calendar.setTime(date)這條語句改變了calendar,稍后,calendar還會用到(在subFormat方法里),而這就是引發問題的根源。想象一下,在一個多線程環境下,有兩個線程持有了同一個SimpleDateFormat的實例,分別調用format方法:
線程1調用format方法,改變了calendar這個字段。
中斷來了。
線程2開始執行,它也改變了calendar。
又中斷了。
線程1回來了,此時,calendar已然不是它所設的值,而是走上了線程2設計的道路。如果多個線程同時爭搶calendar對象,則會出現各種問題,時間不對,線程掛死等等。
分析一下format的實現,我們不難發現,用到成員變量calendar,唯一的好處,就是在調用subFormat時,少了一個參數,卻帶來了這許多的問題。其實,只要在這里用一個局部變量,一路傳遞下去,所有問題都將迎刃而解。
這個問題背后隱藏着一個更為重要的問題--無狀態:無狀態方法的好處之一,就是它在各種環境下,都可以安全的調用。衡量一個方法是否是有狀態的,就看它是否改動了其它的東西,比如全局變量,比如實例的字段。format方法在運行過程中改動了SimpleDateFormat的calendar字段,所以,它是有狀態的。
有三種方法可以解決這個問題:
1、在每次需要使用的時候,進行SimpleDateFormat實例的創建,這種方式會導致創建一些對象實例,占用一些內存,不建議這樣使用。
package com.peidasoft.dateformat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; public class DateUtil { public static String formatDate(Date date)throws ParseException{ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.format(date); } public static Date parse(String strDate) throws ParseException{ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.parse(strDate); } }
說明:在需要用到SimpleDateFormat 的地方新建一個實例,不管什么時候,將有線程安全問題的對象由共享變為局部私有都能避免多線程問題,不過也加重了創建對象的負擔。在一般情況下,這樣其實對性能影響比不是很明顯的。
2、使用同步的方式,在調用方法的時候加上synchronized,這樣可以讓線程調用方法時,進行加鎖,也就是會造成線程間的互斥,對性能影響比較大。
package com.peidasoft.dateformat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; public class DateSyncUtil { private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public static String formatDate(Date date)throws ParseException{ synchronized(sdf){ return sdf.format(date); } } public static Date parse(String strDate) throws ParseException{ synchronized(sdf){ return sdf.parse(strDate); } } }
說明:當線程較多時,當一個線程調用該方法時,其他想要調用此方法的線程就要block,多線程並發量大的時候會對性能有一定的影響。
3、使用ThreadLocal進行保存,相當於一個線程只會有一個實例,進而減少了實例數量,也防止了線程間的互斥,推薦使用這種方式。
package com.peidasoft.dateformat; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; public class ConcurrentDateUtil { private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() { @Override protected DateFormat initialValue() { 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); } }
或者另一種寫法:
package com.peidasoft.dateformat; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; public class ThreadLocalDateUtil { private static final String date_format = "yyyy-MM-dd HH:mm:ss"; private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>(); public static DateFormat getDateFormat() { DateFormat df = threadLocal.get(); if(df==null){ df = new SimpleDateFormat(date_format); threadLocal.set(df); } return df; } public static String formatDate(Date date) throws ParseException { return getDateFormat().format(date); } public static Date parse(String strDate) throws ParseException { return getDateFormat().parse(strDate); } }
說明:使用ThreadLocal, 也是將共享變量變為獨享,線程獨享肯定能比方法獨享在並發環境中能減少不少創建對象的開銷。如果對性能要求比較高的情況下,一般推薦使用這種方法。
二、FastDateFormat,線程安全的,可以直接使用,不必考慮多線程的情況
FastDateFormat format = FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss"); System.out.println(format.format(new Date())); // 可以使用DateFormatUtils類來操作,方法里面也是使用的FastDateFormat類來做的 System.out.println(DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss"));
三、Joda-Time,線程安全
Joda-Time與以上兩種有所區別,不僅僅可以對時間進行格式化輸出,而且可以生成瞬時時間值,並與Calendar、Date等對象相互轉化,極大的方便了程序的兼容性。
Joda-Time的類具有不可變性,因此他們的實例是無法修改的,就跟String的對象一樣。
這種不可變性提現在所有API方法中,這些方法返回的都是新的類實例,與原來實例不同。
以下是Joda-Time的一些使用方法
// 得到當前時間 Date currentDate = new Date(); DateTime dateTime = new DateTime(); // DateTime.now() System.out.println(currentDate.getTime()); System.out.println(dateTime.getMillis()); // 指定某一個時間,如2016-08-29 15:57:02 Date oneDate = new Date(1472457422728L); DateTime oneDateTime = new DateTime(1472457422728L); DateTime oneDateTime1 = new DateTime(2016, 8, 29, 15, 57, 2, 728); System.out.println(oneDate.toString()); System.out.println(oneDateTime.toString()); // datetime默認的輸出格式為yyyy-MM-ddTHH:mm:ss.SSS System.out.println(oneDateTime1.toString("MM/dd/yyyy hh:mm:ss.SSSa")); // 直接就可以輸出規定的格式 // DateTime和Date之間的轉換 Date convertDate = new Date(); DateTime dt1 = new DateTime(convertDate); System.out.println(dt1.toString()); Date d1 = dt1.toDate(); System.out.println(d1.toString()); // DateTime和Calendar之間的轉換 Calendar c1 = Calendar.getInstance(); DateTime dt2 = new DateTime(c1); System.out.println(dt2.toString()); Calendar c2 = dt2.toCalendar(null); // 默認時區Asia/Shanghai System.out.println(c2.getTimeZone()); // 時間格式化 DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss"); DateTime dt3 = DateTime.parse("2016-08-29 13:32:33", formatter); System.out.println(dt3.toString()); // 若是不指定格式,會采用默認的格式,yyyy-MM-ddTHH:mm:ss.SSS,若被解析字符串只到年月日,后面的時分秒會全部默認為0 DateTime dt4 = DateTime.parse("2016-08-29T"); System.out.println(dt4.toString()); // 輸出locale 輸出2016年08月29日 16:43:14 星期一 System.out.println(new DateTime().toString("yyyy年MM月dd日 HH:mm:ss EE", Locale.CHINESE)); // 計算兩個日期間隔的天數 LocalDate start = new DateTime().toLocalDate(); LocalDate end = new LocalDate(2016, 8, 25); System.out.println(Days.daysBetween(start ,end).getDays()); // 這里要求start必須早於end,否則計算出來的是個負數 // 相同的還有間隔年數、月數、小時數、分鍾數、秒數等計算 // 類如Years、Hours等 // 對日期的加減操作 DateTime dt5 = new DateTime(); dt5 = dt5.plusYears(1) // 增加年 .plusMonths(1) // 增加月 .plusDays(1) // 增加日 .minusHours(1) // 減小時 .minusMinutes(1) // 減分鍾 .minusSeconds(1); // 減秒數 System.out.println(dt5.toString()); // 判斷是否閏月 DateTime dt6 = new DateTime(); DateTime.Property month = dt6.monthOfYear(); System.out.println(month.isLeap());
原文鏈接:https://blog.csdn.net/zxh87/article/details/19414885,https://jjhpeopl.iteye.com/blog/2321528