1. 程式人生 > 實用技巧 >執行緒基礎知識08- ThreadLocal基礎總結

執行緒基礎知識08- ThreadLocal基礎總結

     ThreadLocal在平時開發中是用的比較多的一個類,主要用於儲存執行緒的資料。下面我對ThreadLocal進行一下總結;

     dreamcatcher-cx大佬對ThreadLocal的設計總結,寫的比較深刻,很有幫助。

主要使用場景:

  • 多資料來源切換,記錄當前執行緒訪問的資料來源

  • spring框架事務管理,用於存放事務資料;

  • springsecurity安全框架,用於儲存使用者登入資訊;

除了以上還有很多,不限於以上。

解讀原始碼

資料儲存ThreadLockMap

  • 通過Entry陣列進行儲存的;

  • Entry繼承Reference類,是弱關聯的類,當ThreadLocal的例項為空時,GC會快速收回;一般用於處理資料量較大且維持時間較短的業務。

  • Entry節點儲存的是ThreadLocal物件和物件值

//是通過Entry陣列進行儲存的 
private Entry[] table;

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

set 方法

  • 存放ThreadLocal<?>的是弱關聯的容器,GC會等儲存滿的時候處理;

  • 存放的過程中,會對key判斷,調整陣列位置;

 private void set(ThreadLocal<?> key, Object value) {
            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)]) {
                ThreadLocal<?> k = e.get();//獲取存入的ThreadLock
                if (k == key) {//判斷是否一致,如果一致,替換值並返回
                    e.value = value;
                    return;
                }

                if (k == null) {//如果i節點獲取到的ThreadLocal為空的話
                    /**
                     * 1.從i 的向前迴圈查詢tab[i]節點的ThreadLocal不為空的位置slotToExpunge;
                     * 2.從i以下節點迴圈遍歷,會有兩種情況;
                     * 3.第一種情況:找到對應key相等的節點,和當前遍歷的節點位置進行交換;如果slotToExpunge和i節點相同,則進行rehash
                     * 4.第二種情況:key為空,並且slotToExpunge和遍歷的位置相等的情況下,slotToExpunge == 遍歷的位置;  
                     * 5.根據傳入的i和slotToExpunge是否相等,進行陣列的節點的清除和rehash操作   
                     */                
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);//新增信的節點
            int sz = ++size;
            /**
             * 判斷是不是要進行擴容
             * Entry陣列的擴容是2倍擴容的;            
             */
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

get方法

  • 獲取儲存的Entry節點
   private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                /**
                 * 從i位置往後逐個遍歷查詢如果找到了就返回,沒找到返回null;
                 */   
                return getEntryAfterMiss(key, i, e);
        }

remove方法

  • 查詢對應資料節點進行刪除就行
  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類的方法

set方法資料儲存

  • 首次儲存,新建一個ThreadLockMap,以當前ThreaLock為key;

  • 如果執行緒的儲存區域已經初始化過,則更新儲存區域中的資料;

public void set(T value) {
        Thread t = Thread.currentThread();// 獲取當前執行緒
        ThreadLocalMap map = getMap(t);//獲取儲存資料的MAP
        if (map != null)//判斷儲存資料的map是否為空,如果為空則建立map,如果不為空,則存入資料
            map.set(this, value);
        else
            createMap(t, value);//如果沒有就建立一個,當前Thread
    }
//獲取當前執行緒的值儲存區域
ThreadLocalMap getMap(Thread t) {
        return t.threadLocals; //執行緒的成員變數
    }
//給當前執行緒建立一個新的ThreadLocalMap,並存儲以當前ThreaLocal例項為key的鍵值對。
 void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

get方法

  • 如果在訪問之前沒有儲存任何資料,則對ThreadLockMap進行初始化,綁定當前執行緒。並進行初始值的建立,和當前ThreadLock例項進行繫結,kv儲存

  • 如果獲取到之前儲存的值,則進行返回存入的值;

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);//獲取當前執行緒的儲存map
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);//獲取當前的ThreadLock的節點,具體上面有介紹
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();//如果不存在則進行初始化
    }


//進行一些初始化操作
private T setInitialValue() {
        T value = initialValue();//ThreaLock提供的一個可繼承的方法,進行初始化操作
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);//獲取當前執行緒的
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

remove 方法

  public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)//在儲存的map不為空的情況下,移除當前ThreadLock例項
             m.remove(this);
     }

總結

注意點:

1. ThreadLocalMap儲存的key值為當前的ThreadLock例項,所以一般要求ThreadLocal是單例的

2. 單個執行緒可以有多個ThreadLocal例項,儲存資料。

總結分析:

1.ThreadLocal的設計思路:

  • 原理:相當給當前執行緒的建立獨有資料域,方便當前執行緒訪問。這樣避免了多執行緒環境中,訪問當前執行緒鎖的相關問題

  • 之所以使用ThreadLocalMap進行儲存,而不用HashMap,上面原始碼分析說了,源於Reference類的特性。

    • 弱應用,當物件無用的時候,會快速被GC回收。符合執行緒的特點,一般一個執行緒時間不會很長,當伴隨大量資料時能快速回收

    • 當前執行緒結束的時候,要通過呼叫ThreadLocal的remove方法,即使將ThreadLocalMap中的對應例項key設定為空,方便GC快速回收

  • ThreadLocal操作更有優勢,下面我具體分析以下

為什麼用ThreadLocal類直接操作儲存資料 ?

1.簡化程式碼:

試想一下上面的原始碼,如果直接從Thread中存放資料,
首先,要用Thread.currentThread()獲取當前執行緒,
然後,再通過當前執行緒thread獲取儲存的Map
再根據key獲取對那個的操作

從上面的原始碼可以看出,這些操作細節,都已經在ThreadLocal類中進行了隱藏;
1.避免每次使用時的重複程式碼;
2.而使用ThreadLocal例項獲取資料,相當於拿key直接獲取資料,隱藏了獲取細節

2.限制和統一管理儲存值

1. 內部的Entry陣列,進行了泛型約束,避免了直接在Thread類中Map操作key的不規範性;

2. 避免了開發人員直接操作當前執行緒,而使用“變數副本”進行操作