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

ThreadLocal原理分析

概述

  ThreadLocal是面試非常高頻的問題,在很多框架原始碼中都可以看到他的身影,比如Spring,ReentrantReadWriteLock,然後在平時的工作使用的卻並不多,ThreadLocal要解決並不是多執行緒修改共享變數保證執行緒安全的問題,這個是通過悲觀鎖(比如synchronized)或者樂觀鎖(比如CAS)實現的,它要解決的問題是多執行緒環境下修改變數,每個執行緒修改自己的變數副本,執行緒之間互相不影響的問題。本文就介紹一下ThreadLocal是如何實現執行緒之間隔離的。

舉例

為了方便ThreadLocal的理解,這裡先舉一個ThreadLocal的使用小例子,通過例子來分析它的原理。

public class SeqCount {
   // 一般使用private static修飾
    private static ThreadLocal<Integer> seqCount = new ThreadLocal<Integer>(){
        // 實現initialValue()
        public Integer initialValue() {
            return 0;
        }
    };

    public int nextSeq(){
        seqCount.set(seqCount.get() 
+ 1); return seqCount.get(); } public static void main(String[] args){ SeqCount seqCount = new SeqCount(); SeqThread thread1 = new SeqThread(seqCount); SeqThread thread2 = new SeqThread(seqCount); SeqThread thread3 = new SeqThread(seqCount); SeqThread thread4
= new SeqThread(seqCount); thread1.start(); thread2.start(); thread3.start(); thread4.start(); } private static class SeqThread extends Thread{ private SeqCount seqCount; SeqThread(SeqCount seqCount){ this.seqCount = seqCount; } public void run() { for(int i = 0 ; i < 3 ; i++){ System.out.println(Thread.currentThread().getName() + " seqCount值為 :" + seqCount.nextSeq()); } } } }

執行結果

Thread-0 seqCount值為 :1
Thread-0 seqCount值為 :2
Thread-0 seqCount值為 :3
Thread-1 seqCount值為 :1
Thread-1 seqCount值為 :2
Thread-1 seqCount值為 :3
Thread-2 seqCount值為 :1
Thread-2 seqCount值為 :2
Thread-2 seqCount值為 :3
Thread-3 seqCount值為 :1
Thread-3 seqCount值為 :2
Thread-3 seqCount值為 :3

為了對比,把上面的例子修改一下,不使用ThreadLocal看一下執行結果是怎麼樣的。

public class SeqCount1 {
    private static AtomicInteger seqCount1 = new AtomicInteger(0);

    public int nextSeq(){
        return seqCount1.incrementAndGet();
    }

    public static void main(String[] args){
        SeqCount1 seqCount = new SeqCount1();

        SeqThread thread1 = new SeqThread(seqCount);
        SeqThread thread2 = new SeqThread(seqCount);
        SeqThread thread3 = new SeqThread(seqCount);
        SeqThread thread4 = new SeqThread(seqCount);

        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }

    private static class SeqThread extends Thread{
        private SeqCount1 seqCount;

        SeqThread(SeqCount1 seqCount){
            this.seqCount = seqCount;
        }

        public void run() {
            for(int i = 0 ; i < 3 ; i++){
                System.out.println(Thread.currentThread().getName() + " seqCount值為 :" + seqCount.nextSeq());
            }
        }
    }
}

執行結果為:

Thread-0 seqCount值為 :1
Thread-0 seqCount值為 :2
Thread-0 seqCount值為 :3
Thread-1 seqCount值為 :4
Thread-1 seqCount值為 :5
Thread-1 seqCount值為 :6
Thread-2 seqCount值為 :7
Thread-2 seqCount值為 :9
Thread-2 seqCount值為 :10
Thread-3 seqCount值為 :8
Thread-3 seqCount值為 :11
Thread-3 seqCount值為 :12

通過上面兩個例子大家可以清楚的看到,不使用ThreadLocal就變成了一個執行緒同步的問題,而使用了ThreadLocal之後執行緒之間就沒有協作的問題,而是每個執行緒修改自己的變數副本,變數變成了執行緒內部私有的變數。

ThreadLocalMap

  在上面的例子中,大家會發現使用ThreadLocal的get()、set()方法,而這些方法最後要操作就是ThreadLocalMap,所以這裡先介紹一下這個東東,這個map是聯絡ThreadLocal和Thread的橋樑,當分析完這個map,大家對Thread,ThreadLocal,ThreadLocalMap之間的關係就會變得非常清晰。

ThreadLocalMap屬性分析

//ThreadLocalMap是通過Entry實現的key-value儲存
static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
//ThreadLocalMap初始容量
private static final int INITIAL_CAPACITY = 16;
//儲存Entry的陣列
private Entry[] table;
//ThreadLocalMap中元素個數
private int size = 0;
//ThreadLocalMap的負載因子
private int threshold;

針對上面的屬性,做下面幾點解讀:

  1. 看過HashMap原始碼的應該有印象,HashMap實現了Map介面,而且在Map介面中也有一個Entry介面,HashMap是通過Node來儲存key-value的,Node實現了Entry介面。ThreadLocalMap卻完全不同,它既沒有實現Map介面,在Entry中也沒有類似next的指標指向下一個節點,說明ThreadLocalMap中沒有使用連結串列,就直接儲存在陣列上,除此之外,ThreadLocalMap是ThreadLocal的內部類,沒有使用public修飾,預設是隻有當前包下面的類才可以使用,也就是說這個Map我們自己寫的程式碼中是不能直接建立的。
  2. Entry中的key就是ThreadLocal,而且這個ThreadLocal還被WeakReference包裝了一下,也就是說ThreadLocal在這裡是弱引用,如果ThreadLocal為null,可以直接被gc垃圾回收,關於弱引用,後面會舉一個簡單的例子,大家看一下即可。具體可以參考:用弱引用堵住記憶體洩漏
  3. 下面幾個屬性和HashMap中類似,這裡有意思的一點是HashMap的負載因子是0.75,而ThreadLocalMap的負載因子是2/3。
弱引用使用舉例
public class FinalizeTest {
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize methode executed");
    }
    public static void main(String[] args) {
        FinalizeTest finalizeTest = new FinalizeTest();
        WeakReference<FinalizeTest> weak = new WeakReference(finalizeTest);

        Map<WeakReference<FinalizeTest>,Integer> map = new HashMap<>();
        map.put(weak,1);
        System.out.println("====第一次gc");
        System.gc();
        finalizeTest = null;
        System.out.println("====第二次gc");
        System.gc();
    }
}

執行結果

====第一次gc
====第二次gc
finalize methode executed

這裡為了模擬ThreadLocalMap,也搞了一個Map,這個map的key也是一個使用WeakReference包裝的類,事實上這個map中key的引用並沒有影響gc垃圾回收,只要將物件finalizeTest設定為null,就可以正常垃圾回收,所以ThreadLocalMap中Entry節點的key的垃圾回收也是如此。ThreadLocalMap使用弱引用是為了解決記憶體洩漏的問題,至於什麼是記憶體洩漏,參考:對ThreadLocal實現原理的一點思考

ThreadLocalMap構造方法分析

      ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
       //初始化陣列,容量大小為16
            table = new Entry[INITIAL_CAPACITY];
       //通過key的hash值和15做與運算得到桶的位置
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
       //將key-value封裝到Entry中插入陣列
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
       //設定閾值,達到這個閾值就擴容,閾值為16 * (2/3),當然這裡要取整
            setThreshold(INITIAL_CAPACITY);
        }

構造方法很簡單,就不過多介紹了。

ThreadLocalMap常用方法分析

//由於ThreadLocalMap不像HashMap,發生Hash衝突時使用連結串列解決,ThreadLocalMap的做法就是發生hash衝突
//會找當前位置的下一個桶
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);
        }
private Entry getEntry(ThreadLocal<?> key) {
       //確定桶的位置
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
       //如果找的位置entry不為null,並且entry正好是要找的key,就返回
            if (e != null && e.get() == key)
                return e;
            else
          //這一步其實就是發生了hash衝突,本來應該是這個key佔用的位置,卻被別的key給佔用了
          //所以這裡就要去陣列挨個找了
                return getEntryAfterMiss(key, i, e);
        }

//通過key的hash定位到桶中entry,entry中的key和自己的key不相同,就會呼叫這個方法
//引數中的i就是key通過hash定位到在桶中的位置
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;

            while (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == key)
                    return e;
                if (k == null)                        
                    //在這個方法中會把key對應的value給置為null,同時將entry移除
                    expungeStaleEntry(i);
                else
            //如果當前桶中的entry不符合,就找後一個節點

                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }
//向map中插入元素
private void set(ThreadLocal<?> key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
          //這裡使用了一個for迴圈,尋找定位到的桶,如果定位到的桶中有元素
            //就尋找該桶之後沒有存放元素的桶用來存放當前的key
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
          //如果key重複,用新的value覆蓋舊的value
                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {              
                    //在這個方法中會檢測key是否為null,如果為null就把value也置為null                       
                    //同時移除Entry節點
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

private void resize() {
            Entry[] oldTab = table;
            int oldLen = oldTab.length;
            //擴容成原來的2倍
            int newLen = oldLen * 2;
            Entry[] newTab = new Entry[newLen];
            int count = 0;
       //將舊陣列中的元素賦值到新陣列中
            for (int j = 0; j < oldLen; ++j) {
                Entry e = oldTab[j];
                if (e != null) {
                    ThreadLocal<?> k = e.get();
                    if (k == null) {
                   //上面介紹key通過弱引用包裝,可以正常GC,但是value沒有使用弱引用
                        //所以在key被垃圾回收之後,value並不會被回收,所以這裡手動設定為null
                        //為了幫助垃圾回收
                        e.value = null; // Help the GC
                    } else {
                        //重新定位元素在新陣列中的位置
                        int h = k.threadLocalHashCode & (newLen - 1);
                        while (newTab[h] != null)
                            h = nextIndex(h, newLen);
                        newTab[h] = e;
                        count++;
                    }
                }
            }

            setThreshold(newLen);
            size = count;
            table = newTab;
        }

上面都有註釋,這裡提幾點需要注意的地方

  • 由於ThreadLocalMap沒有使用散列表的結構,所以發生hash衝突的時候是尋找下一個桶
  • 把key使用弱引用,可以使得gc正常回收,但是value並不是弱引用,所以在擴容的時候,把value置為null,方便value垃圾回收,在平時寫程式碼的時候,如果某個ThreadLocal不在使用了,最好直接呼叫ThreadLoalMap的remove方法把當前的key,value都移除,防止記憶體洩漏
  • 在getEntry方法和set方法中當key為null,就把value也置為null,同時把Entry也移除了。
  • 裡面有些方法沒有詳細註釋,因為並不是重要方法,所以就沒有仔細看

Thread、ThreadLocal、ThreadLocalMap三者之間的的關係

在我的另一篇分析Thread的文章有提到在Thread原始碼中有這麼一個欄位,如下:

ThreadLocal.ThreadLocalMap threadLocals = null;

這個欄位就是儲存TreadLocalMap的,也就是說每個執行緒都有一個ThreadLocalMap,ThreadLocalMap中儲存這個ThreadLocal和ThreadLocal封裝的成員變數的值,同一個父執行緒的子執行緒的ThreadLocalMap中儲存的ThreadLocal都是一樣的,只是value不同,具體三者之間的關係可以用下圖表示。

ThreadLocal常用方法分析

get方法

public T get() {
//獲取當前執行緒引用 Thread t
= Thread.currentThread();
//獲取當前執行緒的ThreadLocalMap,就是上面介紹的Thread類中的threadLocals欄位 ThreadLocalMap map
= getMap(t); if (map != null) {
//拿到ThreadLocalMap之後,根據key獲取Entry ThreadLocalMap.Entry e
= map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } }
    //如果是首次插入,map沒有建立,建立ThreadLocalMap
return setInitialValue(); }

進入#getMap()方法

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

進入#setInitialValue()方法

private T setInitialValue() {
     //這個方法在最開始舉例的時候重寫了
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
           //建立map
            createMap(t, value);
        return value;
    }

進入#initialValue()方法

    protected T initialValue() {
        return null;
    }

這個返回的泛型T就是ThreadLocal要包裝的成員變數

進入#createMap()方法

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

直接呼叫上面分析的ThreadLocalMap的構造方法建立,並且給Thread中threadLocals賦值,從這裡開始Thread就和ThreadLocal還有ThreadLocalMap聯絡起來了

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);
    }

這個方法很簡單,就不分析了。

remove方法

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

這個也很簡單。

常見應用場景

由於工作中基本沒有使用過,所以在網上看到幾個常見的使用場景,如下:

  1. 把session儲存到ThreadLocal中,但是現在session一般儲存在redis中,用於分散式共享,使用ThreadLocal只能在一個節點的執行緒中共享,無法做到分散式共享,所以這個場景目前來看並不合適。
  2. 由於SimpleDateFormat在格式化時間的時候,執行緒不安全,所以在高併發的時候格式化出來的日期可能是錯誤的,可以使用ThreadLocal封裝SimpleDateFormat,避免每次重新建立這個物件,這個確實是一個使用場景,但是現在是java8的天下,完全可以不用這個格式化類,java8可以通過LocalDateTime獲取日期時間,通過DateTimeFormatter進行格式化,這個是一個執行緒安全的類

        

沒有找到具體日常開發中使用ThreadLocal的場景,所以找到了原始碼中使用ThreadLocal的例子,就是ReentrantReadWriteLock,是一個讀寫鎖,大家有興趣可以看一下我的另一篇文章:ReentrantReadWriteLock原理分析

參考:

【死磕 Java 併發】—– 深入分析 ThreadLocal

通過例子理解java強引用,軟引用,弱引用,虛引用