天天看點

二刷Java多線程:ThreadLocal詳解

1、ThreadLocal簡介

ThreadLocal是一個以ThreadLocal對象為鍵、任意對象為值的存儲結構,提供了線程本地變量,也就是如果建立了一個ThreadLocal變量,那麼通路這個變量的每個線程都會有這個變量的一個本地副本。當多個線程操作這個變量時,實際操作的是自己本地記憶體裡面的變量,進而避免了線程安全問題。建立一個ThreadLocal變量後,每個線程都會複制一個變量到自己的本地記憶體

二刷Java多線程:ThreadLocal詳解

ThreadLocal的内部結構圖如下:

二刷Java多線程:ThreadLocal詳解

2、ThreadLocal使用示例

public class ThreadLocalTest {
    static void print(String str) {
        System.out.println(str + ":" + localVariable.get());
        localVariable.remove();
    }

    static ThreadLocal<String> localVariable = new ThreadLocal<String>();

    public static void main(String[] args) {
        new Thread(new Runnable() {
            public void run() {
                localVariable.set("threadOne local variable");
                print("threadOne");
                System.out.println("threadOne remove after :" + localVariable.get());
            }
        }).start();

        new Thread(new Runnable() {
            public void run() {
                localVariable.set("threadTwo local variable");
                print("threadTwo");
                System.out.println("threadTwo remove after :" + localVariable.get());
            }
        }).start();
    }
}
           

運作結果:

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

3、ThreadLocal的實作原理

二刷Java多線程:ThreadLocal詳解

Thread類中有一個threadLocals和一個inheritableThreadLocals,它們都是ThreadLocalMap類型的變量,而ThreadLocalMap是一個定制化的HashMap。在預設情況下,每個線程中的這兩個變量都為null,隻有目前線程第一次調用ThreadLocal的set()或者get()方法時才會建立它們。其實每個線程的本地變量不是存放在ThreadLocal執行個體裡面,而是存放在調用線程的threadLocals變量裡面。也就是說,ThreadLocal類型的本地變量存放在具體的線程記憶體空間中。ThreadLocal就是一個工具殼,它通過set()方法把value值放入調用線程的threadLocals裡面并存放起來,當調用線程調用它的get()方法時,再從目前線程的threadLocals變量裡面将其拿出來使用。如果調用線程一直不終止,那麼這個本地變量會一直存放在調用線程的threadLocals變量裡面,是以當不需要使用本地變量時可以通過調用ThreadLocal變量的remove()方法,從目前線程的threadLocals裡面删除該本地變量

1)、void set(T value)

public void set(T value) {
        //擷取目前線程
        Thread t = Thread.currentThread();
        //找到目前線程對應的threadLocals變量
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            //第一次調用就建立目前線程對應的threadLocals變量
            createMap(t, value);
    }

    ThreadLocalMap getMap(Thread t) {
        //擷取線程自己的變量threadLocals,threadLocals變量被綁定到了線程的成員變量上
        return t.threadLocals;
    }

    void createMap(Thread t, T firstValue) {
        //建立目前線程的threadLocals變量
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
           

2)、T get()

public T get() {
        //擷取目前線程
        Thread t = Thread.currentThread();
        //擷取目前線程的threadLocals變量
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //如果threadLocals不為null,則傳回對應本地變量的值
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //threadLocals為空則初始化目前線程的threadLocals成員變量
        return setInitialValue();
    }

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

    protected T initialValue() {
        return null;
    }
           

3)、void remove()

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

如果目前線程的threadLocals變量不為空,則删除目前線程中指定ThreadLocal執行個體的本地變量

4)、ThreadLocalMap

ThreadLocalMap是ThreadLocal的内部類,沒有實作Map接口,用獨立的方式實作了Map的功能,其内部的Entry也獨立實作的

二刷Java多線程:ThreadLocal詳解

在ThreadLocalMap中,也是用Entry來儲存K-V結構資料的,但是Entry中key隻能是ThreadLocal對象

static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;

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

Entry繼承自WeakReference(弱引用,生命周期隻能存活到下次GC前),但隻有key是弱引用類型的,value并非弱引用

private static final int INITIAL_CAPACITY = 16;

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

        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }
           

從ThreadLocalMap的構造函數可以得知,ThreadLocalMap初始化容量為16,負載因子為2/3

和HashMap的最大的不同在于,ThreadLocalMap結構非常簡單,沒有next引用,也就是說ThreadLocalMap中解決Hash沖突的方式并非連結清單的方式,而是采用線性探測的方式。所謂線性探測,就是根據初始key的hashcode值确定元素在table數組中的位置,如果發現這個位置上已經有其他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();

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

		private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }
           

在插入過程中,根據ThreadLocal對象的hash值,定位到table中的位置i,過程如下

  • 如果目前位置是空的,那麼正好,就初始化一個Entry對象放在位置i上
  • 位置i已有對象,如果這個Entry對象的key正好是即将設定的key,那麼覆寫value
  • 位置i的對象,和即将設定的key沒關系,那麼隻能找下一個空位置

5)、ThreadLocalMap記憶體洩露問題

二刷Java多線程:ThreadLocal詳解

上圖中,實線代表強引用,虛線代表的是弱引用,如果threadLocal外部強引用被置為null(

threadLocalInstance==null

)的話,threadLocal執行個體就沒有一條引用鍊路可達,很顯然在GC(垃圾回收)的時候勢必會被回收,是以entry就存在key為null的情況,無法通過一個Key為null去通路到該entry的value。同時,就存在了這樣一條引用鍊:

threadRef->currentThread->threadLocalMap->entry->valueRef->valueMemory

,導緻在垃圾回收的時候進行可達性分析的時候,value可達進而不會被回收掉,但是該value永遠不能被通路到,這樣就存在了記憶體洩漏。當然,如果線程執行結束後,threadLocal和threadRef會斷掉,是以threadLocal、threadLocalMap、entry都會被回收掉。可是,在實際使用中我們都是會用線程池去維護我們的線程,比如在

Executors.newFixedThreadPool()

時建立線程的時候,為了複用線程是不會結束的,是以threadLocal記憶體洩漏就值得我們關注

ThreadLocalMap的設計中已經做出了哪些改進?

ThreadLocalMap中的get()和set()方法都針對記憶體洩露問題做了相應的處理,下文為了叙述,針對key為null的entry,源碼注釋為stale entry,就稱之為“髒entry”

ThreadLocalMap的set()方法:

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

                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做了這樣的處理:

  • 如果目前table[i]不為空的話說明hash沖突就需要向後環形查找,若在查找過程中遇到髒entry就通過replaceStaleEntry()進行處理
  • 如果目前table[i]為空的話說明新的entry可以直接插入,但是插入後會調用cleanSomeSlots()方法檢測并清除髒entry

當我們調用threadLocal的get()方法時,當table[i]不是和所要找的key相同的話,會繼續通過threadLocalMap的getEntryAfterMiss()方法向後環形去找

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

        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)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }
           

當key為null的時候,即遇到髒entry也會調用expungeStleEntry()對髒entry進行清理

當我們調用threadLocal.remove()方法時候,實際上會調用threadLocalMap的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;
                }
            }
        }
           

同樣的可以看出,當遇到了key為null的髒entry的時候,也會調用expungeStaleEntry()清理掉髒entry

從以上set()、get()、remove()方法看出,在ThreadLocal的生命周期裡,針對ThreadLocal存在的記憶體洩漏的問題,都會通過expungeStaleEntry()、cleanSomeSlots()、replaceStaleEntry()這三個方法清理掉key為null的髒entry

想要更加深入學習ThreadLocal記憶體洩漏問題可以檢視這篇文章

小結:

二刷Java多線程:ThreadLocal詳解

在每個線程内部都有一個名為threadLocals的成員變量,該變量的類型為HashMap,其中key為我們定義的ThreadLocal變量的this引用,value則為我們使用set方法設定的值。每個線程的本地變量存放線上程自己的記憶體變量threadLocals中,如果目前線程一直不消亡,那麼這些本地變量會一直存在,是以可能會造成記憶體溢岀,因 此使用完畢後要記得調用ThreadLocal的remove()方法删除對應線程的threadLocals中的本地變量

4、InheritableThreadLocal類

同一個ThreadLocal變量在父線程中被設定值後,在子線程中是擷取不到的。而子類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的源碼中可知,InheritableThreadLocal繼承了ThreadLocal,并重寫了三個方法。由代碼(3)可知,InheritableThreadLocal重寫了createMap()方法,那麼現在當第一次調用set()方法時,建立的是目前線程的inheritableThreadLocals變量的執行個體而不再是threadLocals。由代碼(2)可知,當調用get()方法擷取目前線程内部的map變量時,擷取的是inheritableThreadLocals而不再是threadLocals

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

下面我們看一下重寫的代碼(1)何時執行,以及如何讓子線程可以通路父線程的本地變量。這要從建立Thread的代碼說起,打開Thread類的預設構造函數

public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }

    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize) {
        init(g, target, name, stackSize, null, true);
    }

    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name;

        //擷取目前線程,也就是父線程
        Thread parent = currentThread();
        SecurityManager security = System.getSecurityManager();
        if (g == null) {
            
            if (security != null) {
                g = security.getThreadGroup();
            }

            if (g == null) {
                g = parent.getThreadGroup();
            }
        }

        g.checkAccess();

        if (security != null) {
            if (isCCLOverridden(getClass())) {
                security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
            }
        }

        g.addUnstarted();

        this.group = g;
        this.daemon = parent.isDaemon();
        this.priority = parent.getPriority();
        if (security == null || isCCLOverridden(parent.getClass()))
            this.contextClassLoader = parent.getContextClassLoader();
        else
            this.contextClassLoader = parent.contextClassLoader;
        this.inheritedAccessControlContext =
                acc != null ? acc : AccessController.getContext();
        this.target = target;
        setPriority(priority);
        //如果父線程的inheritableThreadLocals變量不為null
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
			//設定子線程中的inheritableThreadLocals變量
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        
        this.stackSize = stackSize;

        tid = nextThreadID();
    }

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

在createlnheritedMap内部使用父線程的inheritableThreadLocals變量作為構造函數建立了一個新的ThreadLocalMap變量,然後指派給了子線程的inheritableThreadLocals變量

小結:

InheritableThreadLocal類通過重寫代碼(2)和(3)讓本地變量儲存到了具體線程的inheritableThreadLocals變量裡面,那麼線程在通過InheritableThreadLocal類執行個體的set()或者get()方法設定變量時,就會建立目前線程的inheritableThreadLocals變量。當父線程建立子線程時,構造函數會把父線程中inheritableThreadLocals變量裡面的本地變量複制一份儲存到子線程的inheritableThreadLocals變量裡面

參考:

https://www.jianshu.com/p/98b68c97df9b

https://blog.csdn.net/wsm0712syb/article/details/51025111

https://www.jianshu.com/p/dde92ec37bd1

繼續閱讀