1. 程式人生 > 實用技巧 >ThreadLocal原始碼分析

ThreadLocal原始碼分析

概述

ThreadLocal提供了一種執行緒安全的資料訪問方式,每個執行緒中都存在一個共享變數副本,從而實現多執行緒狀態下的執行緒安全。

demo

 public static void main(String[] args) {
        final ThreadLocal<Integer> MAIN = ThreadLocal.withInitial(() -> 100);

        MAIN.set(200);

        new Thread(()->{
            System.out.println(Thread.currentThread().getName() + " MAIN:" + MAIN.get());
        }).start();

        System.out.println("MAIN:" + MAIN.get());
        
        //一定要注意,當ThreadLocal不再使用時,一定要呼叫remove方法,以免記憶體洩漏
        MAIN.remove();

        System.out.println("MAIN:" + MAIN.get());

    }

執行之後,列印結果如下:

MAIN:200
Thread-0 MAIN:100

MAIN是在主執行緒中set的值,可以在主執行緒中使用get方法獲得,但線上程中呼叫get方法,結果卻為null,這是為什麼呢?這個原因其實也是為什麼說ThreadLocal能被稱之為執行緒安全的原因。下面我們就通過原始碼來一探究竟。

關鍵屬性

    
    //表示當前ThreadLocal的hashCode,用於計算當前ThreadLocal在ThreadLocalMap中的索引位置
    private final int threadLocalHashCode = nextHashCode();

    // static+ AtomInteger 保證了在一臺機器中每個ThreadLocal的threadLocalHashCode是唯一的
    // 被static修飾十分關鍵,因為一個執行緒在處理業務時,ThreadLocalMap會被set多個ThreadLocal,多個       // ThreadLocal就依靠著threadLocalHashCode進行區分
    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    // 增量常量
    private static final int HASH_INCREMENT = 0x61c88647;

    //計算 ThreadLocal 的 hashCode 值(就是遞增)
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

常用方法

set方法

每個執行緒的set方法都是序列的,因而不會有執行緒安全的問題。

public void set(T value) {
        //獲取當前執行緒
        Thread t = Thread.currentThread();
        // 獲取ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        // 當前的threadLocalMap之前有設定值,則直接進行設定,否則就初始化
        if (map != null)
            map.set(this, value);
        else
            //初始化threadLocalMap
            createMap(t, value);
    }

//獲取執行緒的threadLocalMap屬性
ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

//初始化 ThreadLocalMap
 void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

get方法

get方法主要是從ThreadLocalMap中取出當前ThreadLocal儲存的值。

public T get() {
        //獲取當前執行緒
        Thread t = Thread.currentThread();
        //獲取ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        //map不為null時
        if (map != null) {
            //從map中取出Entry,由於ThreadLocalMap在set時解決hash衝突的策略不同,get的邏輯也不同
            ThreadLocalMap.Entry e = map.getEntry(this);
            //entry不為空的話,讀取當前ThreadLocal中儲存的值
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;、
                    //返回值
                return result;
            }
        }
        //若是map為空 則將當前執行緒的ThreadLocal初始化 並返回初始值null
        return setInitialValue();
    }


private T setInitialValue() {
        //獲取初始值
        T value = initialValue();
       // 獲取當前執行緒
        Thread t = Thread.currentThread();
       // 從當前執行緒中獲取ThreadLocalMap
        ThreadLocalMap map = getMap(t);
       // 如果map不為null的話,直接進行set
        if (map != null)
            map.set(this, value);
        else
            //否則初始化ThreadLocalMap
            createMap(t, value);
     //返回值
        return value;
    }
//直接return null
protected T initialValue() {
        return null;
    }

remove方法

由於ThreadLocal在使用不當時可能存在記憶體洩漏的場景,因而,在使用完ThreadLocal使用完之後,一定要顯示的呼叫remove方法進行清除。

    public void remove() {
         //獲取當前執行緒繫結的ThreadLocalMap
         ThreadLocalMap m = getMap(Thread.currentThread());
        // map不為null的話  
        if (m != null)
            //從map中移除當前threadLocal對應的K-V
             m.remove(this);
     }

可以看出,無論是set、get還是remove 方法,其底層原理都比較簡單,但卻都包含一個共性,就是使用到了ThreadLocalMap,那麼ThreadLocalMap又是一個什麼東東呢?

ThreadLocalMap

ThreadLocalMap是ThreadLocal中的一個靜態內部類,其本質上是一個簡單的Map結構,key是ThreadLocal型別,value是ThreadLocal儲存的值,底層是一個Entry型別陣列組成的資料結構。Entry類的結構如下所示:

static class ThreadLocalMap {

        // Entry繼承自WeakReference 因而Entry陣列中每個Entry節點也是一個弱引用,當沒有引用指向時,
        //會被回收
static class Entry extends WeakReference<ThreadLocal<?>> {
            // 當前ThreadLocal關聯的值
            Object value;
    
            // WeakReference的引用 referent就是ThreadLocal
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

        //初始容量大小
        private static final int INITIAL_CAPACITY = 16;

        // Entry陣列
        private Entry[] table;

        //Entry陣列大小
        private int size = 0;

        //閾值
        private int threshold; // Default to 0

        //設定閾值方法  可以看出是容量的2/3
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }
    
       //計算索引的下一個位置
        private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }
    
       //計算索引的上一個位置
        private static int prevIndex(int i, int len) {
            return ((i - 1 >= 0) ? i - 1 : len - 1);
        }
    
    //構造方法
     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);
        }

.............
    
}

ThreadLocal實現執行緒隔離的原理

ThreadLocal是執行緒安全的,主要是因為ThreadLocalMap是執行緒Thread的一個屬性,如下所示:

threadLocals和inheritableThreadLocals分別是執行緒的兩個屬性,因而每個執行緒的ThreadLocals都是隔離獨享的。在Thread的init方法中,父執行緒在建立子執行緒的情況下,會拷貝inheritableThreadLocals的值,但不會拷貝threadLocals的值,如下所示:

// Thread中的init方法
private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc) {
    ...
        //當父執行緒的inheritableThreadLocals的值不為空時
        // 會把inheritable裡面的值全部傳遞給子執行緒
        if (parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
               //
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
     ...
    }


//ThreadLocal中的createInheritedMap方法
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
        return new ThreadLocalMap(parentMap);
    }

線上程建立時,會把父執行緒的inheritableThreadLocals屬性值進行拷貝。

set方法

  private void set(ThreadLocal<?> key, Object value) {
           //儲存entry陣列
            Entry[] tab = table;
           // 獲取陣列長度
            int len = tab.length;
           // 計算索引下標位置
            int i = key.threadLocalHashCode & (len-1);
            
      // 整體策略:檢視 i 索引位置有沒有值,有值的話,索引位置 + 1,直到找到沒有值的位置
    // 這種解決 hash 衝突的策略,也導致了其在 get 時查詢策略有所不同,體現在 getEntryAfterMiss 中
            for (Entry e = tab[i];
                 e != null;
                 // nextIndex 就是讓在不超過陣列長度的基礎上,把陣列的索引位置 + 1
                 e = tab[i = nextIndex(i, len)]) {
                //獲取threadLocal
                ThreadLocal<?> k = e.get();
                
                //如果二者相等 直接替換並返回
                if (k == key) {
                    e.value = value;
                    return;
                }
                
                //如果為空 說明當前的threadLocal被清理了,直接替換並返回
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
      
            //當前i位置沒有值的話,直接生成一個Entry
            tab[i] = new Entry(key, value);
            // 維護size
            int sz = ++size;
            // 當陣列大小大於等於擴容閾值(陣列大小的三分之二)時,進行擴容
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

需要注意幾點的是:

  • 通過遞增的AtomicInteger作為ThreadLocal的hashCode的;
  • 通過計算hashCode計算的索引位置i處,如果已經有值的話,會從i開始,通過+1,不斷往後尋找,直到找到索引位置為空的地方,把當前ThreadLocal作為key放進去;

getEntry方法

private Entry getEntry(ThreadLocal<?> key) {
            // 計算索引值
            int i = key.threadLocalHashCode & (table.length - 1);
            // 獲取索引處的entry
            Entry e = table[i];
           // e 不為空,並且 e 的 ThreadLocal 的記憶體地址和 key 相同,直接返回,否則就是沒有找到,
           //繼續通過 getEntryAfterMiss 方法找
            if (e != null && e.get() == key)
                return e;
            else
                // 這個取資料的邏輯,是因為 set 時陣列索引位置衝突造成的
                return getEntryAfterMiss(key, i, e);
        }


  private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
           //暫存entry陣列
            Entry[] tab = table;
           // 獲取陣列長度
            int len = tab.length;
      
           //遍歷陣列
            while (e != null) {
                // 記憶體地址一樣,表示找到了 直接返回即可
                ThreadLocal<?> k = e.get();
                if (k == key)
                    return e;
                //如果為null的話 刪除沒用的key
                if (k == null)
                    expungeStaleEntry(i);
                else
                    //否計算出下一個索引的位置
                    i = nextIndex(i, len);
                //繼續下一次遍歷
                e = tab[i];
            }
          //如果最後entry陣列遍歷結束都沒有找到,直接返回null
            return null;
        }

resize方法

        //擴容
        private void resize() {
            //暫存舊的entry陣列
            Entry[] oldTab = table;
            //舊陣列的容量
            int oldLen = oldTab.length;
            //新陣列的容量
            int newLen = oldLen * 2;
            //初始化一個空的新陣列
            Entry[] newTab = new Entry[newLen];
            //記錄數量
            int count = 0;
            
            //開始複製
            for (int j = 0; j < oldLen; ++j) {
                //舊陣列的一個節點
                Entry e = oldTab[j];
                //不為null的話開始進行復制
                if (e != null) {
                    //獲取threadLocal
                    ThreadLocal<?> k = e.get();
                    //為null的話直接進行清除
                    if (k == null) {
                        e.value = null; // Help the GC
                    } else {
                        //計算threadLocal在新entry中的索引位置
                        int h = k.threadLocalHashCode & (newLen - 1);
                        //如果該位置以及有值了,那麼就尋找下一個索引位置,直到為空
                        while (newTab[h] != null)
                            h = nextIndex(h, newLen);
                        //將值拷貝到新陣列的位置
                        newTab[h] = e;
                        //更新數量
                        count++;
                    }
                }
            }
            
            //重新設定擴容時的閾值,新陣列長度的2/3
            setThreshold(newLen);
            //維護size大小
            size = count;
            //將entry陣列的引用指向新陣列
            table = newTab;
        }

擴容時的邏輯也比較清晰:

  • 擴容時新陣列大小為原來的兩倍;
  • 擴容時沒有執行緒安全問題,因為ThreadLocalMap是執行緒本身的一個屬性,一個執行緒同一時刻只能對ThreadLocalMap進行操作,因為同一個執行緒執行業務邏輯時必然是序列的,那麼操作ThreadLocalMap必然也是序列的;

ThreadLocal記憶體洩漏原因探究

在demo演示中我們提到過,在使用ThreadLocal的過程中,如果使用不當,最後可能會導致記憶體洩漏的問題,那麼原因是什麼呢?

先來明確一下記憶體洩漏的概念:

記憶體洩漏主要有兩種情況,一是堆中申請的空間沒有被釋放;二是物件已不再被使用,但仍在記憶體中保留著。

先來看一下ThreadLocal和當前Thread在堆疊中的佈局圖吧。

注:上面的連線線中,實線代表強引用,虛線代表弱引用。

上面我們已經說過了,ThreadLocalMap中的Entry靜態類繼承了WeakReference類,它的Key是ThreadLocal物件的弱引用。那什麼是弱引用呢?根據《深入理解Java虛擬機器 第二版》中的定義:

弱引用是用來描述非必需物件的,被弱引用關聯的物件只能生存到下一次垃圾回收發生之前。當垃圾回收器工作時,無論當前記憶體是否足夠,都會回收掉只被弱引用關聯的物件。

一般情況下,當我們不再使用threadLocal變數時,會手動將該變數置為null,這樣堆中的threadLocal例項物件將不會再被任何強引用所指向,這樣垃圾回收器就可以對其進行回收。此時,根據上圖我們可知,在垃圾回收之後,ThreadLocalMap中的Entry的key已經變成了null。但是,如果此時執行緒還存活著繼續執行,則key為null,但value指向Object(存在著強引用關係)的Entry物件仍然不會被回收,此時就會發生記憶體洩漏。當然,如果執行緒在完成任務之後就結束了生命週期,那麼隨後ThreadLocalMap和Entry也會隨之消亡。但如果使用的是執行緒池,執行緒在完成任務之後會回放到執行緒池中從而繼續被複用,那麼此時value就會一直存在,導致記憶體洩漏。

那麼問題來了,既然弱引用可能導致記憶體洩漏,那麼改為強引用呢?

還是跟上面一樣,一起來分析一下。當手動將threadLocal置為null時,雖然threadLocal Ref—ThreadLocal例項之間沒有引用關係,但Entry中key與ThreadLocal之間仍然存在著強引用關係,就會產生key和value都不為null的Entry物件,但是ThreadLocal我們明明已經不需要了,且只有執行緒一直執行下去,那麼threadLocal例項還是無法被回收,這樣還是會發生記憶體洩漏

因而,雖然弱引用同樣也會導致記憶體洩漏的問題,但ThreadLocal的set、get以及remove操作都會清除ThreadLocalMap中Entry陣列中key為null的Entry,從而降低出現記憶體洩漏的風險。