前言
前幾天在京東的同學給我打了個電話,聊了下家常,技術宅的我多嘴問了最近有沒有學啥? 他說最近有點忙,但抽空也看了幾篇博客,他說我考考你吧,我說可以啊,他問我: ThreadLocal 使用不當會導致 OOM 嗎?我不假思索的回答:會。他繼續追問道:為什麼? 我說:因為 ThreadLocal 和操作它的線程綁定在一起,如果操作他的線程不被銷燬,與之關聯的 ThreadLocal 不會被 GC 。因為使用線程大多都是通過線程池來創建的,因此只要該線程活躍,就不會被線程池銷燬,如果我們使用的時候忘記調用 ThreadLocal 的 remove 方法,則 ThreadLocal 保存的值無法被 GC ,如此多了就會發生 OOM 。然後他突然問了一句:為啥 Thread 裡的threadLocals 屬性的key是弱引用類型的? 這個之前我是不知道的。然後他給我解釋了一下,這也是這篇文章的由來,好記性不如爛筆頭,順便驗證一下他說的,也是對知識的鞏固。
ThreadLocal
多個線程間共享變量,可能會造成線程不安全的問題,需要加鎖來實現線程安全,但是加鎖會降低系統的吞吐量。
但是有些變量就不需要線程間共享。比如數據庫連接池裡的連接,我們可以通過串行線程封閉技術來安全的使用連接池中的連接。一個線程A從連接池中把連接拿走,連接池保證不把該連接給別的線程,線程A同樣不會把連接發佈出去,用完之後返回給連接池,這樣一個連接總是在一個線程中使用,不會同時被兩個線程操作。線程A保存數據庫連接就可以使用 ThreadLocal 來保存,可以在多個方法中獲取操作數據庫,用完刪除即可。(生產者和消費者模式也是使用串行線程封閉技術,大家可以考慮下。)
ThreadLocal 裡的數據,其它線程無法訪問,只要使用者不把數據發佈出去,就可以安全操作它們。我們來看看如何一個 demo 來看下 ThreadLocal 如何使用:
public class NotThreadSafe { private ThreadLocal count = ThreadLocal.withInitial(() -> Integer.MIN_VALUE); public void increment() { Integer countValue = count.get(); countValue++; count.set(countValue); } public void decrement() { Integer countValue = count.get(); countValue--; count.set(countValue); } public int getValue() { return count.get(); } public void remove() { count.remove(); } public static void main(String[] args) { NotThreadSafe notThreadSafe = new NotThreadSafe(); new Thread(() -> { try { notThreadSafe.increment(); System.out.println("increment i=" + notThreadSafe.getValue()); notThreadSafe.decrement(); System.out.println("decrement i=" + notThreadSafe.getValue()); } finally { notThreadSafe.remove(); } }).start(); } }
ThreadLocal與Thread如何綁定
上文我說過 ThreadLocal 會與它所屬的 Thread 綁定,這個綁定是什麼意思呢,下面我們來看看 Thread的一處源碼:
/* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null;
註釋 的譯文:與此線程相關的ThreadLocal值。這個映射由ThreadLocal類維護。
ThreadLocal.ThreadLocalMap 類型的 threadLocals 屬性是保存與當前線程相關的ThreadLocal 實例,該map 由ThreadLocal來維護。下面我來看看 ThreadLocalMap 到底是個什麼。
先來看下官方解釋:
ThreadLocalMap 是一個定製的散列映射,只適用於維護線程本地值。沒有任何操作被導出到 ThreadLocal 類之外。類是包私有的,允許在類線程中聲明字段。為了幫助處理非常大且長期存在的用法,哈希表條目對鍵使用 WeakReference 。但是,由於沒有使用引用隊列,所以只有在表空間不足時才會刪除陳舊的條目。
ThreadLocalMap其實就是一個散列表和HashMap差不多,只不過是定製的,只用於維護線程本地的值。為了幫助處理非常大且長期存在的用法,哈希表條目對鍵使用 WeakReference,現在大家比較關心這個散列表的鍵 對應著的是什麼吧?我們來看看ThreadLocalMap 中的Entry是如何定義的:
static class Entry extends WeakReference> { /**與ThreadLocal關聯的值。*/ Object value; //key 就是ThreadLocal 對象本身,而值就是大家想要保存的數據如數據庫連接 Entry(ThreadLocal> k, Object v) { //將k置為弱引用 super(k); value = v; } }
看了源碼可知:ThreadLocalMap是以ThreadLocal 實例為健,用戶要線程私有化的數據為值的散列表,並且健 還是弱引用類型的。
下面我們來講下 ThreadLocal 如何與線程關聯起來的。ThreadLocal 實例在調用 set 和 get 的時候,會先獲取當前線程的threadLocals 屬性,判斷 threadLocals 屬性是否為空,若不為空則進行獲取或者添加操作,否則會創建一個 ThreadLocalMap 實例賦給當前線程的屬性 threadLocals;然後往裡 put 一個鍵值對,當get 或 set 方法時健都是當前ThreadLocal實例,只不過是get時,值為ThreadLocal 中initValue方法返回的值,默認為 null ;方法為set時,則為調用者傳進的實參。
ThreadLocal 的 get方法:
public T get() { Thread t = Thread.currentThread(); //獲取當前線程的 threadLocals 屬性 ThreadLocalMap map = getMap(t); if (map != null) { //若threadLocals屬性不為空,以 this(當前 ThreadLocal)實例為健獲取對應的值 ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { //若已經設置過值或者有初始值就直接返回 @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } //當前線程的threadLocals屬性為空或者沒有設置過值時設置初始值 return setInitialValue(); } /** * 獲取與給定線程相關聯的ThreadLoal散列表 * @param t 當前線程 */ ThreadLocalMap getMap(Thread t) { return t.threadLocals; } private T setInitialValue() { //調用 initialValue 獲取初始值默認為 null T value = initialValue(); Thread t = Thread.currentThread(); //獲取當前線程的 threadLocals 屬性 ThreadLocalMap map = getMap(t); if (map != null) //如果已經創建與當前線程關聯的 ThreadLoal 散列表,則直接設值 map.set(this, value); else //創建與當前線程相關的 ThreadLocal 散列表 並設值 createMap(t, value); return value; } /** * 創建與當前線程關聯的 ThreadLocal 散列表, * 並將它賦值給給定線程的 threadLocals 屬性 * @param t 當前線程 * @param ThreadLocal散列表第一個Entry的初始值 */ void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
ThreadLocal 中的set 方法
/** * 向當前線程的線程私有變量設置指定的值 */ public void set(T value) { Thread t = Thread.currentThread(); //獲取當前線程的 threadLocals 屬性 ThreadLocalMap map = getMap(t); if (map != null) //如果已經創建與當前線程關聯的 ThreadLoal 散列表,則直接設值 map.set(this, value); else //創建與當前線程相關的 ThreadLocal 散列表, 並設值 createMap(t, value); }
下面我們用一張圖來概括下線程,線程私有變量以及用戶定義的數據之間的關係,加深我們的理解:
上圖中 Entry 中的 key 是弱引用類型的,因此用戶程序使用完ThreadLocal 對象之後忘記調用 remove 方法,下一次 GC 會把只有一個弱引用的ThreadLocal 回收掉,此時 key 指向 null,則無論誰都不能訪問到該key 對應的 value 對象,只要線程實例不退出就無法釋放,如果value 對象佔用內存很大,則可能會造成OOM。但是ThreadLocalMap 底層會對 key為 null的value進行清理。我們下一章討論
後記
我們討論了ThreadLocal 如何使用其與 Thread 之間關係,下一節我們討論下 ThreadLocalMap 的具體實現。
關鍵字: Thread ThreadLocal 最近