首頁>技術>

目錄一 引言二 原始碼解析三 案例四 總結一 引言

ThreadLocal的官方API解釋為:

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

這個類提供執行緒區域性變數。這些變數與正常的變數不同,每個執行緒訪問一個(透過它的get或set方法)都有它自己的、獨立初始化的變數副本。ThreadLocal例項通常是類中的私有靜態欄位,希望將狀態與執行緒關聯(例如,使用者ID或事務ID)。

1、當使用ThreadLocal維護變數時,ThreadLocal為每個使用該變數的執行緒提供獨立的變數副本,        所以每一個執行緒都可以獨立地改變自己的副本,而不會影響其它執行緒所對應的副本2、使用ThreadLocal通常是定義為 private static ,更好是 private final static3、Synchronized用於執行緒間的資料共享,而ThreadLocal則用於執行緒間的資料隔離4、ThreadLocal類封裝了getMap()、Set()、Get()、Remove()4個核心方法

從表面上來看ThreadLocal內部是封閉了一個Map陣列,來實現物件的執行緒封閉,map的key就是當前的執行緒id,value就是我們要儲存的物件。

實際上是ThreadLocal的靜態內部類ThreadLocalMap為每個Thread都維護了一個數組table,hreadLocal確定了一個數組下標,而這個下標就是value儲存的對應位置,繼承自弱引用,用來儲存ThreadLocal和Value之間的對應關係,之所以用弱引用,是為了解決執行緒與ThreadLocal之間的強繫結關係,會導致如果執行緒沒有被回收,則GC便一直無法回收這部分內容。

二 原始碼剖析

2.1 ThreadLocal

    //set方法    public void set(T value) {        //獲取當前執行緒        Thread t = Thread.currentThread();        //實際儲存的資料結構型別        ThreadLocalMap map = getMap(t);        //判斷map是否為空,如果有就set當前物件,沒有建立一個ThreadLocalMap        //並且將其中的值放入建立物件中        if (map != null)            map.set(this, value);        else            createMap(t, value);    }    //get方法     public T get() {        //獲取當前執行緒        Thread t = Thread.currentThread();        //實際儲存的資料結構型別        ThreadLocalMap map = getMap(t);        if (map != null) {            //傳入了當前執行緒的ID,到底層Map Entry裡面去取            ThreadLocalMap.Entry e = map.getEntry(this);            if (e != null) {                @SuppressWarnings("unchecked")                T result = (T)e.value;                return result;            }        }        return setInitialValue();    }       //remove方法     public void remove() {         ThreadLocalMap m = getMap(Thread.currentThread());         if (m != null)             m.remove(this);//呼叫ThreadLocalMap刪除變數     }       //ThreadLocalMap中getEntry方法      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                return getEntryAfterMiss(key, i, e);        }    //getMap()方法   ThreadLocalMap getMap(Thread t) {    //Thread中維護了一個ThreadLocalMap        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            createMap(t, value);        return value;    }    //createMap()方法   void createMap(Thread t, T firstValue) {   //例項化一個新的ThreadLocalMap,並賦值給執行緒的成員變數threadLocals        t.threadLocals = new ThreadLocalMap(this, firstValue);    }

從上面原始碼中我們看到不管是 set() get() remove() 他們都是操作ThreadLocalMap這個靜態內部類的,每一個新的執行緒Thread都會例項化一個ThreadLocalMap並賦值給成員變數threadLocals,使用時若已經存在threadLocals則直接使用已經存在的物件

ThreadLocal.get()

獲取當前執行緒對應的ThreadLocalMap如果當前ThreadLocal物件對應的Entry還存在,並且返回對應的值如果獲取到的ThreadLocalMap為null,則證明還沒有初始化,就呼叫setInitialValue()方法

ThreadLocal.set()

獲取當前執行緒,根據當前執行緒獲取對應的ThreadLocalMap如果對應的ThreadLocalMap不為null,則呼叫set方法儲存對應關係如果為null,建立一個並儲存k-v關係

ThreadLocal.remove()

獲取當前執行緒,根據當前執行緒獲取對應的ThreadLocalMap如果對應的ThreadLocalMap不為null,則呼叫ThreadLocalMap中的remove方法,根據key.threadLocalHashCode & (len-1)獲取當前下標並移除成功後呼叫expungeStaleEntry進行一次連續段清理

2.2 ThreadLocalMap

ThreadLocalMap是ThreadLocal的一個內部類

static class ThreadLocalMap {         /**             * 自定義一個Entry類,並繼承自弱引用         * 同時讓ThreadLocal和儲值形成key-value的關係         * 之所以用弱引用,是為了解決執行緒與ThreadLocal之間的強繫結關係         * 會導致如果執行緒沒有被回收,則GC便一直無法回收這部分內容         *          */        static class Entry extends WeakReference<ThreadLocal<?>> {            /** The value associated with this ThreadLocal. */            Object value;            Entry(ThreadLocal<?> k, Object v) {                super(k);                value = v;            }        }        /**         * Entry陣列的初始化大小(初始化長度16,後續每次都是2倍擴容)         */        private static final int INITIAL_CAPACITY = 16;        /**         * 根據需要調整大小         * 長度必須是2的N次冪         */        private Entry[] table;        /**         * The number of entries in the table.         * table中的個數         */        private int size = 0;        /**         * The next size value at which to resize.         * 下一個要調整大小的大小值(擴容的閾值)         */        private int threshold; // Default to 0        /**         * Set the resize threshold to maintain at worst a 2/3 load factor.         * 根據長度計算擴容閾值         * 保持一定的負債係數         */        private void setThreshold(int len) {            threshold = len * 2 / 3;        }        /**         * Increment i modulo len         * nextIndex:從字面意思我們可以看出來就是獲取下一個索引         * 獲取下一個索引,超出長度則返回         */        private static int nextIndex(int i, int len) {            return ((i + 1 < len) ? i + 1 : 0);        }        /**         * Decrement i modulo len.         * 返回上一個索引,如果-1為負數,返回長度-1的索引         */        private static int prevIndex(int i, int len) {            return ((i - 1 >= 0) ? i - 1 : len - 1);        }        /**         * ThreadLocalMap構造方法         * ThreadLocalMaps是延遲構造的,因此只有在至少要放置一個節點時才建立一個         */        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {            //內部成員陣列,INITIAL_CAPACITY值為16的常量            table = new Entry[INITIAL_CAPACITY];            //透過threadLocalHashCode(HashCode) & (長度-1)的位運算,確定鍵值對的位置            //位運算,結果與取模相同,計算出需要存放的位置            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);            // 建立一個新節點儲存在table當中            table[i] = new Entry(firstKey, firstValue);            //設定table元素為1            size = 1;            //根據長度計算擴容閾值            setThreshold(INITIAL_CAPACITY);        }        /**         * 構造一個包含所有可繼承ThreadLocals的新對映,只能createInheritedMap呼叫         * ThreadLocal本身是執行緒隔離的,一般來說是不會出現資料共享和傳遞的行為         */        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中getEntry方法         */        private Entry getEntry(ThreadLocal<?> key) {            //透過hashcode確定下標            int i = key.threadLocalHashCode & (table.length - 1);            Entry e = table[i];            //如果找到則直接返回            if (e != null && e.get() == key)                return e;            else             // 找不到的話接著從i位置開始向後遍歷,基於線性探測法,是有可能在i之後的位置找到的                return getEntryAfterMiss(key, i, e);        }        /**         * ThreadLocalMap的set方法         */        private void set(ThreadLocal<?> key, Object value) {           //新開一個引用指向table            Entry[] tab = table;            //獲取table長度            int len = tab.length;            ////獲取索引值,threadLocalHashCode進行一個位運算(取模)得到索引i            int i = key.threadLocalHashCode & (len-1);            /**            * 遍歷tab如果已經存在(key)則更新值(value)            * 如果該key已經被回收失效,則替換該失效的key            **/            //            for (Entry e = tab[i];                 e != null;                 e = tab[i = nextIndex(i, len)]) {                ThreadLocal<?> k = e.get();                if (k == key) {                    e.value = value;                    return;                }                //如果 k 為null,則替換當前失效的k所在Entry節點                if (k == null) {                    replaceStaleEntry(key, value, i);                    return;                }            }            //如果上面沒有遍歷成功則建立新值            tab[i] = new Entry(key, value);            // table內元素size自增            int sz = ++size;            //滿足條件陣列擴容x2            if (!cleanSomeSlots(i, sz) && sz >= threshold)                rehash();        }        /**         * remove方法         * 將ThreadLocal物件對應的Entry節點從table當中刪除         */        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();//將引用設定null,方便GC回收                    expungeStaleEntry(i);//從i的位置開始連續段清理工作                    return;                }            }        }        /**        * ThreadLocalMap中replaceStaleEntry方法         */        private void replaceStaleEntry(ThreadLocal<?> key, Object value,                                       int staleSlot) {            // 新建一個引用指向table            Entry[] tab = table;            //獲取table的長度            int len = tab.length;            Entry e;            // 記錄當前失效的節點下標            int slotToExpunge = staleSlot;           /**             * 透過prevIndex(staleSlot, len)可以看出,由staleSlot下標向前掃描             * 查詢並記錄最前位置value為null的下標             */            for (int i = prevIndex(staleSlot, len);                 (e = tab[i]) != null;                 i = prevIndex(i, len))                if (e.get() == null)                    slotToExpunge = i;            // nextIndex(staleSlot, len)可以看出,這個是向後掃描            // occurs first            for (int i = nextIndex(staleSlot, len);                 (e = tab[i]) != null;                 i = nextIndex(i, len)) {                 // 獲取Entry節點對應的ThreadLocal物件                ThreadLocal<?> k = e.get();                  //如果和新的key相等的話,就直接賦值給value,替換i和staleSlot的下標                if (k == key) {                    e.value = value;                    tab[i] = tab[staleSlot];                    tab[staleSlot] = e;                    // 如果之前的元素存在,則開始呼叫cleanSomeSlots清理                    if (slotToExpunge == staleSlot)                        slotToExpunge = i;                     /**                     *在呼叫cleanSomeSlots()    清理之前,會呼叫                     *expungeStaleEntry()從slotToExpunge到table下標所在為                     *null的連續段進行一次清理,返回值就是table為null的下標                     *然後以該下標 len進行一次啟發式清理                     * 最終裡面的方法實際上還是呼叫了expungeStaleEntry                      * 可以看出expungeStaleEntry方法是ThreadLocal核心的清理函式                     */                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);                    return;                }                // If we didn't find stale entry on backward scan, the                // first stale entry seen while scanning for key is the                // first still present in the run.                if (k == null && slotToExpunge == staleSlot)                    slotToExpunge = i;            }            // 如果在table中沒有找到這個key,則直接在當前位置new Entry(key, value)            tab[staleSlot].value = null;            tab[staleSlot] = new Entry(key, value);            // 如果有其他過時的節點正在執行,會將它們進行清除,slotToExpunge會被重新賦值            if (slotToExpunge != staleSlot)                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);        }        /**         * expungeStaleEntry() 啟發式地清理被回收的Entry         * 有兩個地方呼叫到這個方法         * 1、set方法,在判斷是否需要resize之前,會清理並rehash一遍         * 2、替換失效的節點時候,也會進行一次清理        */          private boolean cleanSomeSlots(int i, int n) {            boolean removed = false;            Entry[] tab = table;            int len = tab.length;            do {                i = nextIndex(i, len);                Entry e = tab[i];                //判斷如果Entry物件不為空                if (e != null && e.get() == null) {                    n = len;                    removed = true;                    //呼叫該方法進行回收,                    //對 i 開始到table所在下標為null的範圍內進行一次清理和rehash                    i = expungeStaleEntry(i);                }            } while ( (n >>>= 1) != 0);            return removed;        }          private int expungeStaleEntry(int staleSlot) {            Entry[] tab = table;            int len = tab.length;            // expunge entry at staleSlot            tab[staleSlot].value = null;            tab[staleSlot] = null;            size--;            // Rehash until we encounter null            Entry e;            int i;            for (i = nextIndex(staleSlot, len);                 (e = tab[i]) != null;                 i = nextIndex(i, len)) {                ThreadLocal<?> k = e.get();                if (k == null) {                    e.value = null;                    tab[i] = null;                    size--;                } else {                    int h = k.threadLocalHashCode & (len - 1);                    if (h != i) {                        tab[i] = null;                        while (tab[h] != null)                            h = nextIndex(h, len);                        tab[h] = e;                    }                }            }            return i;        }        /**         * Re-pack and/or re-size the table. First scan the entire         * table removing stale entries. If this doesn't sufficiently         * shrink the size of the table, double the table size.         */        private void rehash() {            expungeStaleEntries();            // Use lower threshold for doubling to avoid hysteresis            if (size >= threshold - threshold / 4)                resize();        }        /**         * 對table進行擴容,因為要保證table的長度是2的冪,所以擴容就擴大2倍         */        private void resize() {        //獲取舊table的長度            Entry[] oldTab = table;            int oldLen = oldTab.length;            int newLen = oldLen * 2;            //建立一個長度為舊長度2倍的Entry陣列            Entry[] newTab = new Entry[newLen];            //記錄插入的有效Entry節點數            int count = 0;             /**             * 從下標0開始,逐個向後遍歷插入到新的table當中             * 透過hashcode & len - 1計算下標,如果該位置已經有Entry陣列,則透過線性探測向後探測插入             */            for (int j = 0; j < oldLen; ++j) {                Entry e = oldTab[j];                if (e != null) {                    ThreadLocal<?> k = e.get();                    if (k == null) {//如遇到key已經為null,則value設定null,方便GC回收                        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            size = count;             // 指向新的Entry陣列            table = newTab;        }    }

ThreadLocalMap.set()

key.threadLocalHashCode & (len-1),將threadLocalHashCode進行一個位運算(取模)得到索引 " i " ,也就是在table中的下標for迴圈遍歷,如果Entry中的key和我們的需要操作的ThreadLocal的相等,這直接賦值替換如果拿到的key為null ,則呼叫replaceStaleEntry()進行替換如果上面的條件都沒有成功滿足,直接在計算的下標中建立新值在進行一次清理之後,呼叫rehash()下的resize()進行擴容

ThreadLocalMap.expungeStaleEntry()

這是 ThreadLocal 中一個核心的清理方法為什麼需要清理?在我們 Entry 中,如果有很多節點是已經過時或者回收了,但是在table陣列中繼續存在,會導致資源浪費我們在清理節點的同時,也會將後面的Entry節點,重新排序,調整Entry大小,這樣我們在取值(get())的時候,可以快速定位資源,加快我們的程式的獲取效率

ThreadLocalMap.remove()

我們在使用remove節點的時候,會使用線性探測的方式,找到當前的key如果當前key一致,呼叫clear()將引用指向null從"i"開始的位置進行一次連續段清理三 案例

目錄結構:

在這裡插入圖片描述

HttpFilter.java

package com.lyy.threadlocal.config;import lombok.extern.slf4j.Slf4j;import javax.servlet.*;import javax.servlet.http.HttpServletRequest;import java.io.IOException;@Slf4jpublic class HttpFilter implements Filter {//初始化需要做的事情    @Override    public void init(FilterConfig filterConfig) throws ServletException {    }    //核心操作在這個裡面    @Override    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {        HttpServletRequest request = (HttpServletRequest)servletRequest;//        request.getSession().getAttribute("user");        System.out.println("do filter:"+Thread.currentThread().getId()+":"+request.getServletPath());        RequestHolder.add(Thread.currentThread().getId());        //讓這個請求完,,同時做下一步處理        filterChain.doFilter(servletRequest,servletResponse);    }    //不再使用的時候做的事情    @Override    public void destroy() {    }}

HttpInterceptor.java

package com.lyy.threadlocal.config;import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;public class HttpInterceptor extends HandlerInterceptorAdapter {    //介面處理之前    @Override    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {        System.out.println("preHandle:");        return true;    }    //介面處理之後    @Override    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {        RequestHolder.remove();       System.out.println("afterCompletion");        return;    }}

RequestHolder.java

package com.lyy.threadlocal.config;public class RequestHolder {    private final static ThreadLocal<Long> requestHolder = new ThreadLocal<>();//    //提供方法傳遞資料    public static void add(Long id){        requestHolder.set(id);    }    public static Long getId(){        //傳入了當前執行緒的ID,到底層Map裡面去取        return requestHolder.get();    }    //移除變數資訊,否則會造成逸出,導致內容永遠不會釋放掉    public static void remove(){        requestHolder.remove();    }}

ThreadLocalController.java

package com.lyy.threadlocal.controller;import com.lyy.threadlocal.config.RequestHolder;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.ResponseBody;@Controller@RequestMapping("/thredLocal")public class ThreadLocalController {    @RequestMapping("test")    @ResponseBody    public Long test(){        return RequestHolder.getId();    }}

ThreadlocalDemoApplication.java

package com.lyy.threadlocal;import com.lyy.threadlocal.config.HttpFilter;import com.lyy.threadlocal.config.HttpInterceptor;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.boot.web.servlet.FilterRegistrationBean;import org.springframework.context.annotation.Bean;import org.springframework.web.servlet.config.annotation.InterceptorRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;@SpringBootApplicationpublic class ThreadlocalDemoApplication extends WebMvcConfigurerAdapter {    public static void main(String[] args) {        SpringApplication.run(ThreadlocalDemoApplication.class, args);    }    @Bean    public FilterRegistrationBean httpFilter(){        FilterRegistrationBean registrationBean = new FilterRegistrationBean();        registrationBean.setFilter(new HttpFilter());        registrationBean.addUrlPatterns("/thredLocal/*");        return registrationBean;    }    @Override    public void addInterceptors(InterceptorRegistry registry) {        registry.addInterceptor(new HttpInterceptor()).addPathPatterns("/**");    }}

輸入:http://localhost:8080/thredLocal/test

後臺列印:

do filter:35:/thredLocal/test preHandle:afterCompletion
四 總結

1、ThreadLocal是透過每個執行緒單獨一份儲存空間,每個ThreadLocal只能儲存一個變數副本。2、相比於Synchronized,ThreadLocal具有執行緒隔離的效果,只有在執行緒內才能獲取到對應的值,執行緒外則不能訪問到想要的值,很好的實現了執行緒封閉。3、每次使用完ThreadLocal,都呼叫它的remove()方法,清除資料,避免記憶體洩漏的風險4、透過上面的原始碼分析,我們也可以看到大神在寫程式碼的時候會考慮到整體實現的方方面面,一些邏輯上的處理是真嚴謹的,我們在看原始碼的時候不能只是做了解,也要看到別人實現功能後面的目的。

27
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 開源ETL神器 KETTLE / PDI / CKETTLE