SimpleDateFormat是Java中很是常見的一個類,用來解析和格式化日期字符串。可是SimpleDateFormat在多線程的環境並非安全的,這個是很容易犯錯的部分,接下來說一下這個問題出現的過程以及解決的思路。java
問題描述:
先看代碼,用來獲取一個月的天數的:git
import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; public class DateUtil { /** * 獲取月份天數 * @param time 201202 * @return */ public static int getDays(String time) throws Exception { // String time = "201202"; SimpleDateFormat sdf = new SimpleDateFormat("yyyyMM"); Date date = sdf.parse(time); Calendar c = Calendar.getInstance(); c.setTime(date); int day = c.getActualMaximum(Calendar.DATE); return day; } }
能夠看到在這個方法裏,每次要獲取值的時候就先要建立一個SimpleDateFormat的實例,頻繁調用這個方法的狀況下很耗性能。爲了不大量實例的頻繁建立和銷燬,咱們一般會使用單例模式或者靜態變量進行改造,通常會這麼改:程序員
import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; public class DateUtil { private static SimpleDateFormat sdf = new SimpleDateFormat("yyyyMM"); /** * 獲取月份天數 * @param time 201202 * @return */ public static int getDays(String time) throws Exception { // String time = "201202"; Date date = sdf.parse(time); Calendar c = Calendar.getInstance(); c.setTime(date); int day = c.getActualMaximum(Calendar.DATE); return day; } }
此時無論調用多少次這個方法,java虛擬機裏只有一個SimpleDateFormat對象,效率和性能確定要比第一個方法好,這個也是不少程序員選擇的方法。可是,在這個多線程的條件下,多個thread共享同一個SimpleDateFormat,而SimpleDateFormat自己又是線程非安全的,這樣就很容易出各類問題。安全
驗證問題:
用一個簡單的例子驗證一下多線程環境下SimpleDateFormat的運行結果:多線程
import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.concurrent.CountDownLatch; public class DateUtil { private static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public static String format(Date date) { return dateFormat.format(date); } public static Date parse(String dateStr) throws ParseException { return dateFormat.parse(dateStr); } public static void main(String[] args) { final CountDownLatch latch = new CountDownLatch(1); final String[] strs = new String[] {"2016-01-01 10:24:00", "2016-01-02 20:48:00", "2016-01-11 12:24:00"}; for (int i = 0; i < 10; i++) { new Thread(new Runnable() { @Override public void run() { try { latch.await(); } catch (InterruptedException e) { e.printStackTrace(); } for (int i = 0; i < 10; i++){ try { System.out.println(Thread.currentThread().getName()+ "\t" + parse(strs[i % strs.length])); Thread.sleep(100); } catch (ParseException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); } latch.countDown(); } }
看一下運行的結果:併發
Thread-9 Fri Jan 01 10:24:00 CST 2016 Thread-1 Sat Feb 25 00:48:00 CST 20162017 Thread-5 Sat Feb 25 00:48:00 CST 20162017 Exception in thread "Thread-4" Exception in thread "Thread-6" java.lang.NumberFormatException: For input string: "2002.E20022E" 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 DateUtil.parse(DateUtil.java:24) at DateUtil$2.run(DateUtil.java:45)
那麼爲何SimpleDateFormat不是線程安全的呢?app
查找問題:ide
首先看一下SimpleDateFormat的源碼:性能
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; }
能夠看到format()方法先將日期存放到一個Calendar對象中,而這個Calendar在SimpleDateFormat中是以成員變量的形式存在的。隨後調用subFormat()時會再次用到成員變量Calendar。這就是問題所在。一樣,在parse()方法裏也會存在相應的問題。
試想,在多線程環境下,若是兩個線程都使用同一個SimpleDateFormat實例,那麼就有可能存在其中一個線程修改了calendar後緊接着另外一個線程也修改了calendar,那麼隨後第一個線程用到calendar時已經不是它所期待的值了。spa
避免問題:
那麼,如何保證SimpleDateFormat的線程安全呢?
1.每次使用SimpleDateFormat時都建立一個局部的SimpleDateFormat對象,跟一開始的那個方法同樣,可是存在性能上的問題,開銷較大。
2.加鎖或者同步
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
public class DateUtil { private static ThreadLocal<SimpleDateFormat> local = new ThreadLocal<SimpleDateFormat>() { @Override protected SimpleDateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); } }; public static String format(Date date) { return local.get().format(date); } public static Date parse(String dateStr) throws ParseException { return local.get().parse(dateStr); } }
使用ThreadLocal能夠確保每一個線程均可以獲得一個單獨的SimpleDateFormat對象,既避免了頻繁建立對象,也避免了多線程的競爭。