多線程編程(3)之ThreadLocal 原理和使用場景

一、什麼是ThreadLocal 

ThreadLocal 是 JDK java.lang 包中的一個用來實現相同線程數據共享不同的線程數據隔離的一個工具。 我們來看下 JDK 源碼中是如何解釋的:

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. 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).

Each thread holds an implicit reference to its copy of a thread-local variable as long as the thread is alive and the ThreadLocal instance is accessible; after a thread goes away, all of its copies of thread-local instances are subject to garbage collection (unless other references to these copies exist).

大致的意思是:

ThreadLocal 這個類提供線程局部變量,這些變量與其他正常的變量的不同之處在於,每一個訪問該變量的線程在其內部都有一個獨立的初始化的變量副本;ThreadLocal 實例變量通常採用private static在類中修飾。

只要 ThreadLocal 的變量能被訪問,並且線程存活,那每個線程都會持有 ThreadLocal 變量的副本。當一個線程結束時,它所持有的所有 ThreadLocal 相對的實例副本都可被回收。

一句話說就是 ThreadLocal 適用於每個線程需要自己獨立的實例且該實例需要在多個方法中被使用(相同線程數據共享),也就是變量在線程間隔離(不同的線程數據隔離)而在方法或類間共享的場景。

二、ThreadLocal 的使用測試

我們先通過兩個例子來看一下 ThreadLocal 的使用。

例子 1 普通變量:

import java.util.concurrent.CountDownLatch;


public class MyStringDemo {
    private String string;

    private String getString() {
        return string;
    }

    private void setString(String string) {
        this.string = string;
    }

    public static void main(String[] args) {
        int threads = 9;
        MyStringDemo demo = new MyStringDemo();
        CountDownLatch countDownLatch = new CountDownLatch(threads);
        for (int i = 0; i < threads; i++) {
            Thread thread = new Thread(() -> {
                demo.setString(Thread.currentThread().getName());
                System.out.println(demo.getString());
                countDownLatch.countDown();
            }, "thread - " + i);
            thread.start();
        }

    }

}

程序的運行的隨機結果如下:

thread - 1
thread - 2
thread - 1
thread - 3
thread - 4
thread - 5
thread - 6
thread - 7
thread - 8

Process finished with exit code 0

從結果我們可以看出多個線程在訪問同一個變量的時候出現的異常,線程間的數據沒有隔離。下面我們來看下采用 ThreadLocal 變量的方式來解決這個問題的例子。

例子 2 ThreadLocal 變量:

import java.util.concurrent.CountDownLatch;


public class MyThreadLocalStringDemo {
    private static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    private String getString() {
        return threadLocal.get();
    }

    private void setString(String string) {
        threadLocal.set(string);
    }

    public static void main(String[] args) {
        int threads = 9;
        MyThreadLocalStringDemo demo = new MyThreadLocalStringDemo();
        CountDownLatch countDownLatch = new CountDownLatch(threads);
        for (int i = 0; i < threads; i++) {
            Thread thread = new Thread(() -> {
                demo.setString(Thread.currentThread().getName());
                System.out.println(demo.getString());
                countDownLatch.countDown();
            }, "thread - " + i);
            thread.start();
        }
    }

}

程序運行結果:

thread - 0
thread - 1
thread - 2
thread - 3
thread - 4
thread - 5
thread - 6
thread - 7
thread - 8

Process finished with exit code 0

從結果來看,這次我們很好的解決了多線程之間數據隔離的問題,十分方便。

這裏可能有的朋友會覺得在例子 1 中我們完全可以通過加鎖來實現這個功能。是的沒錯,加鎖確實可以解決這個問題,但是在這裏我們強調的是線程數據隔離的問題,並不是多線程共享數據的問題。假如我們這裏除了getString() 之外還有很多其他方法也要用到這個 String,這個時候各個方法之間就沒有顯式的數據傳遞過程了,都可以直接中 ThreadLocal 變量中獲取,這纔是 ThreadLocal 的核心,相同線程數據共享不同的線程數據隔離。由於ThreadLocal 是支持泛型的,這裏採用的是存放一個 String 來演示,其實可以存放任何類型,效果都是一樣的。

三、從源碼分析ThreadLocal的實現

在分析源碼前我們明白一個事那就是對象實例與 ThreadLocal 變量的映射關係是由線程 Thread 來維護的,對象實例與 ThreadLocal 變量的映射關係是由線程 Thread 來維護的,對象實例與 ThreadLocal 變量的映射關係是由線程 Thread 來維護的。重要的事情說三遍。換句話說就是對象實例與 ThreadLocal 變量的映射關係是存放的一個 Map 裏面(這個 Map 是個抽象的 Map 並不是 java.util 中的 Map ),而這個 Map 是 Thread 類的一個字段!而真正存放映射關係的 Map 就是 ThreadLocalMap。看到這裏,估計有很多人都會和我一樣有一些疑問

  1. 每個線程的變量副本是怎麼存儲的?

  2. ThreadLocal是如何實現多線程場景下的共享變量副本隔離?

帶着疑問,來看一下ThreadLocal這個類的定義(默認情況下,JDK的源碼都是基於1.8版本)

從ThreadLocal的方法定義來看,還是挺簡單的。就幾個方法:

  • get: 獲取ThreadLocal中當前線程對應的線程局部變量
  • set:設置當前線程的線程局部變量的值
  • remove:將當前線程局部變量的值刪除

另外,還有一個initialValue()方法,在前面的代碼中有演示,作用是返回當前線程局部變量的初始值,這個方法是一個 protected方法,主要是在構造ThreadLocal時用於設置默認的初始值。

3.1set方法的實現

set方法是設置一個線程的局部變量的值,相當於當前線程通過set設置的局部變量的值,只對當前線程可見。

  • Thread.currentThread 獲取當前執行的線程
  • getMap(t) ,根據當前線程得到當前線程的ThreadLocalMap對象,這個對象具體是做什麼的?稍後分析
  • 如果map不爲空,說明當前線程已經構造過ThreadLocalMap,直接將值存儲到map中
  • 如果map爲空,說明是第一次使用,調用 createMap構造

3.2ThreadLocalMap是什麼?

我們來分析一下這句話, ThreadLocalMapmap=getMap(t)獲得一個ThreadLocalMap對象,那這個對象是幹嘛的呢?

其實不用分析,基本上也能猜測出來,Map是一個集合,集合用來存儲數據,那麼在ThreadLocal中,應該就是用來存儲線程的局部變量的。 ThreadLocalMap這個類很關鍵.

ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

t.threadLocals實際上就是訪問Thread類中的ThreadLocalMap這個成員變量

從上面的代碼發現每一個線程都有自己單獨的ThreadLocalMap實例,而對應這個線程的所有本地變量都會保存到這個map內.

3.3ThreadLocalMap是在哪裏構造?

set方法中,有一行代碼 createmap(t,value);,這個方法就是用來構造ThreadLocalMap,從傳入的參數來看,它的實現邏輯基本也能猜出出幾分吧.

/**
     * Create the map associated with a ThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the table.
     */
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }

Threadt 是通過 Thread.currentThread()來獲取的表示當前線程,然後直接通過 newThreadLocalMap將當前線程中的 threadLocals做了初始化 ThreadLocalMap是一個靜態內部類,內部定義了一個Entry對象用來真正存儲數據。

 static class ThreadLocalMap {
 static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }



        /**
         * Construct a new map including all Inheritable ThreadLocals
         * from given parent map. Called only by createInheritedMap.
         *
         * @param parentMap the map associated with parent thread.
         */
        private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];

            for (int j = 0; j < len; j++) {
                Entry e = parentTable[j];
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                    if (key != null) {
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value);
                        int h = key.threadLocalHashCode & (len - 1);
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }

       //省略部分代碼
}

分析到這裏,基本知道了ThreadLocalMap長啥樣了,也知道它是如何構造的?那麼我看到這裏的時候仍然有疑問

  • Entry集成了 WeakReference,這個表示什麼意思?

  • 在構造ThreadLocalMap的時候 newThreadLocalMap(this,firstValue);,key其實是this,this表示當前對象的引用,在當前的案例中,this指的是 ThreadLocal<Integer>local。那麼多個線程對應同一個ThreadLocal實例,怎麼對每一個ThreadLocal對象做區分呢?

3.4解惑WeakReference

weakReference表示弱引用,在Java中有四種引用類型,強引用、弱引用、軟引用、虛引用。

使用弱引用的對象,不會阻止它所指向的對象被垃圾回收器回收。

在Java語言中, 當一個對象o被創建時, 它被放在Heap裏. 當GC運行的時候, 如果發現沒有任何引用指向o, o就會被回收以騰出內存空間. 也就是說, 一個對象被回收, 必須滿足兩個條件:

  • 沒有任何引用指向它
  • GC被運行.

這段代碼中,構造了兩個對象a,b,a是對象DemoA的引用,b是對象DemoB的引用,對象DemoB同時還依賴對象DemoA,那麼這個時候我們認爲從對象DemoB是可以到達對象DemoA的。這種稱爲強可達(strongly reachable)

DemoA a=new Demo ();
DemoB b=new DemoB(a);

如果我們增加一行代碼來將a對象的引用設置爲null,當一個對象不再被其他對象引用的時候,是會被GC回收的,但是對於這個場景來說,即時是a=null,也不可能被回收,因爲DemoB依賴DemoA,這個時候是可能造成內存泄漏的

DemoA a=new Demo ();
DemoB b=new DemoB(a);
a=null;

通過弱引用,有兩個方法可以避免這樣的問題.

對於方法2來說,DemoA只是被弱引用依賴,假設垃圾收集器在某個時間點決定一個對象是弱可達的(weakly reachable)(也就是說當前指向它的全都是弱引用),這時垃圾收集器會清除所有指向該對象的弱引用,然後把這個弱可達對象標記爲可終結(finalizable)的,這樣它隨後就會被回收。

試想一下如果這裏沒有使用弱引用,意味着ThreadLocal的生命週期和線程是強綁定,只要線程沒有銷燬,那麼ThreadLocal一直無法回收。而使用弱引用以後,當ThreadLocal被回收時,由於Entry的key是弱引用,不會影響ThreadLocal的回收防止內存泄漏,同時,在後續的源碼分析中會看到,ThreadLocalMap本身的垃圾清理會用到這一個好處,方便對無效的Entry進行回收。

解惑ThreadLocalMap以this作爲key

在構造ThreadLocalMap時,使用this作爲key來存儲,那麼對於同一個ThreadLocal對象,如果同一個Thread中存儲了多個值,是如何來區分存儲的呢?

答案就在 firstKey.threadLocalHashCode&(INITIAL_CAPACITY-1)

/**
         * Construct a new map initially containing (firstKey, firstValue).
         * ThreadLocalMaps are constructed lazily, so we only create
         * one when we have at least one entry to put in it.
         */
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

關鍵點是 threadLocalHashCode,它相當於一個ThreadLocal的ID,實現的邏輯如下

private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

這裏用到了一個非常完美的散列算法,可以簡單理解爲,對於同一個ThreadLocal下的多個線程來說,當任意線程調用set方法存入一個數據到Entry中的時候,其實會根據 threadLocalHashCode生成一個唯一的id標識對應這個數據,存儲在Entry數據下標中。

remove方法

remove的方法比較簡單,從Entry[]中刪除指定的key就行。

private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

四、ThreadLocal的應用場景及問題

ThreadLocal的實際應用場景:

  1. 比如在線程級別,維護session,維護用戶登錄信息userID(登陸時插入,多個地方獲取)

  2. 數據庫的鏈接對象 Connection,可以通過ThreadLocal來做隔離避免線程安全問題

4.1問題

ThreadLocal的內存泄漏。

ThreadLocalMap中Entry的key使用的是ThreadLocal的弱引用,如果一個ThreadLocal沒有外部強引用,當系統執行GC時,這個ThreadLocal勢必會被回收,這樣一來,ThreadLocalMap中就會出現一個key爲null的Entry,而這個key=null的Entry是無法訪問的,當這個線程一直沒有結束的話,那麼就會存在一條強引用鏈。

Thread Ref - > Thread -> ThreadLocalMap - > Entry -> value 永遠無法回收而造成內存泄漏。

其實我們從源碼分析可以看到,ThreadLocalMap是做了防護措施的:

  • 首先從ThreadLocal的直接索引位置(通過ThreadLocal.threadLocalHashCode & (len-1)運算得到)獲取Entry e,如果e不爲null並且key相同則返回e。

  • 如果e爲null或者key不一致則向下一個位置查詢,如果下一個位置的key和當前需要查詢的key相等,則返回對應的Entry,否則,如果key值爲null,則擦除該位置的Entry,否則繼續向下一個位置查詢。

在這個過程中遇到的key爲null的Entry都會被擦除,那麼Entry內的value也就沒有強引用鏈,自然會被回收。仔細研究代碼可以發現,set操作也有類似的思想,將key爲null的這些Entry都刪除,防止內存泄露。

但是這個設計一來與一個前提條件,就是調用get或者set方法,但是不是所有場景都會滿足這個場景的,所以爲了避免這類的問題,我們可以在合適的位置手動調用ThreadLocal的remove函數刪除不需要的ThreadLocal,防止出現內存泄漏。

所以建議的使用方法是:

  • 將ThreadLocal變量定義成private static的,這樣的話ThreadLocal的生命週期就更長,由於一直存在ThreadLocal的強引用,所以ThreadLocal也就不會被回收,也就能保證任何時候都能根據ThreadLocal的弱引用訪問到Entry的value值,然後remove它,防止內存泄露。
  • 每次使用完ThreadLocal,都調用它的remove()方法,清除數據。

五、小結

在平時忙碌的工作中我們經常解決的是一個業務的需求,往往很少會涉及到底層的源碼或者框架的具體實現代碼。 其實這是很不好的,其實很多的東西的原理都是一樣的,我們需要經常去看一下源碼,瞭解一些底層的實現,不能總是停留在表層,代碼看到多了,才能寫出好的代碼,並且還能學到很多東西。 隨着我們知道的越來越多,我們會發現我們不知道的也越來越多。加油,共勉!

 

 

 

 

 

 

 

 

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