8.5 SimpleDateFormat是線程不安全的
SimpleDateFormat是Java提供的一個格式化和解析日期的工具類,日常開發(fā)中應(yīng)該經(jīng)常會用到,但是由于它是線程不安全的散劫,多線程公用一個SimpleDateFormat實例對日期進行解析或者格式化會導(dǎo)致程序出錯哨鸭,本節(jié)就討論下它為何是線程不安全的蔗蹋,以及如何避免。
問題復(fù)現(xiàn)
為了復(fù)現(xiàn)該問題,編寫如下代碼:
public class TestSimpleDateFormat {
//(1)創(chuàng)建單例實例
static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) {
//(2)創(chuàng)建多個線程,并啟動
for (int i = 0; i <10 ; ++i) {
Thread thread = new Thread(new Runnable() {
public void run() {
try {//(3)使用單例日期實例解析文本
System.out.println(sdf.parse("2017-12-13 15:17:27"));
} catch (ParseException e) {
e.printStackTrace();
}
}
});
thread.start();//(4)啟動線程
}
}
}
代碼(1)創(chuàng)建了SimpleDateFormat的一個實例涨岁,代碼(2)創(chuàng)建10個線程拐袜,每個線程都公用同一個sdf對象對文本日期進行解析,多運行幾次就會拋出java.lang.NumberFormatException異常梢薪,加大線程的個數(shù)有利于該問題復(fù)現(xiàn)蹬铺。
問題分析
為了便于分析首先奉上SimpleDateFormat的類圖結(jié)構(gòu):
可知每個SimpleDateFormat實例里面有一個Calendar對象,從后面會知道其實SimpleDateFormat之所以是線程不安全的就是因為Calendar是線程不安全的秉撇,后者之所以是線程不安全的是因為其中存放日期數(shù)據(jù)的變量都是線程不安全的甜攀,比如里面的fields,time等畜疾。
下面從代碼層面看下parse方法做了什么事情:
public Date parse(String text, ParsePosition pos)
{
//(1)解析日期字符串放入CalendarBuilder的實例calb中
.....
Date parsedDate;
try {//(2)使用calb中解析好的日期數(shù)據(jù)設(shè)置calendar
parsedDate = calb.establish(calendar).getTime();
...
}
catch (IllegalArgumentException e) {
...
return null;
}
return parsedDate;
}
Calendar establish(Calendar cal) {
...
//(3)重置日期對象cal的屬性值
cal.clear();
//(4) 使用calb中中屬性設(shè)置cal
...
//(5)返回設(shè)置好的cal對象
return cal;
}
- 代碼(1)主要的作用是解析字符串日期并把解析好的數(shù)據(jù)放入了 CalendarBuilder的實例calb中赴邻,CalendarBuilder是一個建造者模式印衔,用來存放后面需要的數(shù)據(jù)啡捶。
- 代碼(3)重置Calendar對象里面的屬性值,如下代碼:
public final void clear()
{
for (int i = 0; i < fields.length; ) {
stamp[i] = fields[i] = 0; // UNSET == 0
isSet[i++] = false;
}
areAllFieldsSet = areFieldsSet = false;
isTimeSet = false;
}
- 代碼(4)使用calb中解析好的日期數(shù)據(jù)設(shè)置cal對象
- 代碼(5) 返回設(shè)置好的cal對象
從上面步驟可知步驟(3)(4)(5)操作不是原子性操作奸焙,當多個線程調(diào)用parse
方法時候比如線程A執(zhí)行了步驟(3)(4)也就是設(shè)置好了cal對象瞎暑,在執(zhí)行步驟(5)前線程B執(zhí)行了步驟(3)清空了cal對象,由于多個線程使用的是一個cal對象与帆,所以線程A執(zhí)行步驟(5)返回的就可能是被線程B清空后的對象了赌,當然也有可能線程B執(zhí)行了步驟(4)被線程B修改后的cal對象。從而導(dǎo)致程序錯誤玄糟。
那么怎么解決那勿她?
- 第一種方式:每次使用時候new一個SimpleDateFormat的實例,這樣可以保證每個實例使用自己的Calendar實例,但是每次使用都需要new一個對象阵翎,并且使用后由于沒有其它引用逢并,就會需要被回收,開銷會很大郭卫。
- 第二種方式:究其原因是因為多線程下步驟(3)(4)(5)三個步驟不是一個原子性操作砍聊,那么容易想到的是對其進行同步,讓(3)(4)(5)成為原子操作贰军,可以使用synchronized進行同步玻蝌,具體如下:
public class TestSimpleDateFormat {
// (1)創(chuàng)建單例實例
static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) {
// (2)創(chuàng)建多個線程,并啟動
for (int i = 0; i < 10; ++i) {
Thread thread = new Thread(new Runnable() {
public void run() {
try {// (3)使用單例日期實例解析文本
synchronized (sdf) {
System.out.println(sdf.parse("2017-12-13 15:17:27"));
}
} catch (ParseException e) {
e.printStackTrace();
}
}
});
thread.start();// (4)啟動線程
}
}
}
使用同步意味著多個線程要競爭鎖词疼,在高并發(fā)場景下會導(dǎo)致系統(tǒng)響應(yīng)性能下降俯树。
- 第三種方式:使用ThreadLocal,這樣每個線程只需要使用一個SimpleDateFormat實例相比第一種方式大大節(jié)省了對象的創(chuàng)建銷毀開銷贰盗,并且不需要對多個線程直接進行同步许饿,使用ThreadLocal方式代碼如下:
public class TestSimpleDateFormat2 {
// (1)創(chuàng)建threadlocal實例
static ThreadLocal<DateFormat> safeSdf = new ThreadLocal<DateFormat>(){
@Override
protected SimpleDateFormat initialValue(){
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
public static void main(String[] args) {
// (2)創(chuàng)建多個線程,并啟動
for (int i = 0; i < 10; ++i) {
Thread thread = new Thread(new Runnable() {
public void run() {
try {// (3)使用單例日期實例解析文本
System.out.println(safeSdf.get().parse("2017-12-13 15:17:27"));
} catch (ParseException e) {
e.printStackTrace();
}
}
});
thread.start();// (4)啟動線程
}
}
}
代碼(1)創(chuàng)建了一個線程安全的SimpleDateFormat實例童太,步驟(3)在使用的時候首先使用get()方法獲取當前線程下SimpleDateFormat的實例米辐,在第一次調(diào)用ThreadLocal的get()方法適合會觸發(fā)其initialValue方法用來創(chuàng)建當前線程所需要的SimpleDateFormat對象胸完。
總結(jié)
本節(jié)通過簡單介紹SimpleDateFormat的原理說明了SimpleDateFormat是線程不安全的,應(yīng)該避免多線程下使用SimpleDateFormat的單個實例翘贮,多線程下使用時候最好使用ThreadLocal對象赊窥。更多并發(fā)編程中需要注意的情景以及解決方法敬請期待 Java中高并發(fā)編程必備基礎(chǔ)之并發(fā)包源碼剖析
一書出版