多線程中的ThreadLocal

1.ThreadLocal概述

多線程的併發問題主要存在於多個線程對於同一個變量進行修改產生的數據不一致的問題,同一個變量指的值同一個對象的成員變量或者是同一個類的靜態變量。之前我們常聽過儘量不要使用靜態變量,會引起併發問題,那麼隨着Spring框架的深入人心,單例中的成員變量也出現了多線程併發問題。Struts2接受參數採用成員變量自動封裝,爲此在Spring的配置採用多例模式,而SpringMVC將Spring的容器化發揮到極致,將接受的參數放到了註解和方法的參數中,從而避免了單例出現的線程問題。今天,我們討論的是JDK從1.2就出現的一個併發工具類ThreadLocal,他除了加鎖這種同步方式之外的另一種保證一種規避多線程訪問出現線程不安全的方法,當我們在創建一個變量後,如果每個線程對其進行訪問的時候訪問的都是線程自己的變量這樣就不會存在線程不安全問題。我們先看一下官方是怎麼解釋這個變量的?

大致意思是:此類提供了局部變量表。這些變量與普通變量不同不同之處是,每一個通過get或者set方法訪問一個線程都是他自己的,將變量的副本獨立初始化。ThreadLocal實例通常作用於希望將狀態與線程關聯的類中的私有靜態字段(例如,用戶ID或交易ID)。

只要線程是活動的並且可以訪問{@code ThreadLocal}實例, 每個線程都會對其線程局部變量的副本保留隱式引用。 線程消失後,其線程本地實例的所有副本都將進行垃圾回收(除非存在對這些副本的其他引用)。也就是說,如果創建一個ThreadLocal變量,那麼訪問這個變量的每個線程都會有這個變量的一個副本,在實際多線程操作的時候,操作的是自己本地內存中的變量,從而規避了線程安全問題。而每個線程的副本全部放到ThreadLocalMap中。

2. ThreadLocal簡單實用

public class ThreadLocalExample {
    public static class MyRunnable implements Runnable {
        private ThreadLocal<Double> threadLocal = new ThreadLocal();
        private Double variable;

        @Override
        public void run() {
            threadLocal.set(Math.floor(Math.random() * 100D));
            variable = Math.floor(Math.random() * 100D);
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
            }

            System.out.println("ThreadValue==>"+threadLocal.get());
            System.out.println("Variable==>"+variable);
        }
    }
    
    public static void main(String[] args) {
        MyRunnable sharedRunnableInstance = new MyRunnable();
        Thread thread1 = new Thread(sharedRunnableInstance);
        Thread thread2 = new Thread(sharedRunnableInstance);
        thread1.start();
        thread2.start();
    }

}

通過上面的例子,我們發現將Double放入ThreadLocal中,不會出現多線程併發問題,而成員變量variable卻發生了多線程併發問題。

3.ThreadLocal的內部原理

通過源碼我們發現ThreadLocal主要提供了下面五個方法:

/**
  * Returns the value in the current thread's copy of this
  * thread-local variable.  If the variable has no value for the
  * current thread, it is first initialized to the value returned
  * by an invocation of the {@link #initialValue} method.
  * 
  * 返回此線程局部變量的當前線程副本中的值。
  * 如果該變量沒有當前線程的值,則首先將其初始化爲調用{@link #initialValue}方法返回的值。
  * @return the current thread's value of this thread-local
  */
public T get() { }

/**
  * Sets the current thread's copy of this thread-local variable
  * to the specified value.  Most subclasses will have no need to
  * override this method, relying solely on the {@link #initialValue}
  * method to set the values of thread-locals.
  *
  * 將此線程局部變量的當前線程副本設置爲指定值。
  * 大多數子類將不需要重寫此方法,而僅依靠{@link #initialValue}方法來設置線程局部變量的值。
  *
  * @param value the value to be stored in the current thread's copy of
  *              this thread-local.
  *              要存儲在此本地線程的當前線程副本中的值。
  */
public void set(T value) { }

/**
  * Removes the current thread's value for this thread-local
  * variable.  If this thread-local variable is subsequently
  * {@linkplain #get read} by the current thread, its value will be
  * reinitialized by invoking its {@link #initialValue} method,
  * unless its value is {@linkplain #set set} by the current thread
  * in the interim.  This may result in multiple invocations of the
  * {@code initialValue} method in the current thread.
  * 刪除此線程局部變量的當前線程值。
  * 如果此線程局部變量隨後被當前線程{@linkplain #get read}調用,
  * 則其值將通過調用其{@link #initialValue}方法來重新初始化,
  * 除非當前值是在此期間被設置{@linkplain #set set}。
  * 這可能會導致在當前線程中多次調用{@code initialValue}方法。
  * @since 1.5
  */
public void remove() { }

/**
  * Returns the current thread's "initial value" for this
  * thread-local variable.  This method will be invoked the first
  * time a thread accesses the variable with the {@link #get}
  * method, unless the thread previously invoked the {@link #set}
  * method, in which case the {@code initialValue} method will not
  * be invoked for the thread.  Normally, this method is invoked at
  * most once per thread, but it may be invoked again in case of
  * subsequent invocations of {@link #remove} followed by {@link #get}.
  * 返回此線程局部變量的當前線程的“初始值”。
  * 除非線程先前調用了{@link #set}方法,
  * 否則線程第一次使用{@link #get}方法訪問該變量時將調用此方法,
  * 在這種情況下,{@ code initialValue}方法將不會爲線程被調用。
  * 通常,每個線程最多調用一次此方法,
  * 但是在隨後調用{@link #remove}之後再調用{@link #get}的情況下,可以再次調用此方法。
  *
  * <p>This implementation simply returns {@code null}; if the
  * programmer desires thread-local variables to have an initial
  * value other than {@code null}, {@code ThreadLocal} must be
  * subclassed, and this method overridden.  Typically, an
  * anonymous inner class will be used.
  * 此實現僅返回{@code null};如果程序員希望線程局部變量的初始值不是{@code null},
  * 則必須將{@code ThreadLocal}子類化,並重寫此方法。通常,將使用匿名內部類。
  *
  * @return the initial value for this thread-local
  */
protected T initialValue(){ }

3.1 get方法

 public T get() {
    //獲取當前線程
    Thread t = Thread.currentThread();
    //通過當前線程獲取ThreadLocalMap
    //Thread類中包含一個ThreadLocalMap的成員變量
    ThreadLocalMap map = getMap(t);
    //如果不爲空,則通過ThreadLocalMap中獲取對應value值
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    //如果爲空,需要初始化值
    return setInitialValue();
}
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;
}

首先是取得當前線程,然後通過getMap(t)方法獲取到一個map,map的類型爲ThreadLocalMap。然後接着下面獲取到<key,value>鍵值對,注意這裏獲取鍵值對傳進去的是 this,而不是當前線程t。 如果獲取成功,則返回value值。如果map爲空,則調用setInitialValue方法返回value。

在setInitialValue方法中,首先執行了initialValue方法(我們上面提到的最後一個方法),接着通過當前線程獲取ThreadLocalMap,如果不存在則創建。創建的代碼很簡單,只是通過ThreadLocal對象和設置的Value值創建ThreadLocalMap對象。

3.2 set方法

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

這個方法和setInitialValue方法的業務邏輯基本相同,只不過setInitialValue調用了initialValue()的鉤子方法。這裏代碼簡單,我們就不做過多解釋。

3.3 remove方法

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

這個方法是從jdk1.5纔出現的。處理邏輯也很很簡單。通過當前線程獲取到ThreadLocalMap對象,然後移除此ThreadLocal。

3.4 initialValue方法

protected T initialValue() {
    return null;
}

是不是感覺簡單了,什麼也沒有處理,直接返回一個null,那麼何必如此設計呢?當我們發現他的修飾符就會發現,他應該是一個鉤子方法,主要用於提供子類實現的。追溯到源碼中我們發現,Supplier的影子,這就是和jdk8的lamda表達式關聯上了。

static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
    private final Supplier<? extends T> supplier;

    SuppliedThreadLocal(Supplier<? extends T> supplier) {
        this.supplier = Objects.requireNonNull(supplier);
    }
    
    @Override
    protected T initialValue() {
        return supplier.get();
    }
}

4. 總結

在每個線程Thread內部有一個ThreadLocal.ThreadLocalMap類型的成員變量threadLocals,這個threadLocals就是用來存儲實際的變量副本的,鍵值爲當前ThreadLocal變量,value爲變量副本(即T類型的變量)。之所以這裏是一個map,是因爲通過線程會存在多個類中定義ThreadLocal的成員變量。初始時,在Thread裏面,threadLocals爲空,當通過ThreadLocal變量調用get()方法或者set()方法,就會對Thread類中的threadLocals進行初始化,並且以當前ThreadLocal變量爲鍵值,以ThreadLocal要保存的副本變量爲value,存到threadLocals; 然後在當前線程裏面,如果要使用副本變量,就可以通過get方法在threadLocals裏面查找。

5. ThreadLocalMap引發的內存泄漏

ThreadLocal屬於一個工具類,他爲用戶提供get、set、remove接口操作實際存放本地變量的threadLocals(調用線程的成員變量),也知道threadLocals是一個ThreadLocalMap類型的變量。下面我們來看看ThreadLocalMap這個類的一個entry:

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object val
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

public WeakReference(T referent) {
    super(referent); //referent:ThreadLocal的引用
}

//Reference構造方法     
Reference(T referent) {
    this(referent, null);//referent:ThreadLocal的引用
}

Reference(T referent, ReferenceQueue<? super T> queue) {
    this.referent = referent;
    this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}

在上面的代碼中,我們可以看出,當前ThreadLocal的引用k被傳遞給WeakReference的構造函數,所以ThreadLocalMap中的key爲ThreadLocal的弱引用。當一個線程調用ThreadLocal的set方法設置變量的時候,當前線程的ThreadLocalMap就會存放一個記錄,這個記錄的key值爲ThreadLocal的弱引用,value就是通過set設置的值。如果當前線程一直存在且沒有調用該ThreadLocal的remove方法,如果這個時候別的地方還有對ThreadLocal的引用,那麼當前線程中的ThreadLocalMap中會存在對ThreadLocal變量的引用和value對象的引用,是不會釋放的,就會造成內存泄漏。

  考慮這個ThreadLocal變量沒有其他強依賴,如果當前線程還存在,由於線程的ThreadLocalMap裏面的key是弱引用,所以當前線程的ThreadLocalMap裏面的ThreadLocal變量的弱引用在gc的時候就被回收,但是對應的value還是存在的這就可能造成內存泄漏(因爲這個時候ThreadLocalMap會存在key爲null但是value不爲null的entry項)。

  總結:THreadLocalMap中的Entry的key使用的是ThreadLocal對象的弱引用,在沒有其他地方對ThreadLoca依賴,ThreadLocalMap中的ThreadLocal對象就會被回收掉,但是對應的不會被回收,這個時候Map中就可能存在key爲null但是value不爲null的項,這需要實際的時候使用完畢及時調用remove方法避免內存泄漏。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章