1. 程式人生 > >深入剖析ThreadLocal實現原理以及記憶體洩漏問題

深入剖析ThreadLocal實現原理以及記憶體洩漏問題

一、概述

在2017京東校園招聘筆試題中遇到了描述ThreadLocal的實現原理和記憶體洩漏的問題,之前看過ThreadLocal的實現原理,但是網上有很多文章將的很亂,其中有很多文章將ThreadLocal與執行緒同步機制混為一談,特別注意的是ThreadLocal與執行緒同步無關,並不是為了解決多執行緒共享變數問題!
ThreadLocal官網解釋:

  This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one
(via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g.,a user ID or Transaction ID)

->翻譯過來的大概意思就是:ThreadLocal類用來提供執行緒內部的區域性變數。這些變數在多執行緒環境下訪問(通過get或set方法訪問)時能保證各個執行緒裡的變數相對獨立於其他執行緒內的變數,ThreadLocal例項通常來說都是private static型別。
總結:ThreadLocal不是為了解決多執行緒訪問共享變數,而是為每個執行緒建立一個單獨的變數副本,提供了保持物件的方法和避免參數傳遞的複雜性。

ThreadLocal的主要應用場景為按執行緒多例項(每個執行緒對應一個例項)的物件的訪問,並且這個物件很多地方都要用到。例如:同一個網站登入使用者,每個使用者伺服器會為其開一個執行緒,每個執行緒中建立一個ThreadLocal,裡面存使用者基本資訊等,在很多頁面跳轉時,會顯示使用者資訊或者得到使用者的一些資訊等頻繁操作,這樣多執行緒之間並沒有聯絡而且當前執行緒也可以及時獲取想要的資料。

二、實現原理

ThreadLocal可以看做是一個容器,容器裡面存放著屬於當前執行緒的變數。ThreadLocal類提供了四個對外開放的介面方法,這也是使用者操作ThreadLocal類的基本方法:
(1) void set(Object value)設定當前執行緒的執行緒區域性變數的值。
(2) public Object get()該方法返回當前執行緒所對應的執行緒區域性變數。
(3) public void remove()將當前執行緒區域性變數的值刪除,目的是為了減少記憶體的佔用,該方法是JDK 5.0新增的方法。需要指出的是,當執行緒結束後,對應該執行緒的區域性變數將自動被垃圾回收,所以顯式呼叫該方法清除執行緒的區域性變數並不是必須的操作,但它可以加快記憶體回收的速度。
(4) protected Object initialValue()返回該執行緒區域性變數的初始值,該方法是一個protected的方法,顯然是為了讓子類覆蓋而設計的。這個方法是一個延遲呼叫方法,線上程第1次呼叫get()或set(Object)時才執行,並且僅執行1次,ThreadLocal中的預設實現直接返回一個null。

可以通過上述的幾個方法實現ThreadLocal中變數的訪問,資料設定,初始化以及刪除區域性變數,那ThreadLocal內部是如何為每一個執行緒維護變數副本的呢?

其實在ThreadLocal類中有一個靜態內部類ThreadLocalMap(其類似於Map),用鍵值對的形式儲存每一個執行緒的變數副本,ThreadLocalMap中元素的key為當前ThreadLocal物件,而value對應執行緒的變數副本,每個執行緒可能存在多個ThreadLocal。

原始碼:

/**
 Returns the value in the current thread's copy of this
 thread-local variable.  If the variable has no value for thecurrent thread, it is first initialized to the value returned by an invocation of the {@link #initialValue} method.
  @return the current thread's value of this thread-local
 */
public T get() {
    Thread t = Thread.currentThread();//當前執行緒
    ThreadLocalMap map = getMap(t);//獲取當前執行緒對應的ThreadLocalMap
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);//獲取對應ThreadLocal的變數值
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();//若當前執行緒還未建立ThreadLocalMap,則返回呼叫此方法並在其中呼叫createMap方法進行建立並返回初始值。
}
//設定變數的值
public void set(T value) {
   Thread t = Thread.currentThread();
   ThreadLocalMap map = getMap(t);
   if (map != null)
       map.set(this, value);
   else
       createMap(t, value);
}
private T setInitialValue() {
   T value = initialValue();
   Thread t = Thread.currentThread();
   ThreadLocalMap map = getMap(t);
   if (map != null)
       map.set(this, value);
   else
       createMap(t, value);
   return value;
}
/**
為當前執行緒建立一個ThreadLocalMap的threadlocals,並將第一個值存入到當前map中
@param t the current thread
@param firstValue value for the initial entry of the map
*/
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
//刪除當前執行緒中ThreadLocalMap對應的ThreadLocal
public void remove() {
       ThreadLocalMap m = getMap(Thread.currentThread());
       if (m != null)
           m.remove(this);
}

上述是在ThreadLocal類中的幾個主要的方法,他們的核心都是對其內部類ThreadLocalMap進行操作,下面看一下該類的原始碼:

static class ThreadLocalMap {
  //map中的每個節點Entry,其鍵key是ThreadLocal並且還是弱引用,這也導致了後續會產生記憶體洩漏問題的原因。
 static class Entry extends WeakReference<ThreadLocal<?>> {
           Object value;
           Entry(ThreadLocal<?> k, Object v) {
               super(k);
               value = v;
   }
    /**
     * 初始化容量為16,以為對其擴充也必須是2的指數 
     */
    private static final int INITIAL_CAPACITY = 16;
    /**
     * 真正用於儲存執行緒的每個ThreadLocal的陣列,將ThreadLocal和其對應的值包裝為一個Entry。
     */
    private Entry[] table;


    ///....其他的方法和操作都和map的類似
}

總之,為不同執行緒建立不同的ThreadLocalMap,用執行緒本身為區分點,每個執行緒之間其實沒有任何的聯絡,說是說存放了變數的副本,其實可以理解為為每個執行緒單獨new了一個物件。

三、記憶體洩漏問題(參考其他博文)

  在上面提到過,每個thread中都存在一個map, map的型別是ThreadLocal.ThreadLocalMap. Map中的key為一個threadlocal例項. 這個Map的確使用了弱引用,不過弱引用只是針對key. 每個key都弱引用指向threadlocal. 當把threadlocal例項置為null以後,沒有任何強引用指向threadlocal例項,所以threadlocal將會被gc回收. 但是,我們的value卻不能回收,因為存在一條從current thread連線過來的強引用. 只有當前thread結束以後, current thread就不會存在棧中,強引用斷開, Current Thread, Map, value將全部被GC回收.
  所以得出一個結論就是隻要這個執行緒物件被gc回收,就不會出現記憶體洩露,但在threadLocal設為null和執行緒結束這段時間不會被回收的,就發生了我們認為的記憶體洩露。其實這是一個對概念理解的不一致,也沒什麼好爭論的。最要命的是執行緒物件不被回收的情況,這就發生了真正意義上的記憶體洩露。比如使用執行緒池的時候,執行緒結束是不會銷燬的,會再次使用的。就可能出現記憶體洩露。