官术网_书友最值得收藏!

1.11 ThreadLocal

多線程訪問同一個共享變量時特別容易出現(xiàn)并發(fā)問題,特別是在多個線程需要對一個共享變量進行寫入時。為了保證線程安全,一般使用者在訪問共享變量時需要進行適當(dāng)?shù)耐剑鐖D1-3所示。

同步的措施一般是加鎖,這就需要使用者對鎖有一定的了解,這顯然加重了使用者的負擔(dān)。那么有沒有一種方式可以做到,當(dāng)創(chuàng)建一個變量后,每個線程對其進行訪問的時候訪問的是自己線程的變量呢?其實ThreadLocal就可以做這件事情,雖然ThreadLocal并不是為了解決這個問題而出現(xiàn)的。

圖1-3

ThreadLocal是JDK包提供的,它提供了線程本地變量,也就是如果你創(chuàng)建了一個ThreadLocal變量,那么訪問這個變量的每個線程都會有這個變量的一個本地副本。當(dāng)多個線程操作這個變量時,實際操作的是自己本地內(nèi)存里面的變量,從而避免了線程安全問題。創(chuàng)建一個ThreadLocal變量后,每個線程都會復(fù)制一個變量到自己的本地內(nèi)存,如圖1-4所示。

圖1-4

1.11.1 ThreadLocal使用示例

本節(jié)介紹如何使用ThreadLocal。本例開啟了兩個線程,在每個線程內(nèi)部都設(shè)置了本地變量的值,然后調(diào)用print函數(shù)打印當(dāng)前本地變量的值。如果打印后調(diào)用了本地變量的remove方法,則會刪除本地內(nèi)存中的該變量,代碼如下。

public class ThreadLocalTest {
    //(1)print函數(shù)
    static void print(String str){
        //1.1 打印當(dāng)前線程本地內(nèi)存中l(wèi)ocalVariable變量的值
        System.out.println(str + ":" +localVariable.get());
        //1.2 清除當(dāng)前線程本地內(nèi)存中的localVariable變量
        //localVariable.remove();
    }
    //(2) 創(chuàng)建ThreadLocal變量
    static ThreadLocal<String> localVariable = new ThreadLocal<>();
    public static void main(String[] args) {
        //(3) 創(chuàng)建線程one
        Thread threadOne = new Thread(new  Runnable() {
            public void run() {
                //3.1 設(shè)置線程One中本地變量localVariable的值
                localVariable.set("threadOne local variable");
                //3.2 調(diào)用打印函數(shù)
                print("threadOne");
                //3.3 打印本地變量值
                System.out.println("threadOne remove after" + ":" +localVariable.get());
            }
        });
        //(4) 創(chuàng)建線程two
        Thread threadTwo = new Thread(new  Runnable() {
            public void run() {
                //4.1 設(shè)置線程Two中本地變量localVariable的值
                localVariable.set("threadTwo local variable");
                //4.2 調(diào)用打印函數(shù)
                print("threadTwo");
                //4.3 打印本地變量值
                System.out.println("threadTwo remove after" + ":" +localVariable.get());
            }
        });
        //(5)啟動線程
        threadOne.start();
        threadTwo.start();
    }

運行結(jié)果如下。

threadOne:threadOne local variable
threadTwo:threadTwo local variable
threadOne remove after:threadOne local variable
threadTwo remove after:threadTwo local variable

代碼(2)創(chuàng)建了一個ThreadLocal變量。

代碼(3)和(4)分別創(chuàng)建了線程One和Two。

代碼(5)啟動了兩個線程。

線程One中的代碼3.1通過set方法設(shè)置了localVariable的值,這其實設(shè)置的是線程One本地內(nèi)存中的一個副本,這個副本線程Two是訪問不了的。然后代碼3.2調(diào)用了print函數(shù),代碼1.1通過get函數(shù)獲取了當(dāng)前線程(線程One)本地內(nèi)存中l(wèi)ocalVariable的值。

線程Two的執(zhí)行類似于線程One。

打開代碼1.2的注釋后,再次運行,運行結(jié)果如下。

threadOne:threadOne local variable
threadOne remove after:null
threadTwo:threadTwo local variable
threadTwo remove after:null

1.11.2 ThreadLocal的實現(xiàn)原理

首先看一下ThreadLocal相關(guān)類的類圖結(jié)構(gòu),如圖1-5所示。

圖1-5

由該圖可知,Thread類中有一個threadLocals和一個inheritableThreadLocals,它們都是ThreadLocalMap類型的變量,而ThreadLocalMap是一個定制化的Hashmap。在默認情況下,每個線程中的這兩個變量都為null,只有當(dāng)前線程第一次調(diào)用ThreadLocal的set或者get方法時才會創(chuàng)建它們。其實每個線程的本地變量不是存放在ThreadLocal實例里面,而是存放在調(diào)用線程的threadLocals變量里面。也就是說,ThreadLocal類型的本地變量存放在具體的線程內(nèi)存空間中。ThreadLocal就是一個工具殼,它通過set方法把value值放入調(diào)用線程的threadLocals里面并存放起來,當(dāng)調(diào)用線程調(diào)用它的get方法時,再從當(dāng)前線程的threadLocals變量里面將其拿出來使用。如果調(diào)用線程一直不終止,那么這個本地變量會一直存放在調(diào)用線程的threadLocals變量里面,所以當(dāng)不需要使用本地變量時可以通過調(diào)用ThreadLocal變量的remove方法,從當(dāng)前線程的threadLocals里面刪除該本地變量。另外,Thread里面的threadLocals為何被設(shè)計為map結(jié)構(gòu)?很明顯是因為每個線程可以關(guān)聯(lián)多個ThreadLocal變量。

下面簡單分析ThreadLocal的set、get及remove方法的實現(xiàn)邏輯。

1.void set(T value)

    public void set(T value) {
        //(1)獲取當(dāng)前線程
        Thread t = Thread.currentThread();
        //(2)將當(dāng)前線程作為key,去查找對應(yīng)的線程變量,找到則設(shè)置
        ThreadLocalMap map = getMap(t);
        if (map ! = null)
            map.set(this, value);
        else
        //(3)第一次調(diào)用就創(chuàng)建當(dāng)前線程對應(yīng)的HashMap
            createMap(t, value);
    }

代碼(1)首先獲取調(diào)用線程,然后使用當(dāng)前線程作為參數(shù)調(diào)用getMap(t)方法,getMap(Thread t)的代碼如下。

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

可以看到,getMap(t)的作用是獲取線程自己的變量threadLocals, threadlocal變量被綁定到了線程的成員變量上。

如果getMap(t)的返回值不為空,則把value值設(shè)置到threadLocals中,也就是把當(dāng)前變量值放入當(dāng)前線程的內(nèi)存變量threadLocals中。threadLocals是一個HashMap結(jié)構(gòu),其中key就是當(dāng)前ThreadLocal的實例對象引用,value是通過set方法傳遞的值。

如果getMap(t)返回空值則說明是第一次調(diào)用set方法,這時創(chuàng)建當(dāng)前線程的threadLocals變量。下面來看createMap(t, value)做什么。

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

它創(chuàng)建當(dāng)前線程的threadLocals變量。

2.T get()

    public T get() {
        //(4) 獲取當(dāng)前線程
        Thread t = Thread.currentThread();
        //(5)獲取當(dāng)前線程的threadLocals變量
        ThreadLocalMap map = getMap(t);
        //(6)如果threadLocals不為null,則返回對應(yīng)本地變量的值
        if (map ! = null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e ! = null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //(7)threadLocals為空則初始化當(dāng)前線程的threadLocals成員變量
                return setInitialValue();
    }

代碼(4)首先獲取當(dāng)前線程實例,如果當(dāng)前線程的threadLocals變量不為null,則直接返回當(dāng)前線程綁定的本地變量,否則執(zhí)行代碼(7)進行初始化。setInitialValue()的代碼如下。

    private T setInitialValue() {
        //(8)初始化為null
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        //(9)如果當(dāng)前線程的threadLocals變量不為空
        if (map ! = null)
            map.set(this, value);
        else
        //(10)如果當(dāng)前線程的threadLocals變量為空
            createMap(t, value);
        return value;
    }
    protected T initialValue() {
        return null;
    }

如果當(dāng)前線程的threadLocals變量不為空,則設(shè)置當(dāng)前線程的本地變量值為null,否則調(diào)用createMap方法創(chuàng)建當(dāng)前線程的createMap變量。

3.void remove()

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

如以上代碼所示,如果當(dāng)前線程的threadLocals變量不為空,則刪除當(dāng)前線程中指定ThreadLocal實例的本地變量。

總結(jié):如圖1-6所示,在每個線程內(nèi)部都有一個名為threadLocals的成員變量,該變量的類型為HashMap,其中key為我們定義的ThreadLocal變量的this引用,value則為我們使用set方法設(shè)置的值。每個線程的本地變量存放在線程自己的內(nèi)存變量threadLocals中,如果當(dāng)前線程一直不消亡,那么這些本地變量會一直存在,所以可能會造成內(nèi)存溢出,因此使用完畢后要記得調(diào)用ThreadLocal的remove方法刪除對應(yīng)線程的threadLocals中的本地變量。在高級篇要講解的JUC包里面的ThreadLocalRandom,就是借鑒ThreadLocal的思想實現(xiàn)的,后面會具體講解。

圖1-6

1.11.3 ThreadLocal不支持繼承性

首先看一個例子。

public class TestThreadLocal {
    //(1)創(chuàng)建線程變量
    public static ThreadLocal<String> threadLocal = new ThreadLocal<String>();
    public static void main(String[] args) {
        //(2) 設(shè)置線程變量
        threadLocal.set("hello world");
        //(3) 啟動子線程
        Thread thread = new Thread(new  Runnable() {
            public void run() {
                //(4) 子線程輸出線程變量的值
                System.out.println("thread:" + threadLocal.get());
            }
        });
        thread.start();
        //(5) 主線程輸出線程變量的值
        System.out.println("main:" + threadLocal.get());
    }
}

輸出結(jié)果如下。

main:hello world
thread:null

也就是說,同一個ThreadLocal變量在父線程中被設(shè)置值后,在子線程中是獲取不到的。根據(jù)上節(jié)的介紹,這應(yīng)該是正常現(xiàn)象,因為在子線程thread里面調(diào)用get方法時當(dāng)前線程為thread線程,而這里調(diào)用set方法設(shè)置線程變量的是main線程,兩者是不同的線程,自然子線程訪問時返回null。那么有沒有辦法讓子線程能訪問到父線程中的值?答案是有。

1.11.4 InheritableThreadLocal類

為了解決上節(jié)提出的問題,InheritableThreadLocal應(yīng)運而生。InheritableThreadLocal繼承自ThreadLocal,其提供了一個特性,就是讓子線程可以訪問在父線程中設(shè)置的本地變量。下面看一下InheritableThreadLocal的代碼。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    //(1)
    protected T childValue(T parentValue) {
        return parentValue;
    }
    //(2)
    ThreadLocalMap getMap(Thread t) {
      return t.inheritableThreadLocals;
    }
    //(3)
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

由如上代碼可知,InheritableThreadLocal繼承了ThreadLocal,并重寫了三個方法。由代碼(3)可知,InheritableThreadLocal重寫了createMap方法,那么現(xiàn)在當(dāng)?shù)谝淮握{(diào)用set方法時,創(chuàng)建的是當(dāng)前線程的inheritableThreadLocals變量的實例而不再是threadLocals。由代碼(2)可知,當(dāng)調(diào)用get方法獲取當(dāng)前線程內(nèi)部的map變量時,獲取的是inheritableThreadLocals而不再是threadLocals。

綜上可知,在InheritableThreadLocal的世界里,變量inheritableThreadLocals替代了threadLocals。

下面我們看一下重寫的代碼(1)何時執(zhí)行,以及如何讓子線程可以訪問父線程的本地變量。這要從創(chuàng)建Thread的代碼說起,打開Thread類的默認構(gòu)造函數(shù),代碼如下。

    public Thread(Runnable target) {
     init(null, target, "Thread-" + nextThreadNum(), 0);
  }
   private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc) {
        ...
        //(4)獲取當(dāng)前線程
        Thread parent = currentThread();
        ...
        //(5)如果父線程的inheritableThreadLocals變量不為null
        if (parent.inheritableThreadLocals ! = null)
        //(6)設(shè)置子線程中的inheritableThreadLocals變量
        this.inheritableThreadLocals =
  ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
     this.stackSize = stackSize;
     tid = nextThreadID();
  }

如上代碼在創(chuàng)建線程時,在構(gòu)造函數(shù)里面會調(diào)用init方法。代碼(4)獲取了當(dāng)前線程(這里是指main函數(shù)所在的線程,也就是父線程),然后代碼(5)判斷main函數(shù)所在線程里面的inheritableThreadLocals屬性是否為null,前面我們講了InheritableThreadLocal類的get和set方法操作的是inheritableThreadLocals,所以這里的inheritableThreadLocal變量不為null,因此會執(zhí)行代碼(6)。下面看一下createInheritedMap的代碼。

    static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
        return new ThreadLocalMap(parentMap);
    }

可以看到,在createInheritedMap內(nèi)部使用父線程的inheritableThreadLocals變量作為構(gòu)造函數(shù)創(chuàng)建了一個新的ThreadLocalMap變量,然后賦值給了子線程的inheritableThreadLocals變量。下面我們看看在ThreadLocalMap的構(gòu)造函數(shù)內(nèi)部都做了什么事情。

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) {
                        //(7)調(diào)用重寫的方法
                        Object value = key.childValue(e.value); //返回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++;
                    }
                }
            }
        }

在該構(gòu)造函數(shù)內(nèi)部把父線程的inheritableThreadLocals成員變量的值復(fù)制到新的ThreadLocalMap對象中,其中代碼(7)調(diào)用了InheritableThreadLocal類重寫的代碼(1)。

總結(jié):InheritableThreadLocal類通過重寫代碼(2)和(3)讓本地變量保存到了具體線程的inheritableThreadLocals變量里面,那么線程在通過InheritableThreadLocal類實例的set或者get方法設(shè)置變量時,就會創(chuàng)建當(dāng)前線程的inheritableThreadLocals變量。當(dāng)父線程創(chuàng)建子線程時,構(gòu)造函數(shù)會把父線程中inheritableThreadLocals變量里面的本地變量復(fù)制一份保存到子線程的inheritableThreadLocals變量里面。

把1.11.3節(jié)中的代碼(1)修改為

    //(1) 創(chuàng)建線程變量
    public static ThreadLocal<String> threadLocal = new InheritableThreadLocal<Stri ng>();

運行結(jié)果如下。

thread:hello world
main:hello world

可見,現(xiàn)在可以從子線程正常獲取到線程變量的值了。

那么在什么情況下需要子線程可以獲取父線程的threadlocal變量呢?情況還是蠻多的,比如子線程需要使用存放在threadlocal變量中的用戶登錄信息,再比如一些中間件需要把統(tǒng)一的id追蹤的整個調(diào)用鏈路記錄下來。其實子線程使用父線程中的threadlocal方法有多種方式,比如創(chuàng)建線程時傳入父線程中的變量,并將其復(fù)制到子線程中,或者在父線程中構(gòu)造一個map作為參數(shù)傳遞給子線程,但是這些都改變了我們的使用習(xí)慣,所以在這些情況下InheritableThreadLocal就顯得比較有用。

主站蜘蛛池模板: 祁门县| 黄大仙区| 泰兴市| 印江| 佛教| 三河市| 黄陵县| 磐石市| 砚山县| 卓尼县| 石渠县| 利津县| 桦川县| 馆陶县| 临武县| 保德县| 砀山县| 邵东县| 三明市| 河池市| 荔波县| 牙克石市| 抚松县| 东阳市| 霍山县| 浑源县| 马边| 周至县| 城口县| 枞阳县| 康平县| 沅陵县| 疏附县| 黄冈市| 安新县| 开平市| 广西| 留坝县| 普兰店市| 克山县| 临潭县|