java基礎之ThreadLocal


早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal為解決多線程程序的並發問題提供了一種新的思路。使用這個工具類可以很簡潔地編寫出優美的多線程程序。ThreadLocal是指作用域為Thread的局部變量,也許把它命名為ThreadLocalVariable更容易讓人理解一些。此博客很多內容參考了(這篇博客https://www.cnblogs.com/fsmly/p/11020641.html).

介紹

多線程訪問同一個共享變量的時候容易出現並發問題,特別是多個線程對一個變量進行寫入的時候,為了保證線程安全,一般使用者在訪問共享變量的時候需要進行額外的同步措施才能保證線程安全性。ThreadLocal是除了加鎖這種同步方式之外的一種保證一種規避多線程訪問出現線程不安全的方法,當我們在創建一個變量后,如果每個線程對其進行訪問的時候訪問的都是線程自己的變量這樣就不會存在線程不安全問題。 ThreadLocal是JDK包提供的,它提供線程本地變量,如果創建一個ThreadLocal變量,那么訪問這個變量的每個線程都會有這個變量的一個副本,在實際多線程操作的時候,操作的是自己本地內存中的變量,從而規避了線程安全問題,如下圖所示:

ThreadLocal示例圖

ThreadLocal使用示例

下面的例子中,開啟兩個線程,在每個線程內部設置了本地變量的值,然后調用print方法打印當前本地變量的值。如果在打印之后調用本地變量的remove方法會刪除本地內存中的變量,代碼如下所示:

package test;

public class ThreadLocalTest {

    static ThreadLocal<String> localVar = new ThreadLocal<>();

    static void print(String str) {
        //打印當前線程中本地內存中本地變量的值
        System.out.println(str + " :" + localVar.get());
        //清除本地內存中的本地變量
        localVar.remove();
    }

    public static void main(String[] args) {
        Thread t1  = new Thread(new Runnable() {
            @Override
            public void run() {
                //設置線程1中本地變量的值
                localVar.set("localVar1");
                //調用打印方法
                print("thread1");
                //打印本地變量
                System.out.println("after remove : " + localVar.get());
            }
        });

        Thread t2  = new Thread(new Runnable() {
            @Override
            public void run() {
                //設置線程1中本地變量的值
                localVar.set("localVar2");
                //調用打印方法
                print("thread2");
                //打印本地變量
                System.out.println("after remove : " + localVar.get());
            }
        });

        t1.start();
        t2.start();
    }
}

ThreadLocal的實現原理

從上一節中我們可以看出,ThreadLocal主要有set和get方法,用於設置和獲取線程中的變量,那么ThreadLocal是怎么實現這個功能的呢?和ThreadLocal實現相關的類主要有三個:ThreadLocal、Thread、ThreadLocalMap,三者之間的關系同樣如下圖所示:

  1. ThreadLocalMap:名字上看是Map,實際上是一個數組,不過它的功能和Map類似,可以按照key查找數據。
  2. Thread:線程大家應該都知道,那么在ThreadLocal中他起什么作用呢?一個Thread中會包含兩個ThreadLocalMap,分別用於存儲本線程和父線程的ThreadLocal數據。每一個ThreadLocal變量會在線程中對應一條ThreadLocalMap的key-value,其中key是ThreadLocal的唯一Hash值。
  3. ThreadLocal:每個ThreadLocal都會有一個唯一的Hash值,用於查找這個ThreadLocal在ThreadLocalMap中的值;ThreadLocal提供了方法用於獲取當前線程的ThreadLocal數據。

ThreadLocal示例圖

數據存放的位置

ThreadLocal只是一層訪問線程數據的殼,ThreadLocal get和set的數據不會在ThreadLocal的實例中存放,而是存放在線程Thread中的ThreadLocalMap,ThreadLocal只是提供了一個訪問這些數據的途徑。

ThreadLoca的set方法將value添加到調用線程的ThreadLocalMap中,當調用線程調用get方法時候能夠從它的ThreadLocalMap中取出變量。如果調用線程一直不終止,那么這個本地變量將會一直存放在他的ThreadLocalMap中,所以不使用本地變量的時候需要調用remove方法將ThreadLocalMap中刪除不用的本地變量。

set方法存放數據

ThreadLocal方法的set可以向當前線程的ThreadLocalMap中放入數據,存放數據的源碼如下所示,Set過程分為以下步驟:

  1. 獲取當前線程。
  2. 從當前線程中獲取ThreadLocalMap變量。
  3. 如果當前線程的ThreadLocalMap不為空,用當前的ThreadLocal為Key,需要存放的數據為Value,存放數據。
  4. 如果當前線程的ThreadLocalMap為空,創建ThreadLocalMap並存放數據。
public void set(T value) {
    //(1)獲取當前線程(調用者線程)
    Thread t = Thread.currentThread();
    //(2)以當前線程作為key值,去查找對應的線程變量,找到對應的map
    ThreadLocalMap map = getMap(t);
    //(3)如果map不為null,就直接添加本地變量,key為當前定義的ThreadLocal變量的this引用,值為添加的本地變量值
    if (map != null)
        map.set(this, value);
    //(4)如果map為null,說明首次添加,需要首先創建出對應的map
    else
        createMap(t, value);
}

get方法獲取數據

ThreadLocal方法的get可以獲取當前線程ThreadLocalMap中存放的數據,獲取存放數據的源碼如下所示,get過程分為以下步驟:

  1. 獲取當前線程
  2. 從當前線程中獲取ThreadLocalMap變量。
  3. 如果ThreadLocalMap變量不為null,就可以在map中查找到本地變量的值。
  4. 如果ThreadLocalMap變量為null,那么就初始化當前線程的ThreadLocalMap。
public T get() {
    //(1)獲取當前線程
    Thread t = Thread.currentThread();
    //(2)獲取當前線程的threadLocals變量
    ThreadLocalMap map = getMap(t);
    //(3)如果threadLocals變量不為null,就可以在map中查找到本地變量的值
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    //(4)執行到此處,threadLocals為null,調用該更改初始化當前線程的threadLocals變量
    return setInitialValue();
}

private T setInitialValue() {
    //protected T initialValue() {return null;}
    T value = initialValue();
    //獲取當前線程
    Thread t = Thread.currentThread();
    //以當前線程作為key值,去查找對應的線程變量,找到對應的map
    ThreadLocalMap map = getMap(t);
    //如果map不為null,就直接添加本地變量,key為當前線程,值為添加的本地變量值
    if (map != null)
        map.set(this, value);
    //如果map為null,說明首次添加,需要首先創建出對應的map
    else
        createMap(t, value);
    return value;
}

ThreadLocal不支持繼承性

同一個ThreadLocal變量在父線程中被設置值后,在子線程中是獲取不到的。(threadLocals中為當前調用線程對應的本地變量,所以二者自然是不能共享的)。

package test;

public class ThreadLocalTest2 {

    //(1)創建ThreadLocal變量
    public static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        //在main線程中添加main線程的本地變量
        threadLocal.set("mainVal");
        //新創建一個子線程
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("子線程中的本地變量值:"+threadLocal.get());
            }
        });
        thread.start();
        //輸出main線程中的本地變量值
        System.out.println("mainx線程中的本地變量值:"+threadLocal.get());
    }
}

InheritableThreadLocal類

在上面說到的ThreadLocal類是不能提供子線程訪問父線程的本地變量的,而InheritableThreadLocal類則可以做到這個功能,下面是該類的源碼,InheritableThreadLocal類繼承了ThreadLocal類,並重寫了childValue、getMap、createMap三個方法。我們接下來分別介紹一下三種方法的用處。

  1. createMap:當線程中不存在ThreadLocalMap變量,但是調用set或者get方法設置值的時候,需要初始化ThreadLocalMap變量時調用該方法。
  2. getMap:需要獲取線程的ThreadLocalMap時調用該方法,這里返回的ThreadLocalMap始終為InheritableThreadLocalMap。
  3. childValue:在創建新線程的時候,如果父線程有ThreadLocalMap變量並且允許inherite ThreadLocalMap,那么程序會復制父線程的InheritableThreadLocal到子線程中,childValue表示在復制過程中如何根據父線程中得到數據生成線程中的數據。

InteritableThreadLocal示例圖


public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    /**
     * Creates an inheritable thread local variable.
     */
    public InheritableThreadLocal() {}

    /**
     * Computes the child's initial value for this inheritable thread-local
     * variable as a function of the parent's value at the time the child
     * thread is created.  This method is called from within the parent
     * thread before the child is started.
     * <p>
     * This method merely returns its input argument, and should be overridden
     * if a different behavior is desired.
     */
    protected T childValue(T parentValue) {
        return parentValue;
    }

    /**
     * Get the map associated with a ThreadLocal.
     */
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    /**
     * Create the map associated with a ThreadLocal.
     */
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

總結:Thread會在構造函數中將父線程的inheritableThreadLocals成員變量的值賦值到新的ThreadLocalMap對象中。返回之后賦值給子線程的inheritableThreadLocals。InheritableThreadLocals類通過重寫getMap和createMap兩個方法將本地變量保存到了具體線程的inheritableThreadLocals變量中,當線程通過InheritableThreadLocals實例的set或者get方法設置變量的時候,就會創建當前線程的inheritableThreadLocals變量。而父線程創建子線程的時候,ThreadLocalMap中的構造函數會將父線程的inheritableThreadLocals中的變量復制一份到子線程的inheritableThreadLocals變量中。

ThreadLocal內存泄漏

通過前面的分析我們知道,ThreadLocal的線程數據是存放在ThreadLocalMap中的,所以如果ThreadLocal出現內存泄漏,那么肯定是ThreadLocalMap中存儲的數據出現了泄露,我們需要看看ThreadLocalMap中的數據結構。ThreadLocalMap的數據結構如下所示,ThreadLocalMap中的數據存儲在一個Entry數組中,Entry中有對ThreadLocal的WeakReference。

InteritableThreadLocal示例圖

什么情況下會出現內存泄露呢?

  1. 當一個線程調用ThreadLocal的set方法設置變量的時候,當前線程的ThreadLocalMap就會存放一個記錄,這個記錄的key值為ThreadLocal的弱引用,value就是通過set設置的值。
  2. 如果當前線程一直存在且沒有調用該ThreadLocal的remove方法,如果這個時候別的地方還有對ThreadLocal的引用,那么當前線程中的ThreadLocalMap中會存在對ThreadLocal變量的引用和value對象的引用,是不會釋放的,就會造成內存泄漏。
  3. 考慮這個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方法避免內存泄漏。

Java中的四種引用類型

上文中我們說到了WeakReference,大家可能對這個詞有點陌生,Java中有四種引用類型:

  1. 強引用:Java中默認的引用類型,一個對象如果具有強引用那么只要這種引用還存在就不會被GC。
  2. 軟引用:簡言之,如果一個對象具有弱引用,在JVM發生OOM之前(即內存充足夠使用),是不會GC這個對象的;只有到JVM內存不足的時候才會GC掉這個對象。軟引用和一個引用隊列聯合使用,如果軟引用所引用的對象被回收之后,該引用就會加入到與之關聯的引用隊列中。
  3. 弱引用(這里討論ThreadLocalMap中的Entry類的重點):如果一個對象只具有弱引用,那么這個對象就會被垃圾回收器GC掉(被弱引用所引用的對象只能生存到下一次GC之前,當發生GC時候,無論當前內存是否足夠,弱引用所引用的對象都會被回收掉)。弱引用也是和一個引用隊列聯合使用,如果弱引用的對象被垃圾回收期回收掉,JVM會將這個引用加入到與之關聯的引用隊列中。若引用的對象可以通過弱引用的get方法得到,當引用的對象唄回收掉之后,再調用get方法就會返回null;
  4. 虛引用:虛引用是所有引用中最弱的一種引用,其存在就是為了將關聯虛引用的對象在被GC掉之后收到一個通知。(不能通過get方法獲得其指向的對象)。

我是御狐神,歡迎大家關注我的微信公眾號:wzm2zsd

qrcode_for_gh_83670e17bbd7_344-2021-09-04-10-55-16

本文最先發布至微信公眾號,版權所有,禁止轉載!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM