ThreadLocal簡(jiǎn)析

基本原理&使用場(chǎng)景

在多線程的并發(fā)訪問(wèn)的場(chǎng)景,除了使用鎖來(lái)控制不同線程對(duì)臨界區(qū)的訪問(wèn),來(lái)避免競(jìng)爭(zhēng),還有另外一種方式,就是ThreadLocal.
ThreadLocal中持有的數(shù)據(jù)只有當(dāng)前線程可以訪問(wèn),其他線程訪問(wèn)不了,這樣就避免了線程競(jìng)爭(zhēng)。

基本使用

下面是一個(gè)簡(jiǎn)單的threadLocal使用的例子,展示了ThreadLocal在多線程場(chǎng)景中的基本使用。

public class ThreadLocalDemo {

    private static final ThreadLocal<Integer> threadLocalFruit = new ThreadLocal<>();

    private static final ThreadLocal<Integer> threadLocalVegetable = new ThreadLocal<>();

    public static void main(String[] args) {
        new Thread(new ThreadLocalTestThread("農(nóng)人A", 3, 5)).start();
        new Thread(new ThreadLocalTestThread("農(nóng)人B", 7, 9)).start();
        LockSupport.park();
    }

    static void say(String words, Object... params) {
        System.out.println(MessageFormat.format(words, params));
    }

    static class ThreadLocalTestThread implements Runnable {

        private String name;

        private int fruitNum;

        private int vegetableNum;

        private Random random = new Random();

        public ThreadLocalTestThread(String name, int fruitNum, int vegetableNum) {
            this.name = name;
            this.fruitNum = fruitNum;
            this.vegetableNum = vegetableNum;

        }

        @Override
        public void run() {
            while (true) {
                say("{0} 收獲 {1} 個(gè)水果", name, fruitNum);
                threadLocalFruit.set(fruitNum);
                say("{0} 收獲 {1} 個(gè)蔬菜", name, vegetableNum);
                threadLocalVegetable.set(vegetableNum);
                try {
                    TimeUnit.MICROSECONDS.sleep(random.nextInt(500));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                say("{0} 用掉 {1} 個(gè)水果", name, threadLocalFruit.get());
                say("{0} 用掉 {1} 個(gè)蔬菜", name, threadLocalVegetable.get());
            }
        }
    }
}

程序的執(zhí)行結(jié)果如下,從執(zhí)行結(jié)果來(lái)看,【農(nóng)人1】線程對(duì)threadlocal的set僅【農(nóng)人1】可見,【農(nóng)人2】線程對(duì)threadlocal的set僅【農(nóng)人2】可見,兩者對(duì)threadloacl的get/set互不影響。
農(nóng)人B 收獲 7 個(gè)水果
農(nóng)人A 收獲 3 個(gè)水果
農(nóng)人B 收獲 9 個(gè)蔬菜
農(nóng)人A 收獲 5 個(gè)蔬菜
農(nóng)人B 用掉 7 個(gè)水果
農(nóng)人B 用掉 9 個(gè)蔬菜
農(nóng)人A 用掉 3 個(gè)水果
農(nóng)人B 收獲 7 個(gè)水果
農(nóng)人A 用掉 5 個(gè)蔬菜
農(nóng)人B 收獲 9 個(gè)蔬菜
農(nóng)人A 收獲 3 個(gè)水果
農(nóng)人A 收獲 5 個(gè)蔬菜
農(nóng)人B 用掉 7 個(gè)水果
農(nóng)人B 用掉 9 個(gè)蔬菜
農(nóng)人A 用掉 3 個(gè)水果
農(nóng)人B 收獲 7 個(gè)水果
農(nóng)人A 用掉 5 個(gè)蔬菜
農(nóng)人B 收獲 9 個(gè)蔬菜
農(nóng)人A 收獲 3 個(gè)水果
農(nóng)人A 收獲 5 個(gè)蔬菜

原理介紹

這么神奇的特性是怎么做到的呢?先看ThreadLocal的get方法源碼.
從get方法的源代碼來(lái)看,從threadlocal獲取數(shù)據(jù)的時(shí)候,是從當(dāng)前線程對(duì)象的ThreadLocalMap中獲取的,所以獲取到的數(shù)據(jù)線程鎖特有的,不會(huì)被其他線程修改。

   public T get() {
        Thread t = Thread.currentThread();
        //每個(gè)線程都有自己的ThreadLocalMap
        //ThreadLocalMap保存threadlocal中的數(shù)據(jù)
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //如果當(dāng)前線程的ThreadLocalMap為空,則初始化ThreadLocalMap,
        //并將initialValue方法的返回值放入到ThreadLocalMap中。
        return setInitialValue();
    }
    
   ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }  

從下面的代碼來(lái)看, ThreadLocalMap的key是一個(gè)弱引用,弱應(yīng)用關(guān)聯(lián)的對(duì)象內(nèi)存在jvm gc的時(shí)候會(huì)被回收掉,可以避免key導(dǎo)致的內(nèi)存泄露的問(wèn)題,但value仍然是強(qiáng)引用,當(dāng)key被回收之后,value就獲取不到了,可能導(dǎo)致內(nèi)存泄露。既然這樣,為什么key要使用弱引用呢?詳見【ThreadLocal內(nèi)存泄露風(fēng)險(xiǎn)分析】

static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

ThreadLocal內(nèi)存泄露風(fēng)險(xiǎn)分析

上面提到,既然弱引用的key被回收掉之后會(huì)導(dǎo)致value的內(nèi)存泄露,那為什么key仍然要使用弱應(yīng)用呢?當(dāng)ThreadLocal對(duì)象沒有強(qiáng)應(yīng)用時(shí),它們需要被清理掉,如果key是強(qiáng)引用,當(dāng)引用threadLocal的對(duì)象都被回收掉時(shí),因?yàn)閗ey是強(qiáng)引用,還指向threadLocal,導(dǎo)致threadlocal無(wú)法被回收掉。從下面的引用關(guān)系圖能比較直觀的看到這個(gè)問(wèn)題。


threadlocalmap1.jpg

既然可能會(huì)造成內(nèi)存泄露,那怎么解決這個(gè)問(wèn)題呢。ThreadLocal給出的解決方案是在get/set的時(shí)候,會(huì)調(diào)用expungeStaleEntry去清理key=null的value和entry。

 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-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;
                }
                //這里k為null,則執(zhí)行value、entry的清理動(dòng)作
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

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

數(shù)據(jù)結(jié)構(gòu)&算法

ThreadLocal的核心數(shù)據(jù)結(jié)構(gòu)就是ThreadLocalMap,核心的代碼如下:
這里會(huì)即將k為null的value也設(shè)置為null,然后將entry也設(shè)置為null,這樣gc就可以回收掉了。然后對(duì)于k不為null的entry使用了高德斯算法(俗稱洗牌算法)來(lái)重排,保證table中的元素隨機(jī)分布,盡量避免hash沖突。
此外,從set方法源碼也知,ThreadLocalMap 解決hash沖突使用的是開放地址法,就是一直在數(shù)組中尋找null的位置放入當(dāng)前數(shù)據(jù)。解決hash沖突另一個(gè)比較有名的算法是鏈?zhǔn)降刂贩?,HashMap就是經(jīng)典案例,只不過(guò)java8之后,hashMap做了改進(jìn)改為使用紅黑樹了。

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

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

其他

1、為避免threadlocal造成的內(nèi)存泄露,要在最后調(diào)用remove方法。
2、正常情況,當(dāng)Thread執(zhí)行完會(huì)被銷毀,Thread指向的threadLocalMap就會(huì)變?yōu)槔?,里面的entry也就會(huì)被回收掉。
3、發(fā)生內(nèi)存泄露的場(chǎng)景一般是在線程池的場(chǎng)景,當(dāng)線程場(chǎng)景存活,key是弱引用被回收掉之后,key變成null,value的值還在,就可能出現(xiàn)內(nèi)存泄露
4、在線程池的場(chǎng)景,如果在最后沒有做remove,還可能會(huì)導(dǎo)致ThreadLocal里的數(shù)據(jù)被二次改寫,造成多線程修改同個(gè)ThreadLocal的假象,其實(shí)是相同的線程對(duì)象被復(fù)用后,二次修改原線程對(duì)象ThreadLocal數(shù)據(jù)的問(wèn)題。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • ThreadLocal 是什么? ThreadLocal,即線程局部變量,是一個(gè)線程內(nèi)部的數(shù)據(jù)存儲(chǔ)類,通過(guò)它可以在...
    tandeneck閱讀 353評(píng)論 0 0
  • 1. ThreadLocal簡(jiǎn)介 顧名思義,ThreadLocal的功能就是為每一個(gè)使用局部變量的線程都提供一個(gè)變...
    MadnessXiong閱讀 253評(píng)論 0 0
  • 一、簡(jiǎn)介 并發(fā)編程中,當(dāng)訪問(wèn)共享數(shù)據(jù)時(shí),通常需要使用同步技術(shù)。但如果數(shù)據(jù)不發(fā)布(逸出)到線程以外,僅僅在單線程中被...
    邱simple閱讀 3,523評(píng)論 3 12
  • 前言 源碼 基于 Android SDK 28 JDK 1.8 說(shuō)起 ThreadLocal,大家可能會(huì)比較...
    SYfarming閱讀 1,091評(píng)論 0 1
  • 原理 產(chǎn)生線程安全問(wèn)題的根源在于多線程之間的數(shù)據(jù)共享。如果沒有數(shù)據(jù)共享,就沒有多線程并發(fā)安全問(wèn)題。ThreadLo...
    Java耕耘者閱讀 329評(píng)論 0 0

友情鏈接更多精彩內(nèi)容