天天看點

java多線程之ThreadLocal源碼分析

什麼是ThreadLocal?

關于ThreadLocal的知識網上有很多,但參差不齊很片面,看了很多部落格後發現有一篇寫的很全面客觀,貼出來大家可以自行觀看:http://www.iteye.com/topic/103804

下面講一下我自己的了解:線程本地存儲區(Thread Local Storage,簡稱為TLS),每個線程都有自己的私有的本地存儲區域,不同線程之間彼此不能通路對方的TLS區域。ThreadLocal不是用來解決共享對象的多線程通路問題的,一般情況下,通過ThreadLocal.set() 到線程中的對象是該線程自己使用的對象,其他線程是不需要通路的,也通路不到的。各個線程中通路的是不同的對象。

ThreadLocal的結構

首先我們來看一下ThreadLocal這個類的結構:

java多線程之ThreadLocal源碼分析

可以看到除了ThreadLocal的一些方法和變量之外,還有兩個靜态内部類:ThreadLocalMap和SuppliedThreadLocal,其中ThreadLocal是實作的關鍵,我們來看一下他的結構:

java多線程之ThreadLocal源碼分析

ThreadLocalMap裡還有一個内部類Entry,其實每個線程存的值都在這裡實作:

java多線程之ThreadLocal源碼分析

ThreadLocal執行個體

如果隻是單純的講這個類難免枯燥,我們結合一個執行個體來講;比如我們要在一個類中放一個String類型的字元串,讓每個線程都能設定和獲得不同的字元串,我們可以這樣寫:

public class Test {
    public static ThreadLocal<String> threadLocal = new ThreadLocal<>();


    static class Thread1 extends Thread{

        @Override
        public void run() {
            //在新線程中設定值
            threadLocal.set("a");
            System.out.println("線程:"+Thread.currentThread()+"值:"+threadLocal.get());
        }
    }

    public static void main(String[] args) {
        //在主線程中設定值
        threadLocal.set("main");

        Thread1 thread1 = new Thread1();
        thread1.start();

        System.out.println("線程:"+Thread.currentThread()+"值:"+threadLocal.get());
    }
}
           

首先在Test類中建立一個ThreadLocal對象,指定泛型類型為String,然後在Test類中建立一個靜态内部類Thread1,在他的run方法中給ThreadLocal設定值,然後取出值。在mian方法中首先給ThreadLocal設定一個值,然後開啟Thread1線程,運作結果如下:

線程:Thread[main,,main]值:main
線程:Thread[Thread-,,main]值:a
           

可以看到主線程中設定的值和主線程中列印的值是相對應的,而新線程中設定的值和新線程中列印的值是相對應的,這就是ThreadLocal。

執行個體分析

下面我們對這個執行個體進行分析,看ThreadLocal是如何在不同的線程中取出不同的值的:

在我們new出一個Threadlocal執行個體時,ThreadLocal做了那些操作呢,我們看一下他的構造函數:

/**
     * Creates a thread local variable.
     */
    public ThreadLocal() {
    }
           

我們可以看到他的構造函數是空的,也就是說當我們new一個執行個體時隻是初始化了成員變量,我們看一下他的靜态代碼和成員變量:

private final int threadLocalHashCode = nextHashCode();

    /**
     * The next hash code to be given out. Updated atomically. Starts at
     * zero.
     */
    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    /**
     * The difference between successively generated hash codes - turns
     * implicit sequential thread-local IDs into near-optimally spread
     * multiplicative hash values for power-of-two-sized tables.
     */
    private static final int HASH_INCREMENT = ;

    /**
     * Returns the next hash code.
     */
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
           

其中有隻有一個threadLocalHashCode是在建立對象時初始化的,是以ThreadLocal執行個體的變量隻有這個threadLocalHashCode,而且是final的,用來區分不同的ThreadLocal執行個體,至于為什麼要這樣一個變量後面會詳細說。

接下來通過threadLocal.set(“main”)來設定值,這個方法很關鍵:

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
           

首先擷取到目前線程的執行個體,然後在通過 getMap(t)方法擷取到與線程綁定的ThreadLocalMap執行個體,如果擷取到的執行個體不為空,則通過ThreadLocalMap的set方法設定值,否則建立一個ThreadLocalMap執行個體。

我雖然隻用幾句話總結了這個方法,但其實裡面的具體實作還是挺複雜的,我們來看一下 getMap(t)和createMap(t, value)這兩個方法:

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

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

getMap是傳回一個ThreadLocalMap執行個體,而createMap是建立一個ThreadLocalMap執行個體,注意看建立的執行個體是放在那裡的,t.threadLocals也就是Thread裡的變量,我們看一下Thread類裡的這個屬性:

/* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
           

這下就清楚了,我們儲存值的時候,首先會通過Thread.currentThread()擷取目前線程的執行個體,然後通過這個執行個體的getMap(t)方法擷取線程裡存放的ThreadLocalMap對象,如果目前線程裡的ThreadLocalMap對象為空則建立一個執行個體放到該線程中,這樣每個線程都擁有一個不同的ThreadLocalMap執行個體,而這個執行個體就是不同線程儲存不同值的關鍵。搞清了這一點就基本了解ThreadLocal的原理了。

接着我們在看一下ThreadLocalMap的構造函數:

ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - );
            table[i] = new Entry(firstKey, firstValue);
            size = ;
            setThreshold(INITIAL_CAPACITY);
        }
           

首先初始化一個Entry數組,關于Entry其實他就相當于一個鍵值對,ThreadLocal是他的鍵(key),我們存放的值是他的值(values),原來我們的值最終是存到了這個Entry中了。然後通過ThreadLocal的threadLocalHashCode 屬性計算出一個值,還記得這個變量嗎,其實他的作用就是映射一個ThreadLocal在Entry數組中的位置,由于每個Threadlocal的threadLocalHashCode值是不一樣的,這樣當我們取值的時候,通過這個threadLocalHashCode 就可以找到我們存儲的Entry對象的位置,這裡也許有人會好奇了,為什麼不直接通過key也就是ThreadLocal在數組中查詢呢,hashMap不就是這樣做的嗎,其實hashMap底層也是使用的hash表來查詢的,這樣做的好處是查詢快。我們通過threadLocalHashCode計算出了目前ThreadLocal的Entry對象在Entry數組中的位置,接着我們就把Entry對象初始化然後放到數組的對應位置。

為什麼要用一個Entry數組呢?直接将值存到一個Entry對象中,然後ThreadLocalMap持有這個執行個體不就行了嗎?剛開始我也在這裡糾結了好久,但是仔細一想,如果我們線程要存兩個值,但是一個線程隻有一個ThreadLocalMap執行個體,這顯然就不行了,是以要用Entry數組,将不同的值存到數組中,例:

public class Test {
    public static ThreadLocal<String> threadLocal = new ThreadLocal<>();
    public static ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>();


    static class Thread1 extends Thread{

        @Override
        public void run() {
            //在新線程中設定值
            threadLocal.set("a");
            threadLocal2.set();
            System.out.println("線程:"+Thread.currentThread()+"值:"+threadLocal.get());
            System.out.println("線程:"+Thread.currentThread()+"值:"+threadLocal2.get());
        }
    }

    public static void main(String[] args) {
        //在主線程中設定值
        threadLocal.set("main");
        threadLocal2.set();
        Thread1 thread1 = new Thread1();
        thread1.start();

        System.out.println("線程:"+Thread.currentThread()+"值:"+threadLocal.get());
        System.out.println("線程:"+Thread.currentThread()+"值:"+threadLocal2.get());
    }
}
           

運作結果:

線程:Thread[main,,main]值:main
線程:Thread[Thread-,,main]值:a
線程:Thread[main,,main]值:
線程:Thread[Thread-,,main]值:
           

現在還差一個map.set(this, value)方法:

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

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

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

這個方法其實就是省去了初始化Entry數組的過程,直接通過threadLocalHashCode計算出了目前ThreadLocal的Entry對象在Entry數組中的位置。

這裡我們先總結幾個知識點:

  1. ThreadLocal的構造函數是的,在初始化的時候隻初始化一個threadLocalHashCode變量,這是唯一辨別目前ThreadLocal對象的
  2. ThreadLocalMap的執行個體是存放在Thread中的,ThreadLocalMap的構造函數接收兩個參數:ThreadLocal和values
  3. 我們存的值最終是以鍵值對的形式存在Entry對象中的,ThreadLocal是鍵,values是值
  4. ThreadLocal進行set操作時會先擷取目前線程執行個體中的ThreadLocalMap對象,然後将值設定到ThreadLocalMap中存儲的Entry數組的Entry對象中。
  5. Entry對象存到Entry數組中的位置是由threadLocalHashCode計算得到的,這樣做是為了節省查找時間,不懂的百度一下hash表
  6. Entry數組是為了實作一個線程存儲多個值

下面我們看一下threadLocal.get()方法:

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null)
                return (T)e.value;
        }
        return setInitialValue();
    }
           

get方法也是要先擷取目前線程的執行個體,然後在通過 getMap(t)方法擷取到與線程綁定的ThreadLocalMap執行個體,如果擷取到的執行個體不為空則通過這個ThreadLocalMap的getEntry()方法擷取Entry執行個體,然後直接就擷取了Entry執行個體中的值就可以了,如果ThreadLocalMap為空就初始化一個null值傳回。

下面我們來詳細分析一下get方法, getMap(t)不用講了,和之前set裡的一樣,我們來看一下 map.getEntry(this)方法:

private Entry getEntry(ThreadLocal key) {
            int i = key.threadLocalHashCode & (table.length - );
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }
           

這裡就用到threadLocalHashCode 來擷取Entry對象在Entry數組中的位置,隻是簡單的數學計算就可以完成,可以說根本不用時間,但是如果一個個取出Entry對象再一個個比較他們的key,這浪費的時間可想而知,取出Entry對象後判斷是否為空,并且判斷一下取出的key是否是和目前ThreadLocal一樣 ,判斷完成後傳回Entry對象就ok了,至于如果Entry為空或者key值不一樣的處理大家自己去看getEntryAfterMiss(key, i, e)的源碼,我就不講了,這種情況一般不會出現。

我們再看一下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;
    }
           

這個方法調用的條件是我們之前沒有調用set方法給ThreadLocal設定值,這時是沒有值能夠取出來的,是以就調用一下該方法初始一個值,這個方法的第一行就是調用一個initialValue()方法,這個方法很重要,我們看一下他的實作:

protected T initialValue() {
        return null;
    }
           

傳回空,沒錯因為我們沒有值隻能傳回空,注意這個方法是可以重寫的,我們可以自定義他的預設值:

public static ThreadLocal<String> threadLocal = new ThreadLocal<String>(){
        @Override
        protected String initialValue() {
            return "defaultValues";
        }
    };
           
public static void main(String[] args) {
        //在主線程中設定值
//        threadLocal.set("main");
        threadLocal2.set();
        Thread1 thread1 = new Thread1();
        thread1.start();

        System.out.println("線程:"+Thread.currentThread()+"值:"+threadLocal.get());
        System.out.println("線程:"+Thread.currentThread()+"值:"+threadLocal2.get());
    }
線程:Thread[main,,main]值:defaultValues
線程:Thread[main,,main]值:
線程:Thread[Thread-,,main]值:a
線程:Thread[Thread-,,main]值:
           

在調用了initialValue()方法之後再次對ThreadLocalMap是否為空做一次判斷,然後掉用 map.set(this, value)或者createMap(t, value)方法将我們初始化的值設定到Entry中。

這裡再總結幾個知識點:

  1. ThreadLocal進行get操作時也會先擷取目前線程執行個體中的ThreadLocalMap對象,然後通過該對象擷取Entry,進而擷取到存儲的值。
  2. ThreadLocal是有預設的值的,可以自定義也可以為空,預設的值也會存入Entry對象中。

到這裡ThreadLocal的原理已經講完了,可能有人會說這個東西根本沒用到過,我們學他幹嘛,我想說我們看源碼更多的不是為了去使用它,如果隻是ThreadLocal的使用我想幾百字就解決了,我們是為了了解他的思想和解決問題的思路,這才是能提升我們的東西。