SimpleDateFormat是JDK中長久以來自帶的日期時間格式化類,但是它有線程安全性方面的問題泳唠,使用時要避免它帶來的影響。
SimpleDateFormat是線程不安全的
寫一個SimpleDateFormat在并發(fā)環(huán)境下簡單的例子先。
public class SimpleDateFormatExample {
private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) {
// 創(chuàng)建線程池溪椎,最好直接new ThreadPoolExecutor,而不是用Executors工具類
// ExecutorService threadPool = Executors.newCachedThreadPool();
ExecutorService threadPool = new ThreadPoolExecutor(
5, 50, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(100)
);
List<String> dates = Arrays.asList(
"2019-02-21 15:47:01",
"2018-03-22 16:46:02",
"2017-04-23 17:45:03",
"2016-05-24 18:44:04",
"2015-06-25 19:43:05",
"2014-07-26 20:42:06",
"2013-08-27 21:41:07",
"2012-09-28 22:40:08",
"2011-10-29 23:39:09"
);
for (String date : dates) {
threadPool.execute(() -> {
try {
System.out.println(simpleDateFormat.parse(date));
} catch (ParseException e) {
e.printStackTrace();
}
});
}
}
}
輸出非常混亂校读,拋出大量NumberFormatException沼侣,以及得出錯誤的結(jié)果。
Exception in thread "pool-1-thread-2" java.lang.NumberFormatException: For input string: ""
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:601)
at java.lang.Long.parseLong(Long.java:631)
at java.text.DigitList.getLong(DigitList.java:195)
at java.text.DecimalFormat.parse(DecimalFormat.java:2084)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at me.lmagics.SimpleDateFormatExample.lambda$main$0(SimpleDateFormatExample.java:42)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
Exception in thread "pool-1-thread-4" Exception in thread "pool-1-thread-3" java.lang.NumberFormatException: For input string: ".809E.809E22"
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:2089)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at me.lmagics.SimpleDateFormatExample.lambda$main$0(SimpleDateFormatExample.java:42)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
java.lang.NumberFormatException: For input string: ".809E.809E22"
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:2089)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
Wed Mar 23 16:46:02 CST 1
Tue May 24 18:44:04 CST 2016
at java.text.DateFormat.parse(DateFormat.java:364)
Wed Mar 23 16:46:02 CST 1
at me.lmagics.SimpleDateFormatExample.lambda$main$0(SimpleDateFormatExample.java:42)
Thu Feb 21 15:47:01 CST 2019
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
Thu Jun 25 19:43:05 CST 2015
Thu Oct 29 23:39:09 CST 2011
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
然后通過SimpleDateFormat的源碼來分析它線程不安全的根本原因歉秫。
SimpleDateFormat是繼承自DateFormat類蛾洛,DateFormat類中維護了一個全局的Calendar變量。
/**
* The {@link Calendar} instance used for calculating the date-time fields
* and the instant of time. This field is used for both formatting and
* parsing.
*
* <p>Subclasses should initialize this field to a {@link Calendar}
* appropriate for the {@link Locale} associated with this
* <code>DateFormat</code>.
* @serial
*/
protected Calendar calendar;
從注釋可以看出端考,這個Calendar對象既用于格式化也用于解析日期時間雅潭。再查看parse()方法接近最后的部分。
Date parsedDate;
try {
parsedDate = calb.establish(calendar).getTime();
// If the year value is ambiguous,
// then the two-digit year == the default start year
if (ambiguousYear[0]) {
if (parsedDate.before(defaultCenturyStart)) {
parsedDate = calb.addYear(100).establish(calendar).getTime();
}
}
}
// An IllegalArgumentException will be thrown by Calendar.getTime()
// if any fields are out of range, e.g., MONTH == 17.
catch (IllegalArgumentException e) {
...
}
return parsedDate;
可見却特,最后的返回值是通過調(diào)用CalendarBuilder.establish()方法獲得的扶供,而它的入?yún)⒄镁褪乔懊娴腃alendar對象。
Calendar establish(Calendar cal) {
boolean weekDate = isSet(WEEK_YEAR)
&& field[WEEK_YEAR] > field[YEAR];
if (weekDate && !cal.isWeekDateSupported()) {
// Use YEAR instead
if (!isSet(YEAR)) {
set(YEAR, field[MAX_FIELD + WEEK_YEAR]);
}
weekDate = false;
}
cal.clear();
// Set the fields from the min stamp to the max stamp so that
// the field resolution works in the Calendar.
for (int stamp = MINIMUM_USER_STAMP; stamp < nextStamp; stamp++) {
for (int index = 0; index <= maxFieldIndex; index++) {
if (field[index] == stamp) {
cal.set(index, field[MAX_FIELD + index]);
break;
}
}
}
if (weekDate) {
int weekOfYear = isSet(WEEK_OF_YEAR) ? field[MAX_FIELD + WEEK_OF_YEAR] : 1;
int dayOfWeek = isSet(DAY_OF_WEEK) ?
field[MAX_FIELD + DAY_OF_WEEK] : cal.getFirstDayOfWeek();
if (!isValidDayOfWeek(dayOfWeek) && cal.isLenient()) {
if (dayOfWeek >= 8) {
dayOfWeek--;
weekOfYear += dayOfWeek / 7;
dayOfWeek = (dayOfWeek % 7) + 1;
} else {
while (dayOfWeek <= 0) {
dayOfWeek += 7;
weekOfYear--;
}
}
dayOfWeek = toCalendarDayOfWeek(dayOfWeek);
}
cal.setWeekDate(field[MAX_FIELD + WEEK_YEAR], weekOfYear, dayOfWeek);
}
return cal;
}
該方法中先后調(diào)用了cal.clear()與cal.set()裂明,也就是先清除cal對象中設(shè)置的值椿浓,再重新設(shè)置新的值。由于Calendar內(nèi)部并沒有線程安全機制闽晦,并且這兩個操作也都不是原子性的扳碍,所以當(dāng)多個線程同時操作一個SimpleDateFormat時就會引起cal的值混亂。類似地仙蛉,format()方法也存在同樣的問題笋敞。
為什么用ThreadLocal能解決問題
ThreadLocal即線程本地變量。它用來為每個線程維護一個專屬的變量副本荠瘪,線程對自己的變量副本進行操作時夯巷,對其他線程的變量副本沒有任何影響。由此可見哀墓,它特別適合解決并發(fā)情況下變量共享造成的線程安全性問題趁餐,前提是各個副本隔離后不影響業(yè)務(wù)運行。
以上面的SimpleDateFormat問題為例篮绰,ThreadLocal可以這樣使用后雷。
// 可以直接設(shè)置初始值
private static ThreadLocal<SimpleDateFormat> simpleDateFormat = ThreadLocal.withInitial(() ->
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
);
// 也可以調(diào)用set()方法
private static ThreadLocal<SimpleDateFormat> simpleDateFormat = new ThreadLocal<>();
simpleDateFormat.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
...
// 調(diào)用get()方法取得值
System.out.println(simpleDateFormat.get().parse(date));
// 移除
simpleDateFormat.remove();
那么ThreadLocal是采用了什么機制來實現(xiàn)變量副本隔離的呢?在Thread類內(nèi)部吠各,有如下的定義臀突。
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
可見每個線程都維護了一個叫ThreadLocalMap的東西,它是ThreadLocal中定義的一個靜態(tài)內(nèi)部類贾漏。其實現(xiàn)類似于HashMap惧辈,但沒實現(xiàn)Map接口,數(shù)據(jù)結(jié)構(gòu)和內(nèi)部邏輯也有不同磕瓷。ThreadLocalMap.Entry是這樣定義的盒齿。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
該Entry的鍵值類型都是確定的念逞。值就是變量的副本,鍵是對ThreadLocal對象的一個弱引用边翁。由于線程并不能直接訪問和存取ThreadLocalMap翎承,只能藉由ThreadLocal進行,因此不同的線程之間的變量副本就實現(xiàn)了隔離符匾。
上面的圖來自阿里Java開發(fā)手冊叨咖,清晰地示出了線程、ThreadLocal啊胶、ThreadLocalMap三者的引用關(guān)系甸各,鼓掌。
另外焰坪,ThreadLocal還可能存在內(nèi)存泄漏的問題趣倾,前人已經(jīng)寫過很好的分析文章,如http://www.reibang.com/p/a1cd61fa22da某饰,下面稍作總結(jié)儒恋。
-
回憶Java中的4種引用類型:強、軟黔漂、弱诫尽、虛引用,其引用強度依次遞減炬守。其中弱引用只能存活到下一次Young GC發(fā)生之前牧嫉。
- ThreadLocalMap.Entry中的鍵就是弱引用,如果它被回收减途,會出現(xiàn)key為null但value仍然存在的情況(value是強引用酣藻,當(dāng)然Entry也沒有被回收),有內(nèi)存泄漏風(fēng)險观蜗。ThreadLocal的設(shè)計者已經(jīng)考慮到了這種情況,調(diào)用get()/set()/remove()方法時衣洁,都會調(diào)用expungeStaleEntry()方法來刪除這種key已經(jīng)被回收了的Entry墓捻。這段代碼很有意思,關(guān)鍵點添加了一點注釋坊夫。
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
// 將值和Entry都設(shè)成null砖第,這樣在下一次GC根搜索時均不可達,就被回收了
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
// 還沒完环凿,接下來會繼續(xù)找key為null的其他Entry梧兼,一起刪掉
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
// 找到了,將值和Entry都設(shè)成null
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
// nextIndex()方法就是(i + 1)%len智听,ThreadLocalMap是采用線性探測開放定址解決hash沖突的
// 這比HashMap的鏈地址法(數(shù)組+鏈表)簡單得多羽杰,當(dāng)然ThreadLocal變量多了之后渡紫,解決沖突的時間會邊長
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
- 但是就算這樣設(shè)計了,也不能完全防止ThreadLocal內(nèi)存泄漏考赛,因為它可以是static的惕澎,也有可能在分配了變量副本之后沒調(diào)用任何方法。另外颜骤,由于ThreadLocalMap的生命周期和線程一樣長唧喉,因此不管Entry的鍵是對ThreadLocal的強引用還是弱引用,都有可能出現(xiàn)ThreadLocal被回收變成null的情況忍抽。
- 所以八孝,完全避免內(nèi)存泄漏的唯一手段就是在ThreadLocal用完后,調(diào)用remove()方法鸠项。