問題:
public class Test {
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
private static class Task implements Runnable {
public void run() {
try {
System.out.println(sdf.parse("2016-03-21 12:00:00").getTime());
} catch (ParseException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Task task = new Task();
Thread t1 = new Thread(task);
t1.start();
Thread t2 = new Thread(task);
t2.start();
Thread t3 = new Thread(task);
t3.start();
Thread t4 = new Thread(task);
t4.start();
Thread t5 = new Thread(task);
t5.start();
}
}
平時經常都會用到SimpleDateFormat來對日期字符串進行解析和格式化。而且一般都是創建一個Util工具類,然后定義一個靜態的SimpleDateFormat實例變量。需要格式化時就直接使用這個變量進行操作。
在大多數正常的測試情況之下,出來的結果都是沒問題的。然而,我最近在看《Java并發編程實踐》的過程中才發現這樣做并不是線程安全的,當在并發量較高的生產環境中,問題就出現了,會出現各種不同的錯誤,例如線程被掛死,轉換的時間不正確。
下圖的輸出結果是線程被掛死了:
下圖是轉換的時間不對:
原因
以前之所以忽略這個問題,一來對多線程沒有很深入的理解;二是因為從這個類中完全看不出與線程安全有什么關系,因為SimpleDateFormat 實例變量已經是用final 修飾了,就一直以為是狀態不變的了。
而在jdk的官方文檔里面,也有提到
" Date formats are not synchronized. It is recommended to create separate format instances for each thread. If multiple threads access a format concurrently, it must be synchronized externally."
就是說。Date formats不是同步的,建議每個線程都分別創建format實例變量;或者如果多個線共享一個format的話,必須保持在使用format時是同步的
而從源碼入手分析原因,會發現在SimpleDateFormat類的parse()方法有這么一段注釋
This parsing operation uses the DateFormat#calendar
to produce a Date. All of the
calendar's date-time fields are Calendar#clear()
cleared before parsing, and the calendar's default
values of the date-time fields are used for any missing
date-time information.
大概意思就是parse()方法使用calendar來生成返回的Date實例,而每次parse之前,都會先把calendar里的相關屬性清除掉。
問題是這個calendar是個全局變量,也就是線程共享的。因此就會出現一個線程剛把calendar設置好,另一個線程把它給清空了,
這時第一個線程再parse的話就會有問題了。
解決方案
需要的時候創建局部變量
public Date formatDate(Date d) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
return sdf.parse(d);
}
創建一個共享的SimpleDateFormat實例變量,但是在使用的時候,需要對這個變量進行同步
private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
public Date formatDate(Date d) {
synchronized(sdf) {
return sdf.parse(d);
}
}
使用ThreadLocal為每個線程都創建一個線程獨享SimpleDateFormat變量
private ThreadLocal<SimpleDateFormat> tl = new ThreadLocal<SimpleDateFormat>();
public Date formatDate(Date d) {
SimpleDateFormat sdf = tl.get();
if(sdf == null) {
sdf = new SimpleDateFormat("yyyy-MM-dd");
tl.set(sdf);
}
return sdf.parse(d);
}
性能對比
分別測試了一下上面三種方案,每種方案分別開了10000線程去進行時間格式化,記錄下消耗的時間。測試代碼如下:
public class Test {
private final static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
/**
* Option 1: Create local instances when required
*/
public static Date parse1(String dateStr) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
try {
return sdf.parse(dateStr);
} catch (ParseException e) {
e.printStackTrace();
}
return null;
}
/**
* Option 2: Create an instance of SimpleDateFormat as a class variable but synchronize access to it.
*/
public static Date parse2(String dateStr) {
synchronized (sdf) {
try {
return sdf.parse(dateStr);
} catch (ParseException e) {
e.printStackTrace();
}
}
return null;
}
/**
* Option 3: Create a ThreadLocal to store a different instance of SimpleDateFormat for each thread.
*/
private static ThreadLocal<SimpleDateFormat> tl = new ThreadLocal<SimpleDateFormat>();
public static Date parse3(String dateStr) {
SimpleDateFormat sdf = tl.get();
if (sdf == null) {
sdf = new SimpleDateFormat("yyyy-MM-hh");
tl.set(sdf);
}
try {
return sdf.parse(dateStr);
} catch (ParseException e) {
e.printStackTrace();
}
return null;
}
private static class Task implements Callable<Date> {
public Date call() throws Exception {
return parse1("2016-03-21 12:00:00");
}
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(50);
List<Future<Date>> resultList = new ArrayList<Future<Date>>();
long s = System.currentTimeMillis();
//創建10000個任務并執行
for (int i = 0; i < 10000; i++) {
//使用ExecutorService執行Callable類型的任務,并將結果保存在future變量中
Future<Date> future = executorService.submit(new Task());
//將任務執行結果存儲到List中
resultList.add(future);
}
//遍歷任務的結果
for (Future<Date> fs : resultList) {
try {
while (!fs.isDone())
;//Future返回如果沒有完成,則一直循環等待,直到Future返回完成
System.out.println(fs.get().getTime()); //打印各個線程(任務)執行的結果
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
} finally {
//啟動一次順序關閉,執行以前提交的任務,但不接受新任務
executorService.shutdown();
}
}
long e = System.currentTimeMillis();
System.out.println("time elapse: " + (e - s));
}
}
對于每種方案,我各執行了12次,然后去掉一個最高消耗,一個最低消耗,剩下的取平均值。測試結果如下:
方案1: 平均 410ms
方案2: 平均 217ms
方案3: 平均 300ms
從結果看出,方案1性能最差,因為每次都需要new一個format實例。不推薦使用
方案2雖然看起來最優,但線程越來越多時,因為使用了同步塊,當一個線程調用format的方法時,其余線程就會阻塞,性能會優一定影響。
方案3,每個線程維護一個format實例,50個線程就有50個實例,對內存占用的消耗會比方案2要大,當影響不會太大,網上說法是:
對性能要求比較高的情況下,一般推薦使用這種方法
綜上所述,方案2和方案3都可以,至于具體使用哪一種,具體情況而定