<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Java面經(jīng)之ThreadLocal底層原理

          共 9717字,需瀏覽 20分鐘

           ·

          2021-03-26 21:49

          點(diǎn)擊藍(lán)字關(guān)注我們,獲取更多面經(jīng)







          ThreadLocal底層原理







          1 概述

          ThreadLocal是java.lang包中的一個(gè)類,用來實(shí)現(xiàn)變量的線程封閉性,即只有當(dāng)前線程可以操作該變量,通過把一個(gè)變量存在當(dāng)前線程的一個(gè)Map容器中來實(shí)現(xiàn)。當(dāng)然,這樣解釋很抽象,對(duì)于一些人來說難以理解,這里我先介紹一個(gè)《java并發(fā)編程實(shí)戰(zhàn)》中描述的jdbc應(yīng)用場景,來讓你知道ThreadLocal到底有什么用。


          2 ThreadLocal的意義

          我們知道,當(dāng)在普通方法中創(chuàng)建一個(gè)變量類,若沒有特別在方法區(qū)域外留有該類的引用,當(dāng)方法結(jié)束后在其它地方不能夠再使用這個(gè)類。當(dāng)我們?cè)谄渌胤竭€要用到方法中創(chuàng)建的對(duì)象時(shí),我們通常會(huì)用一個(gè)全局變量指向這個(gè)對(duì)象,這樣在整個(gè)項(xiàng)目中都能再訪問這個(gè)對(duì)象了。但想想,在多線程情況下,每個(gè)線程訪問該方法都會(huì)創(chuàng)建一個(gè)全局對(duì)象,在高并發(fā)下那我們豈不是要?jiǎng)?chuàng)建成千上萬個(gè)全局變量來存?若該對(duì)象還帶有每個(gè)線程特有的參數(shù),那就要保證每個(gè)線程在之后能調(diào)用自己的創(chuàng)建的對(duì)象,一般情況下是很難進(jìn)行管理的。而ThreadLocal就做到了既能讓對(duì)象在其它地方被創(chuàng)建線程訪問,也省去了自己管理全局對(duì)象的麻煩。


          一般來講,在單線程應(yīng)用程序中可能會(huì)維持一個(gè)全局的數(shù)據(jù)庫連接,并在程序啟動(dòng)時(shí)初始化這個(gè)連接對(duì)象,從而避免在調(diào)用每個(gè)方法(save,get等)時(shí)都要傳遞一個(gè)Connection對(duì)象。由于Jdbc連接對(duì)象不一定是線程安全的,因此,當(dāng)多線程應(yīng)用程序在沒有協(xié)同的情況下使用全局變量時(shí),就不是線程安全的,比如線程一剛獲取全局的Connection,準(zhǔn)備進(jìn)行數(shù)據(jù)庫操作,但線程二卻執(zhí)行了Connection.close()。


          這種情況下我們可能會(huì)取消Connection這個(gè)全局變量,在每次要進(jìn)行數(shù)據(jù)庫相關(guān)操作時(shí)直接new一個(gè)Connection對(duì)象進(jìn)行連接,而這又會(huì)導(dǎo)致一個(gè)線程執(zhí)行多次數(shù)據(jù)庫操作時(shí)要new多個(gè)connection對(duì)象,加大系統(tǒng)的負(fù)擔(dān)。這時(shí)就會(huì)想能不能有這樣一種方法,既不讓Connection成為全局變量來保證線程安全,又可以實(shí)現(xiàn)全局Connection帶來的“一次連接”式的便利?ThreadLocal就是做這件事的。


          3 簡要應(yīng)用

          下面第一行代碼new了一個(gè)靜態(tài)的ThreadLocal<Connection>。以后只要讓每個(gè)線程在進(jìn)行jdbc操作前都執(zhí)行第二行代碼,就會(huì)把一個(gè)創(chuàng)建的Connection對(duì)象存放到當(dāng)前線程的map容器中(ThreadLocalMap,見下文介紹),后面該線程要進(jìn)行數(shù)據(jù)庫操作時(shí)只要執(zhí)行第三行代碼,就能拿到這個(gè)引用進(jìn)行連接,不用每次都new一個(gè)新的。


          private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<>(); connectionHolder.set(DriverManager.getConnection(DB_URL)); connectionHolder.get();


          你可能會(huì)問只是一句簡單的connectionHolder.get()代碼,而且ThreadLocal對(duì)象只有一個(gè),那怎么能準(zhǔn)確的拿到當(dāng)前線程中的Connection呢?答案就是:這個(gè)connection是存在當(dāng)前線程(一個(gè)Thread)中的,不是ThreadLocal中。表面上調(diào)用的ThreadLocal.get(),實(shí)際上是在當(dāng)前線程對(duì)象的Map容器中進(jìn)行設(shè)置和查找。不過當(dāng)前線程的map容器中可能會(huì)存多個(gè)ThreadLocal的值,所以Map中的key就是ThreadLoca對(duì)象,值就是connection,來進(jìn)行區(qū)分。還不懂的話見下面的代碼解析,很簡單。


          4 源代碼分析

          ThreadLocalMap是ThreadLocal類中的靜態(tài)內(nèi)部類,可以看成一個(gè)map容器。Thread類中有一個(gè)全局變量threadLocals 就是ThreadLocalMap類型,它才是ThreadLocal存儲(chǔ)數(shù)據(jù)的真正容器。


          public class Thread implements Runnable {/* ThreadLocal values pertaining to this thread. This map is maintained     * by the ThreadLocal class. */    ThreadLocal.ThreadLocalMap threadLocals = null;}


          4.1 ThreadLocalMap底層結(jié)構(gòu)

          在看ThreadLocal的底層代碼之前,我們先來看看ThreadLocalMap的操作,這樣有利于接下來更好地理解。

          static class ThreadLocalMap {    private static final int INITIAL_CAPACITY = 16;     private int size = 0;     private int threshold;     private Entry[] table;    //Thread的map容器     // 構(gòu)造方法,在第一次調(diào)用ThreadLocal.set()時(shí)會(huì)new一個(gè)ThreadLocalMap    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);    }     static class Entry extends WeakReference<ThreadLocal<?>> {            Object value;            Entry(ThreadLocal<?> k, Object v) {                super(k);                value = v;            }     }}


          可能會(huì)好奇,table中的Entry只有一個(gè)value,并沒有像Hashmap中那樣是鍵值對(duì)啊,為什么叫它map容器呢?其實(shí)Entry是有key的,key就是構(gòu)造Entry時(shí)傳入的ThreadLocal參數(shù),不過指向ThreadLocal的成員變量是從Reference類中繼承的。從Entry的構(gòu)造方法中可看到傳入了一個(gè)ThreadLocal對(duì)象,它就是key,我們追蹤super(k),最后發(fā)現(xiàn)在Entry的父類Reference中,傳給了成員變量referent。至于這個(gè)referent是怎么用到容器的操作上,下面馬上會(huì)看到。

          public abstract class Reference<T> {    private T referent;           // key。注入了傳入的ThreadLocal,被Entry繼承      //ThreadLocal最終傳給了參數(shù)referent    Reference(T referent, ReferenceQueue<? super T> queue) {        this.referent = referent;        this.queue = (queue == null) ? ReferenceQueue.NULL : queue;    }     // 獲取“key”    public T get() {        return this.referent;    }}


          4.2 ThreadLocalMap的set方法

          set方法有點(diǎn)像HashMap的put方法,HashMap中也是用一個(gè)Node[] table來存儲(chǔ)數(shù)據(jù),Node里面包含了鍵值對(duì),而這里Entry也是<referent,value>鍵值對(duì)。


          首先根據(jù)根據(jù)key的hashcode計(jì)算出插入的Entry在table中的位置,然后從計(jì)算出的位置向前循環(huán)遍歷,直到找到的Entry為null或key為null(被GC了),就把Entry插入到該位置。在遍歷的過程中會(huì)判斷遍歷元素與插入元素的是否是相同的Entry,規(guī)則是比較Entry的key即referent??梢钥吹较旅嫱ㄟ^Entry.get()來獲取key,而get()方法繼承自Reference父類中,就是返回referent屬性。

           

             private void set(ThreadLocal<?> key, Object value) {            Entry[] tab = table;            int len = tab.length;             //根據(jù)ThreadLocal的hash值計(jì)算應(yīng)該從table中哪個(gè)位置開始(跟HashMap一樣)            int i = key.threadLocalHashCode & (len-1);             //遍歷table中的每一個(gè)Entry            for (Entry e = tab[i];                 e != null;                 e = tab[i = nextIndex(i, len)]) {                ThreadLocal<?> k = e.get();                 //如果找到相同的key,覆蓋并返回                if (k == key) {                    e.value = value;                    return;                }                 //如果發(fā)現(xiàn)了空key為空,即ThreadLocal對(duì)象被GC了,就把數(shù)據(jù)放到該位置                if (k == null) {                    replaceStaleEntry(key, value, i);                    return;                }            }             //當(dāng)返現(xiàn)e==null時(shí),執(zhí)行下面代碼,new再檢測(cè)是否擴(kuò)容            tab[i] = new Entry(key, value);            int sz = ++size;            if (!cleanSomeSlots(i, sz) && sz >= threshold)                rehash();        }


          4.3 ThreadLocalMap的getEntry方法

          首先根據(jù)key找到table中的一個(gè)位置,如果該位置上的元素不為空且key等于傳入的key,則直接返回該位置上的Entry;若位置上的元素為null或key為null,則返回null;否則向前循環(huán)遍歷table直到發(fā)現(xiàn)key相等的Entry返回,或遍歷到null直接返回null。

           

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


          4.4 ThreadLocal的set方法

          先拿到當(dāng)前線程的ThreadLocalMap容器,若線程的容器為null(一次都沒有使用過)則初始化,創(chuàng)建一個(gè)新的ThreadLocalMap同時(shí)將key和value傳入,插入到指定位置,ThreadLocalMap的構(gòu)造方法參見4.1中的代碼。若不為空則直接調(diào)用該容器的set方法插入鍵值對(duì)。兩種方法都是把自己當(dāng)key傳入。

              public void set(T value) {        Thread t = Thread.currentThread();    //獲取當(dāng)前線程對(duì)象        ThreadLocalMap map = getMap(t);       //拿到t的map容器        if (map != null)                    map.set(this, value);            //map不空,根據(jù)key(調(diào)用set方法的ThreadLocal對(duì)象)設(shè)置值        else            createMap(t, value);             //map為空則創(chuàng)建一個(gè)map(第一次插入,有點(diǎn)怪怪的)    }     // 從當(dāng)前線程中獲取ThreadLocalMap     ThreadLocalMap getMap(Thread t) {        return t.threadLocals;    }     // 初始化當(dāng)前線程的ThreadLocalMap容器    void createMap(Thread t, T firstValue) {        t.threadLocals = new ThreadLocalMap(this, firstValue);    }


          4.5 ThreadLocal的get方法

          很簡單,直接調(diào)用的ThreadLocalMap的get方法,把自己作為key傳入getEntry方法中。若容器為null,則調(diào)用setInitialValue方法。

          public T get() {        Thread t = Thread.currentThread();        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;            }        }        return setInitialValue();    }


          4.6ThreadLocal的setInitialValue方法

          會(huì)返回一個(gè)初始值,由initialValue返回??梢钥吹絠nitialValue方法只是返回了一個(gè)null,如果我們不重寫它的話。同時(shí)再加一個(gè)判斷,若容器為null,則跟set方法一樣進(jìn)行初始化,不過這里的value不是傳入的,而是initialValue,默認(rèn)為null。

          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;    } protected T initialValue() {        return null;    }


          5 案例理解

          下面創(chuàng)建了5個(gè)線程,在每個(gè)線程執(zhí)行期間調(diào)用了兩個(gè)ThreadLocal對(duì)象的set方法,然后打印get方法的返回值。顯然同一個(gè)ThreadLocal在不同線程中調(diào)用get和set方法是能區(qū)分線程的。

          public class Test1 {     public static void main(String[] args) {        for (int i=0;i<5;++i){            new MyThread().start();        }    }} class MyThread extends Thread{     private static final ThreadLocal<String> threadLocal1 = new ThreadLocal<String>();    private static final ThreadLocal<String> threadLocal2 = new ThreadLocal<String>();     @Override    public void run(){        threadLocal1.set(getName()+":調(diào)用了threadLocal1的set方法");        threadLocal2.set(getName()+":調(diào)用了threadLocal2的set方法");        System.out.println(threadLocal1.get());        System.out.println(threadLocal2.get());    }} /*---------------打印結(jié)果-----------------Thread-0:調(diào)用了threadLocal1的set方法Thread-0:調(diào)用了threadLocal2的set方法Thread-2:調(diào)用了threadLocal1的set方法Thread-2:調(diào)用了threadLocal2的set方法Thread-3:調(diào)用了threadLocal1的set方法Thread-4:調(diào)用了threadLocal1的set方法Thread-3:調(diào)用了threadLocal2的set方法Thread-4:調(diào)用了threadLocal2的set方法Thread-1:調(diào)用了threadLocal1的set方法Thread-1:調(diào)用了threadLocal2的set方法*/


          5 注意:臟讀、內(nèi)存泄漏

          5.1 臟讀

          當(dāng)使用線程池的時(shí)候,由于工作線程是循環(huán)利用的,上一個(gè)任務(wù)線程通過ThreadLocal在工作線程中存入了數(shù)據(jù),下一個(gè)任務(wù)線程被該工作線程執(zhí)行時(shí),依然能夠讀到上一個(gè)任務(wù)線程存入的數(shù)據(jù),也就是讀到了臟數(shù)據(jù)。


          解決辦法就是在一個(gè)線程執(zhí)行結(jié)束后將數(shù)據(jù)通過ThreadLocal.remove()刪除掉。


          5.2 內(nèi)存泄漏

          由于ThreadLocalMap是以弱引用的方式引用著ThreadLocal,換句話說,就是ThreadLocal是被ThreadLocalMap以弱引用的方式關(guān)聯(lián)著,因此如果ThreadLocal沒有被ThreadLocalMap以外的對(duì)象引用(如手動(dòng)令ThreadLocal = null或下面紅色字體),則在下一次GC的時(shí)候,ThreadLocal實(shí)例就會(huì)被回收,那么此時(shí)ThreadLocalMap里的一組KV的K就是null了,因此在沒有額外操作的情況下,此處的V便不會(huì)被外部訪問到,而且只要Thread實(shí)例一直存在,Thread實(shí)例就強(qiáng)引用著ThreadLocalMap,因此ThreadLocalMap就不會(huì)被回收,那么這里K為null的V就一直占用著內(nèi)存。


          綜上,發(fā)生內(nèi)存泄露的條件是


          ThreadLocal實(shí)例沒有被外部強(qiáng)引用,比如我們假設(shè)在提交到線程池的task中實(shí)例化的ThreadLocal對(duì)象,當(dāng)task結(jié)束時(shí),ThreadLocal的強(qiáng)引用也就結(jié)束了

          ThreadLocal實(shí)例被回收,但是在ThreadLocalMap中的V沒有被任何清理機(jī)制有效清理

          當(dāng)前Thread實(shí)例一直存在,則會(huì)一直強(qiáng)引用著ThreadLocalMap,也就是說ThreadLocalMap也不會(huì)被GC

          也就是說,如果Thread實(shí)例還在,但是ThreadLocal實(shí)例卻不在了,則ThreadLocal實(shí)例作為key所關(guān)聯(lián)的value無法被外部訪問,卻還被強(qiáng)引用著,因此出現(xiàn)了內(nèi)存泄露。


          解決辦法就是創(chuàng)建ThreadLocal時(shí)將ThreadLocal變量設(shè)置成全局static類型,當(dāng)需要存儲(chǔ)線程私有的數(shù)據(jù)時(shí),通過全局的ThreadLocal變量來存,不要在普通方法體中定義局部的ThreadLocal變量來存數(shù)據(jù);不要手動(dòng)將ThreadLocal引用指向null。









          更多面經(jīng)





          360-測(cè)試開發(fā)面經(jīng)(一)


          百度-測(cè)試開發(fā)面經(jīng)(一)


          字節(jié)跳動(dòng)-測(cè)試開發(fā)面經(jīng)(一)


              掃描二維碼

             獲取更多面經(jīng)

            扶搖就業(yè)  


          瀏覽 46
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  俺来也网 | 最新亚洲中文字幕 | 国产操逼视频播放 | 五月丁香夫妻自拍偷拍 | 天天添天天爽 |