前言
并發(fā)是Java開(kāi)發(fā)中繞不開(kāi)的一個(gè)話(huà)題?,F(xiàn)代處理器都是多核心,想要更好地榨干機(jī)器的性能,多線(xiàn)程編程是必不可少,所以,線(xiàn)程安全是每位Java Engineer的必修課。
應(yīng)對(duì)線(xiàn)程安全問(wèn)題,可大致分為兩種方式:
- 同步: 用Synchronized關(guān)鍵字,或者用java.util.concurrent.locks.Lock工具類(lèi)給臨界資源加鎖。
- 避免資源爭(zhēng)用: 將全局資源放在ThreadLocal變量中,避免并發(fā)訪(fǎng)問(wèn)。
本文將介紹第二種方式:ThreadLocal的實(shí)現(xiàn)原理以及為什么能保證線(xiàn)程安全。
ThreadLocal
下面是ThreadLocal的一個(gè)常見(jiàn)使用場(chǎng)景:
public class ThreadLocalTest {
// 一般都將ThreadLocal定義為靜態(tài)變量
private static final ThreadLocal<DateFormat> format = new ThreadLocal<DateFormat>(){
// 初始化ThreadLocal的值
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
public static void main(String[] args) {
// 啟動(dòng)20個(gè)線(xiàn)程
for (int i = 0; i < 20; i++) {
new Thread(() -> {
try {
// 得到SimpleDateFormat在本線(xiàn)程中的副本
DateFormat localFormat = format.get();
// 解析日期,這里并不會(huì)報(bào)錯(cuò)
Date date = localFormat.parse("2000-11-11 11:11:11");
System.out.println(date);
} catch (ParseException e) {
e.printStackTrace();
}
}).start();
}
}
}
大家應(yīng)該都知道,Java中SimpleDateFormat不是線(xiàn)程安全的,參考這篇文章。然而上述代碼的確不會(huì)報(bào)錯(cuò),說(shuō)明ThreadLocal確實(shí)能保證并發(fā)安全。
源碼解析
ThreadLocal概覽
上面的例子中,我們調(diào)用了ThreadLocal的initialValue和get方法,且來(lái)看一下get方法的實(shí)現(xiàn):
// 此類(lèi)的作者是兩個(gè)大神,前者是《Effective Java》的作者,后者是Java并發(fā)包的作者,并發(fā)大師
/*
* @author Josh Bloch and Doug Lea
* @since 1.2
*/
public class ThreadLocal<T> {
public T get() {
// 得到當(dāng)前線(xiàn)程
Thread t = Thread.currentThread();
// 根據(jù)當(dāng)前線(xiàn)程,拿到一個(gè)Map,暫且可以將之類(lèi)比為HashMap鍵值對(duì)形式
// 可見(jiàn)這個(gè)Map是與本線(xiàn)程相關(guān)的
ThreadLocalMap map = getMap(t);
if (map != null) {
// 通過(guò)this從Map中拿Entry,說(shuō)明Map中的Key就是ThreadLocal變量本身
// value就是ThreadLocal中所保存的對(duì)象
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 若Map沒(méi)有初始化(map == null),或者當(dāng)前ThreadLocal變量沒(méi)有初始化(e == null)
// 則調(diào)用此方法完成初始化
return setInitialValue();
}
// 原來(lái),這個(gè)ThreadLocalMap只是線(xiàn)程的一個(gè)成員變量!
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
}
public class Thread implements Runnable {
// Thread類(lèi)中定義了一個(gè)全局變量ThreadLocalMap
// 用來(lái)存放本線(xiàn)程中所有的ThreadLocal類(lèi)型變量,初始值為null
ThreadLocal.ThreadLocalMap threadLocals = null;
}
由get方法,我們可以得到如下信息:
- ThreadLocal變量保存在一個(gè)Map中,而這個(gè)Map正是Thread類(lèi)的一個(gè)全局變量。這也是ThreadLocal實(shí)現(xiàn)線(xiàn)程安全的一個(gè)關(guān)鍵點(diǎn):各個(gè)線(xiàn)程都有自己的Map,每個(gè)線(xiàn)程操作的都是自己的ThreadLocal變量副本,互不影響。
- ThreadLocalMap保存線(xiàn)程中所有的ThreadLocal變量,ThreadLocal變量是Key,ThreadLocal所對(duì)應(yīng)的值為Value。(在本文開(kāi)始的例子中,Key為format變量,Value為initialValue方法返回的值new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
- ThreadLocal是懶加載的,當(dāng)發(fā)現(xiàn)ThreadLocalMap或者當(dāng)前ThreadLocal變量未初始化時(shí),會(huì)調(diào)用setInitialValue方法進(jìn)行初始化。

ThreadLocal其他方法
繼續(xù)來(lái)看setInitialValue方法做了什么事情:
private T setInitialValue() {
// 調(diào)用initialValue方法初始化
// 這個(gè)方法即為我們定義ThreadLocal變量的時(shí)候重寫(xiě)的方法
T value = initialValue();
Thread t = Thread.currentThread();
// 獲取當(dāng)前線(xiàn)程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
// 如果Map已經(jīng)初始化好了,那直接初始化當(dāng)前ThreadLocal變量:
// 將自己(當(dāng)前ThreadLocal變量)作為key,保存的值作為value,set到Map里面去
map.set(this, value);
else
// 如果Map還未初始化,則初始化Map
createMap(t, value);
return value;
}
// 默認(rèn)的initialValue方法定義為protected,就是給我們重寫(xiě)的
protected T initialValue() {
return null;
}
void createMap(Thread t, T firstValue) {
// 新建一個(gè)ThreadLocalMap,賦值給當(dāng)前Thread
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
其他還有set和remove方法,很簡(jiǎn)單這里不另外講解。
難道ThreadLocal就此結(jié)束了么?有這么簡(jiǎn)單么?當(dāng)然沒(méi)有。因?yàn)?strong>ThreadLocalMap是Thread的一個(gè)成員變量,所以它的生命周期跟線(xiàn)程是一樣長(zhǎng)的。也就是說(shuō),只要線(xiàn)程還沒(méi)有被銷(xiāo)毀,那么Map就會(huì)常駐內(nèi)存,無(wú)法被GC,很容易造成內(nèi)存泄漏。那ThreadLocal是如何解決的呢?
答案是弱引用,Java中的引用類(lèi)型,可以參考這篇文章。
ThreadLocalMap
ThreadLocalMap是ThreadLocal的一個(gè)內(nèi)部類(lèi)。Java中有現(xiàn)成的類(lèi)HashMap,而ThreadLocal又費(fèi)勁千辛萬(wàn)苦自己實(shí)現(xiàn)了一個(gè)ThreadLocalMap,就是為防止內(nèi)存泄漏。
下面我們來(lái)探秘ThreadLocalMap,它跟普通的HashMap有什么區(qū)別。
ThreadLocalMap的數(shù)據(jù)結(jié)構(gòu)
static class ThreadLocalMap {
// 內(nèi)部類(lèi)Entry繼承了WeakReference
static class Entry extends WeakReference<ThreadLocal<?>> {
// ThreadLocal變量中保存的值
Object value;
// 可以看到,Entry只是簡(jiǎn)單的Key-Value,并沒(méi)有類(lèi)似HashMap中的鏈表
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
// ThreadLocalMap默認(rèn)大小
private static final int INITIAL_CAPACITY = 16;
// 此Entry數(shù)組,就是所有ThreadLocal存放的地方
private Entry[] table;
}
ThreadLocalMap維護(hù)了一個(gè)Entry數(shù)組(沒(méi)有鏈表,這是跟HashMap不一樣的地方),用來(lái)存放線(xiàn)程中所有的ThreadLocal變量。Entry繼承了WeakReference,并關(guān)聯(lián)了ThreadLocal,當(dāng)外界沒(méi)有其他強(qiáng)引用指向ThreadLocal對(duì)象時(shí),該ThreadLocal對(duì)象會(huì)在下一次GC時(shí)被內(nèi)存回收,也就是Entry中的Key會(huì)被回收掉,所以下面會(huì)看到清理key為null的Entry的操作。
Set操作
當(dāng)HashMap遇到哈希沖突的時(shí)候,是通過(guò)在同一個(gè)Hash Key上建立鏈表來(lái)解決的。既然ThreadLocalMap只維護(hù)了一個(gè)Entry數(shù)組,那它是怎么解決哈希沖突的呢?我們來(lái)看set方法的源碼:
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 根據(jù)ThreadLocal的hashcode,計(jì)算出在table中的槽位(index)
int i = key.threadLocalHashCode & (len-1);
// 從位置i開(kāi)始,逐個(gè)往后循環(huán),找到第一個(gè)空的槽位(條件e == null)
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 如果key相等,則直接將舊value覆蓋掉,換成新value
if (k == key) {
// 新值替換掉舊值,并return掉
e.value = value;
return;
}
// key == null,說(shuō)明弱引用之前已經(jīng)被內(nèi)存回收,則將值設(shè)在此槽位
if (k == null) {
// 該方法后面再解析
replaceStaleEntry(key, value, i);
return;
}
}
// 走到這里,這個(gè)i 是從key真正所在的hash槽之后數(shù),第一個(gè)非空槽位
// 將value包裝成Entry,放到位置i中
tab[i] = new Entry(key, value);
int sz = ++size;
// 查找是否有Entry已經(jīng)被回收
// 如果找到有Entry被回收,或者table的size大于閾值,執(zhí)行rehash操作
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
// 獲取下一個(gè)index。其實(shí)就是i + 1。當(dāng)超出table長(zhǎng)度的時(shí)候,歸0重新來(lái)
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
ThreadLocalMap是用開(kāi)放地址發(fā)來(lái)解決哈希沖突的。如果目標(biāo)槽位已經(jīng)有值了,首先判斷該值是不是就是自己。如果是,那就替換舊值;如果不是,再判斷該槽位的值是否有效(槽位上的ThreadLocal變量有沒(méi)有被垃圾回收),如果無(wú)效,則直接設(shè)置在該槽位,并執(zhí)行一些清理操作。如果該槽位上是一個(gè)有效的值,那么往后繼續(xù)尋找,直到找到空槽位為止。流程大概如下:

清理無(wú)效的Entry
到這里,我們應(yīng)該帶著一個(gè)疑問(wèn):弱引用清除的只是Entry中的key,也就是ThreadLocal變量,而Entry本身依然占據(jù)著table中的槽位。那代碼中是在哪里清理這些無(wú)效的Entry的呢?我們重點(diǎn)看一下上面沒(méi)有分析的兩個(gè)方法replaceStaleEntry和cleanSomeSlots
cleanSomeSlots
// 顧名思義,清除部分槽位,默認(rèn)掃描log(n)個(gè)槽位
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
// 注意無(wú)效Entry的判斷條件是,e.get() == null
// 即Entry中保存的弱引用已經(jīng)被GC,這種情況需要將對(duì)應(yīng)Entry清除
if (e != null && e.get() == null) {
// 如果發(fā)現(xiàn)有無(wú)效entry,那n會(huì)重新設(shè)置為table的長(zhǎng)度
// 即會(huì)繼續(xù)查找log(n)個(gè)槽位,判斷有沒(méi)有無(wú)效Entry
n = len;
removed = true;
// 調(diào)用expungeStaleEntry方法清除i位置的槽位
i = expungeStaleEntry(i);
}
// 循環(huán)條件為n右移一位,即除以2。所以默認(rèn)是循環(huán)log(n)次
} while ( (n >>>= 1) != 0);
// 如果有槽位被清除,返回true
return removed;
}
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 將i位置的槽位置為空
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
Entry e;
int i;
// 繼續(xù)往后檢查是否有無(wú)效Entry,直到遇到空的槽位tab[i]==null為止
for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// 如果Entry無(wú)效,將其清除
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
// 重新計(jì)算hash值h
int h = k.threadLocalHashCode & (len - 1);
// 如果新hash值h不等于當(dāng)前位置的槽位值i,這種情況需要rehash
// 給當(dāng)前i位置的e,重新找更合理的槽位
if (h != i) {
// 將i位置置空
tab[i] = null;
// 從h位置往后找第一個(gè)空槽位
while (tab[h] != null)
h = nextIndex(h, len);
// 將e放在第一個(gè)空槽位上
tab[h] = e;
}
}
}
// 返回接下來(lái)第一個(gè)空槽位的下標(biāo)
return i;
}
cleanSomeSlots方法會(huì)掃描部分的槽位,查看是否有無(wú)效的Entry。如果沒(méi)有找到,那么只掃描log(n)個(gè)槽位;如果有找到無(wú)效槽位,則會(huì)清除該槽位,并額外再掃描log(n)個(gè)槽位,以此類(lèi)推。
清空槽位的工作是expungeStaleEntry方法做的,除了清除當(dāng)前位置的Entry之外,它還會(huì)檢查往后連續(xù)的非空Entry,并清除其中無(wú)效值。同時(shí)還會(huì)判斷并處理rehash。這里為什么要rehash?因?yàn)榍懊嬗袩o(wú)效Entry被清除掉了,如果后面的Entry是因?yàn)閔ash沖突而被延到后面的,就可以把后面的Entry移到前面空出來(lái)的位置上,從而提高查詢(xún)效率。
cleanSomeSlots舉例

上圖的情況,我們分兩種情況討論:
-
如果從i=2開(kāi)始找:
- tab[2]所在位置為null,繼續(xù)循環(huán)i=nextIndex(i, len)=nextIndex(2, 8)=3
- tab[3]所在位置(k3,v3)有效,繼續(xù)循環(huán)i=nextIndex(i, len)=nextIndex(3, 4)=0
- tab[0]所在位置(k1,v1)有效,繼續(xù)循環(huán)i=nextIndex(i, len)=nextIndex(0, 2)=1
- tab[1]所在位置(k2,v2)有效,繼續(xù)循環(huán)i=nextIndex(i, len)=nextIndex(1, 1)=0
- tab[0]所在位置(k1,v1)有效,n==0結(jié)束
-
如果從i=11開(kāi)始找:
- tab[11]所在位置(null,v7)無(wú)效,調(diào)用expungeStaleEntry方法,expungeStaleEntry方法清空tab[11],并會(huì)往后循環(huán)判斷。因?yàn)閠ab[12]位置(null,v8)無(wú)效,所以tab[12]也會(huì)被清空;tab[13]位置(k9,v9)有效,則會(huì)判斷是否需要給(k9,v9)重新放位置。如果對(duì)k9執(zhí)行rehash之后依然是12,則不作處理;如果對(duì)k9執(zhí)行rehash之后是11,說(shuō)明該元素是因?yàn)閔ash碰撞被放到了12的位置,那么需要把元素放到tab[11]的位置。expungeStaleEntry方法返回第一個(gè)為null的下標(biāo)14,n重新設(shè)置為16,i=nextIndex(i, len)=nextIndex(14, 16)=15
- tab[15]所在位置(k10,v10)有效,繼續(xù)循環(huán)i=nextIndex(i, len)=nextIndex(15, 8)=0
- tab[0]所在位置(k1,v1)有效,繼續(xù)循環(huán)i=nextIndex(i, len)=nextIndex(0, 2)=1
- tab[1]所在位置(k2,v2)有效,繼續(xù)循環(huán)i=nextIndex(i, len)=nextIndex(1, 1)=0
- tab[0]所在位置(k1,v1)有效,n==0結(jié)束
replaceStaleEntry方法
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
// slotToExpunge記錄了包含staleSlot的連續(xù)段上,第一個(gè)無(wú)效Entry的下標(biāo)
int slotToExpunge = staleSlot;
// 往前遍歷非空槽位,找到第一個(gè)無(wú)效Entry的下標(biāo),記錄為slotToExpunge
for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
// 往后遍歷非空段,查找key所在的位置,即檢查key之前是否之前已經(jīng)被添加過(guò)
// 為什么到tab[i]==null為止?因?yàn)榭盏牟壑蟮膆ash值肯定已經(jīng)不一樣
for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == key) {
// 如果找到了key,那么說(shuō)明此key之前已經(jīng)添加過(guò),直接覆蓋舊值
// 因?yàn)閟taleSlot小于i,需要將兩個(gè)槽位的值進(jìn)行交換,以提高查詢(xún)效率
// 而被換到i處的無(wú)效Entry,會(huì)在之后的cleanSomeSlots被清除掉
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// 如果slotToExpunge的值并沒(méi)有變,說(shuō)明往前查找的過(guò)程中并未發(fā)現(xiàn)無(wú)效Entry
// 那么以當(dāng)前位置作為cleanSomeSlots的起點(diǎn)
if (slotToExpunge == staleSlot)
slotToExpunge = i;
// 這兩個(gè)方法都已經(jīng)分析過(guò),從slotToExpunge位置開(kāi)始清理無(wú)效Entry
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// 如果前面往前查找沒(méi)有發(fā)現(xiàn)無(wú)效Entry,且此處的Entry無(wú)效(k==null)
// 那么將說(shuō)明i處是第一個(gè)無(wú)效Entry,將slotToExpunge計(jì)為i
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// 如果key沒(méi)有找到,說(shuō)明這是一個(gè)新Entry,那么直接新建一個(gè)Entry放在staleSlot位置
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
if (slotToExpunge != staleSlot)
// 這兩個(gè)方法都已經(jīng)分析過(guò),從slotToExpunge位置開(kāi)始清理無(wú)效Entry
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
這個(gè)方法其實(shí)就是三個(gè)步驟:
- 往后查找該key在table中是否存在。如果存在,即之前已經(jīng)set過(guò)該key,那么需要覆蓋掉舊值,并且將key所在元素移到staleSlot位置。(為什么要移位置?因?yàn)樵厮诘奈恢胕,肯定在staleSlot之后,所以將元素往前放到staleSlot上可以提高查詢(xún)效率,并避免后續(xù)的rehash操作。)
- 如果key不存在,說(shuō)明是新set的操作,直接新建Entry,放在staleSlot位置。
- 調(diào)用cleanSomeSlots方法,清除無(wú)效的Entry
其他方法
剩下的方法都比較簡(jiǎn)單,解析見(jiàn)源碼注釋?zhuān)涣硗饨忉?br> get方法:
// get操作的方法
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
// i位置元素即為要找的元素,直接返回
if (e != null && e.get() == key)
return e;
else
// 否則調(diào)用getEntryAfterMiss方法
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// 從i位置開(kāi)始,往后遍歷查找,直到空槽位為止。為什么到空槽位為止?
// 根據(jù)開(kāi)地址法,空槽位之后的元素hash值肯定已經(jīng)不一樣,沒(méi)必要再繼續(xù)
while (e != null) {
ThreadLocal<?> k = e.get();
// key相等,這就是目標(biāo)元素,直接返回
if (k == key)
return e;
// key為null,則是無(wú)效元素,調(diào)用expungeStaleEntry方法清除i位置的元素
if (k == null)
expungeStaleEntry(i);
else
// 繼續(xù)尋找下一個(gè)元素
i = nextIndex(i, len);
e = tab[i];
}
// 沒(méi)有找到目標(biāo)元素,返回null
return null;
}
remove方法:
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
// 還是一樣的遍歷邏輯
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
// 找到目標(biāo)元素
if (e.get() == key) {
e.clear();
// 調(diào)用expungeStaleEntry方法清除i位置的元素
expungeStaleEntry(i);
return;
}
}
}
resize方法
// 當(dāng)元素個(gè)數(shù)大于threshold(默認(rèn)是table長(zhǎng)度的2/3)時(shí),需要resize
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
// 新table長(zhǎng)度是舊table的2倍
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
// 遍歷舊table
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
// 如果key為null,則這是個(gè)無(wú)效Entry,直接跳過(guò)(將值置為空方便GC)
if (k == null) {
e.value = null; // Help the GC
} else {
// 根據(jù)新table的長(zhǎng)度重新計(jì)算hash值
int h = k.threadLocalHashCode & (newLen - 1);
// 根據(jù)開(kāi)地址法,從h開(kāi)始找到第一個(gè)空槽位
while (newTab[h] != null)
h = nextIndex(h, newLen);
// 將該值放到該位置
newTab[h] = e;
count++;
}
}
}
// 設(shè)置新table的一些參數(shù)
setThreshold(newLen);
size = count;
table = newTab;
}
總結(jié)
本文從代碼層面,深入介紹了ThreadLocal的實(shí)現(xiàn)原理。
ThreadLocal可以保證線(xiàn)程安全,是因?yàn)樗o為每個(gè)線(xiàn)程都創(chuàng)建了一個(gè)變量的副本。每個(gè)線(xiàn)程訪(fǎng)問(wèn)的都是自己內(nèi)部的變量,不會(huì)有并發(fā)沖突。
作為線(xiàn)程內(nèi)部變量,它跟局部變量有什么區(qū)別呢?一般ThreadLocal都被定義為static,也就是說(shuō),每個(gè)線(xiàn)程只需要?jiǎng)?chuàng)建一份,生命周期跟線(xiàn)程一樣。而局部變量生命周期跟方法與方法一樣,每調(diào)用一次方法,創(chuàng)建一次變量,方法結(jié)束,對(duì)象銷(xiāo)毀。ThreadLocal可以避免一些大對(duì)象的重復(fù)創(chuàng)建銷(xiāo)毀。
ThreadLocalMap的Entry繼承自WeakReference,當(dāng)沒(méi)有其他的強(qiáng)引用指向ThreadLocal變量時(shí),該ThreadLocal變量會(huì)在下次GC中被回收。對(duì)于被回收掉的ThreadLocal變量,不會(huì)顯式地去清理,而是在接下來(lái)的get,set,remove操作中去檢查刪除掉這些無(wú)效ThreadLocal變量所在的Entry,防止可能的內(nèi)存泄漏。