ThreadLocal 你真的會用嗎?

前言

  前幾天在京東的同學給我打了個電話,聊了下家常,技術宅的我多嘴問了最近有沒有學啥? 他說最近有點忙,但抽空也看了幾篇博客,他說我考考你吧,我說可以啊,他問我: 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);
 }

  下面我們用一張圖來概括下線程,線程私有變量以及用戶定義的數據之間的關係,加深我們的理解:

ThreadLocal 你真的會用嗎?

線程,線程私有變量以及用戶定義的數據之間的關係

上圖中 Entry 中的 key 是弱引用類型的,因此用戶程序使用完ThreadLocal 對象之後忘記調用 remove 方法,下一次 GC 會把只有一個弱引用的ThreadLocal 回收掉,此時 key 指向 null,則無論誰都不能訪問到該key 對應的 value 對象,只要線程實例不退出就無法釋放,如果value 對象佔用內存很大,則可能會造成OOM。但是ThreadLocalMap 底層會對 key為 null的value進行清理。我們下一章討論

後記

   我們討論了ThreadLocal 如何使用其與 Thread 之間關係,下一節我們討論下 ThreadLocalMap 的具體實現。


分享到:


相關文章: