欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

ThreadLocal作用原理與內(nèi)存泄露示例解析

 更新時(shí)間:2022年09月03日 16:15:57   作者:三雒  
這篇文章主要為大家介紹了ThreadLocal作用原理與內(nèi)存泄露示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

ThreadLocal作用

對(duì)于Android程序員來(lái)說(shuō),很多人都是在學(xué)習(xí)消息機(jī)制時(shí)候了解到ThreadLocal這個(gè)東西的。那它有什么作用呢?官方文檔大致是這么描述的:

  • ThreadLocal提供了線程局部變量
  • 每個(gè)線程都擁有自己的變量副本,可以通過(guò)ThreadLocal的set或者get方法去設(shè)置或者獲取當(dāng)前線程的變量,變量的初始化也是線程獨(dú)立的(需要實(shí)現(xiàn)initialValue方法)
  • 一般而言ThreadLocal實(shí)例在類中被private static修飾
  • 當(dāng)線程活著并且ThreadLocal實(shí)例能夠訪問到時(shí),每個(gè)線程都會(huì)持有一個(gè)到它的變量的引用
  • 當(dāng)一個(gè)線程死亡后,所有ThreadLocal實(shí)例給它提供的變量都會(huì)被gc回收(除非有其它的引用指向這些變量) 上述中“變量”是指ThreadLocal的get方法獲取的值

簡(jiǎn)單例子

先來(lái)看一個(gè)簡(jiǎn)單的使用例子吧:

public class ThreadId {
    private static final AtomicInteger nextId = new AtomicInteger(0);
    private static final ThreadLocal<Integer> threadId = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return nextId.get();
        }
    };
    public static int get() {
        return threadId.get();
    }
}

這也是官方文檔上的例子,非常簡(jiǎn)單,就是通過(guò)在不同線程調(diào)用ThredId.get()可以獲取唯一的線程Id。如果在調(diào)用ThreadLocal的get方法之前沒有主動(dòng)調(diào)用過(guò)set方法設(shè)置值的話,就會(huì)返回initialValue方法的返回值,并把這個(gè)值存儲(chǔ)為當(dāng)前線程的變量。

ThreadLocal到底是用來(lái)解決什么問題,適用什么場(chǎng)景呢,例子是看懂了,但好像還是沒什么體會(huì)?ThreadLocal既然是提供變量的,我們不妨把我們見過(guò)的變量類型拿出來(lái),做個(gè)對(duì)比

局部變量、成員變量 、 ThreadLocal、靜態(tài)變量

變量類型作用域生命周期線程共享性作用
局部變量方法(代碼塊)內(nèi)部,其他方法(代碼塊)不能訪問方法(代碼塊)開始到結(jié)束只存在于每個(gè)線程的工作內(nèi)存,不能在線程中共享解決變量在方法(代碼塊)內(nèi)部的代碼行之間的共享
成員變量實(shí)例內(nèi)和實(shí)例相同可在線程間共享解決變量在實(shí)例方法之間的共享,否則方法之間只能靠參數(shù)傳遞變量
靜態(tài)變量類內(nèi)部和類的生命周期相同可在多個(gè)線程間共享解決變量在多個(gè)實(shí)例之間的共享
ThreadLocal存儲(chǔ)的變量整個(gè)線程一般而言與線程的生命周期相同不再多線程間共享解決變量在單個(gè)線程中的共享問題,線程中處處可訪問

ThreadLocal存儲(chǔ)的變量本質(zhì)上間接算是Thread的成員變量,ThreadLocal只是提供了一種對(duì)開發(fā)者透明的可以為每個(gè)線程存儲(chǔ)同一維度成員變量的方式。

共享 or 隔離

網(wǎng)上有很多人持有如下的看法: ThreadLocal為解決多線程程序的并發(fā)問題提供了一種新思路或者ThreadLocal是為了解決多線程訪問資源時(shí)的共享問題。 個(gè)人認(rèn)為這些都是錯(cuò)誤的,ThreadLocal保存的變量是線程隔離的,與資源共享沒有任何關(guān)系,也沒有解決什么并發(fā)問題,這一點(diǎn)看了ThreadLocal的原理就會(huì)更加清楚。就好比上面的例子,每個(gè)線程應(yīng)該有一個(gè)線程Id,這并不是什么并發(fā)問題啊。

同時(shí)他們會(huì)拿ThreadLocal與sychronized做對(duì)比,我們要清楚它們根本不是為了解決同一類問題設(shè)計(jì)的。sychronized是在牽涉到共享變量時(shí)候,要做到線程間的同步,保證并發(fā)中的原子性與內(nèi)存可見性,典型的特征是多個(gè)線程會(huì)訪問相同的變量。而ThreadLocal根本不是解決線程同步問題的,它的場(chǎng)景是A線程保存的變量只有A線程需要訪問,而其它的線程并不需要訪問,其他線程也只訪問自己保存的變量。

原理

我們來(lái)一個(gè)開放性的問題,假如現(xiàn)在要給每個(gè)線程增加一個(gè)線程Id,并且Java的Thread類你能隨便修改,你要怎么操作?非常簡(jiǎn)單吧,代碼大概是這樣

public class Thread{
      private int id;
      public void setId(int id){
          this.id=id;
      }
}

那好,現(xiàn)在題目變了,我們現(xiàn)在還得為每個(gè)線程保存一個(gè)Looper對(duì)象,那怎么辦呢?再加一個(gè)Looper的字段不就好了,顯然這種做法肯定是不具有擴(kuò)展性的。那我們用一個(gè)容器類不就好了,很自然地就會(huì)想到Map,像下面這樣

public class Thread{
      private Map<String,Object> map;
     public Map<String,Object> getMap(){
         if(map==null)
            map=new HashMap<>();
         return map;
     }
}

然后我們?cè)诖a里就可以通過(guò)如下代碼來(lái)給Thread設(shè)置“成員變量”了

   Thread.currentThread().getMap().put("id",id);
   Thread.currentThread().getMap().put("looper",looper);

然后可以在該線程執(zhí)行的任意地方,這樣訪問:

  Looper looper=(Looper) Thread.currentThread().getMap().get("looper");

看上去還不錯(cuò),但是還是有些問題:

  • 保存和獲取變量都要用到字符換key
  • 因?yàn)閙ap中要保存各種值,因此泛型只得用Object,這樣獲取時(shí)候就需要強(qiáng)制轉(zhuǎn)換(可用泛型方法解)
  • 當(dāng)該變量沒有作用時(shí)候,此時(shí)線程還沒有執(zhí)行完,需要手動(dòng)設(shè)置該變量為空,否則會(huì)造成內(nèi)存泄漏

為了不通過(guò)字符串訪問,同時(shí)省去強(qiáng)制轉(zhuǎn)換,我們封裝一個(gè)類,就叫ThreadLocal吧,偽代碼如下:

  public class ThreadLocal<T> {
    public void set(T value) {
        Thread t = Thread.currentThread();
         Map map = t.getMap();
        if (map != null)
           //以自己為鍵
            map.put(this, value);
        else
            createMap(t, value);
    }
    public T get() {
        Thread t = Thread.currentThread();
        Map<ThreadLocal<?>,T> map = t.getMap();
        if (map != null) {
            T e = map.get(this);
            return e;
        }
        return setInitialValue();
    }
}

沒錯(cuò),以上基本上就是ThreadLocal的整體設(shè)計(jì)了,只是線程中存儲(chǔ)數(shù)據(jù)的Map是特意實(shí)現(xiàn)的ThreadLocal.ThreadLocalMap。

ThredLocal本身并不存儲(chǔ)變量,只是向每個(gè)線程的threadLocals中存儲(chǔ)鍵值對(duì)。ThreadLocal橫跨線程,提供一種類似切面的概念,這種切面是作用在線程上的。

我們對(duì)ThreadLocal已經(jīng)有一個(gè)整體的認(rèn)識(shí)了,接下來(lái)我們大致看一下源碼

源碼分析

TheadLocal

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

set方法通過(guò)Thread.currentThread方法獲取當(dāng)前線程,然后調(diào)用getMap方法獲取線程的threadLocals字段,并往ThreadLocalMap中放入鍵值對(duì),其中鍵為ThreadLocal實(shí)例自己。

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

接著看get方法:

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

很清晰,其中值得注意的是最后一行的setInitialValue方法,這個(gè)方法在我們沒有調(diào)用過(guò)set方法時(shí)候調(diào)用。

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

setInitialValue方法會(huì)獲取initialValue的返回值并把它放進(jìn)當(dāng)前線程的threadLocals中。默認(rèn)情況下initialValue返回null,我們可以實(shí)現(xiàn)這個(gè)方法來(lái)對(duì)變量進(jìn)行初始化,就像上面TheadId的例子一樣。

remove方法,從當(dāng)前線程的ThreadLocalMap中移除元素。

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

TheadLocalMap

看ThreadLocalMap的代碼我們主要是關(guān)注以下兩個(gè)方面:

  • 散列表的一般設(shè)計(jì)問題。包括散列函數(shù),散列沖突問題解決,負(fù)載因子,再散列等。
  • 內(nèi)存泄漏的相關(guān)處理。一般而言ThreadLocal 引用使用private static修飾,但是假設(shè)某種情況下我們真的不再需要使用它了,手動(dòng)把引用置空。上面我們知道TreadLocal本身作為鍵存儲(chǔ)在TheadLocalMap中,而ThreadLocalMap又被Thread引用,那線程沒結(jié)束的情況下ThreadLocal能被回收嗎?

散列函數(shù) 先來(lái)理一下散列函數(shù)吧,我們?cè)谥蟮拇a中會(huì)看到ThreadLocalMap通過(guò) int i = key.threadLocalHashCode & (len-1);決定元素的位置,其中表大小len為2的冪,因此這里的&操作相當(dāng)于取模。另外我們關(guān)注的是threadLocalHashCode的取值。

  private final int threadLocalHashCode = nextHashCode();
 private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
  private static AtomicInteger nextHashCode =
        new AtomicInteger();
   private static final int HASH_INCREMENT = 0x61c88647;

這里很有意思,每個(gè)ThreadLocal實(shí)例的threadLocalHashCode是在之前ThreadLocal實(shí)例的threadLocalHashCode上加 0x61c88647,為什么偏偏要加這么個(gè)數(shù)呢? 這個(gè)魔數(shù)的選取與斐波那契散列有關(guān)以及黃金分割法有關(guān),具體不是很清楚。它的作用是這樣產(chǎn)生的值與2的冪取模后能在散列表中均勻分布,即便擴(kuò)容也是如此??聪旅嬉欢未a:

  public class MagicHashCode {
      //ThreadLocal中定義的魔數(shù)
     private static final int HASH_INCREMENT = 0x61c88647;
     public static void main(String[] args) {
         hashCode(16);//初始化16
         hashCode(32);//2倍擴(kuò)容
         hashCode(64);
     }
    private static void hashCode(int length){         
        int hashCode = 0; 
         for(int i=0;i<length;i++){
            hashCode = i*HASH_INCREMENT+HASH_INCREMENT;
             System.out.print(hashCode & (length-1));//求取模后的下標(biāo)
             System.out.print(" ");
         }
         System.out.println();
     }
 }

輸出結(jié)果為:

7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 0   //容量為16時(shí)
7 14 21 28 3 10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25 0  //容量為32時(shí)
7 14 21 28 35 42 49 56 63 6 13 20 27 34 41 48 55 62 5 12 19 26 33 40 47 54 61 4 11 18 25 32 39 46 53 60 3 10 17 24 31 38 45 52 59 2 9 16 23 30 37 44 51 58 1 8 15 22 29 36 43 50 57 0  //容量為64時(shí)

因?yàn)門hreadLocalMap使用線性探測(cè)法解決沖突(下文會(huì)看到),均勻分布的好處在于發(fā)生了沖突也能很快找到空的slot,提高效率。

瞄一眼成員變量:

       /**
         * 初始容量,必須是2的冪。這樣的話,方便把取模運(yùn)算轉(zhuǎn)化為與運(yùn)算, 
         * 效率高
         */
        private static final int INITIAL_CAPACITY = 16;
        /**
         * 容納Entry元素,長(zhǎng)度必須是2的冪
         */
        private Entry[] table;
        /**
         * table中的元素個(gè)數(shù).
         */
        private int size = 0;
        /**
         * table里的元素達(dá)到這個(gè)值就需要擴(kuò)容了
         * 其實(shí)是有個(gè)裝載因子的概念的
         */
        private int threshold; // Default to 0

構(gòu)造函數(shù):

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

firstKey和firstValue就是Map存放的第一個(gè)鍵值對(duì)嘍。其中firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1)很關(guān)鍵,就是當(dāng)容量為2的冪時(shí)候,這相當(dāng)于一個(gè)取模操作。然后把Entry存儲(chǔ)到數(shù)組的第i個(gè)位置,設(shè)置擴(kuò)容的閾值。

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

這說(shuō)明當(dāng)數(shù)組里的元素容量達(dá)到2/3時(shí)候就要擴(kuò)容,也就是裝載因子是2/3。 接下來(lái)我們來(lái)看下Entry

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

就這么點(diǎn)東西,這個(gè)Entry只是與HashMap不同,只是個(gè)普通的鍵值對(duì),沒有鏈表結(jié)構(gòu)相關(guān)的東西。

另外Entry只持有對(duì)鍵,也就是ThreadLocal的弱引用,那么我們上面的第二個(gè)問題算是有答案了。當(dāng)沒有其他強(qiáng)引用指向ThreadLocal的時(shí)候,它其實(shí)是會(huì)被回收的。

但是這有引出了另外一個(gè)問題,那Entry呢?當(dāng)鍵都為空的時(shí)候這個(gè)Entry也是沒有什么作用啊,也應(yīng)該被回收啊。不慌,我們接著往下看。

set方法:

 private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
           //如果沖突的話,進(jìn)入該循環(huán),向后探測(cè)
            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) {
                   //該Entry對(duì)應(yīng)的ThreadLocal已經(jīng)被回收,執(zhí)行replaceStaleEntry并返回
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            tab[i] = new Entry(key, value);
            int sz = ++size;
            //進(jìn)行啟發(fā)式清理,如果沒有清理任何元素并且表的大小超過(guò)了閾值,需要擴(kuò)容并重哈希
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

我們發(fā)現(xiàn)如果發(fā)生沖突的話,整體邏輯會(huì)一直調(diào)用nextIndex方法去探測(cè)下一個(gè)位置,直到找到?jīng)]有元素的位置,邏輯上整個(gè)表是一個(gè)環(huán)形。下面是nextIndex的代碼,就是加1而已。

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

線性探測(cè)的過(guò)程中,有一種情況是需要清理對(duì)應(yīng)Entry的,也就是Entry的key為null,我們上面討論過(guò)這種情況下的Entry是無(wú)意義的。

因此調(diào)用 replaceStaleEntry(key, value, i);在看replaceStaleEntry(key, value, i)我們先明確幾個(gè)問題。

采用線性探測(cè)發(fā)解決沖突,在插入過(guò)程中產(chǎn)生沖突的元素之前一定是沒有空的slot的。這樣在也確保在查找過(guò)程,查找到空的slot就可以停止啦。

但是假如我們刪除了一個(gè)元素,就會(huì)破壞這種情況,這時(shí)需要對(duì)表中刪除的元素后面的元素進(jìn)行再散列,以便填上空隙。

空slot:即該位置沒有元素

無(wú)效slot:該位置有元素,但key為null

replaceStaleEntry除了將value放入合適的位置之外,還會(huì)在前后連個(gè)空的slot之間做一次清理expungeStaleEntry,清理掉無(wú)效slot。

private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                               int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;
    // 向前掃描到一個(gè)空的slot為止,找到離這個(gè)空slot最近的無(wú)效slot,記錄為slotToExpunge
    int slotToExpunge = staleSlot;
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len)) {
        if (e.get() == null) {
            slotToExpunge = i;
        }
    }
    // 向后遍歷table
    for (int i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        // 找到了key,將其與無(wú)效slot交換
        if (k == key) {
            // 更新對(duì)應(yīng)slot的value值
            e.value = value;
            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;
            //如果之前還沒有探測(cè)到過(guò)其他無(wú)效的slot
            if (slotToExpunge == staleSlot) {
                slotToExpunge = i;
            }
            // 從slotToExpunge開始做一次連續(xù)段的清理,再做一次啟發(fā)式清理
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }
        // 如果當(dāng)前的slot已經(jīng)無(wú)效,并且向前掃描過(guò)程中沒有無(wú)效slot,則更新slotToExpunge為當(dāng)前位置
        if (k == null && slotToExpunge == staleSlot) {
            slotToExpunge = i;
        }
    }
    // 如果key之前在table中不存在,則放在staleSlot位置
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);
    // 在探測(cè)過(guò)程中如果發(fā)現(xiàn)任何其他無(wú)效slot,連續(xù)段清理后做啟發(fā)式清理
    if (slotToExpunge != staleSlot) {
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
    }
}

expungeStaleEntry主要是清除連續(xù)段之前無(wú)效的slot,然后對(duì)元素進(jìn)行再散列。返回下一個(gè)空的slot位置。

 private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            // 刪除 staleSlot
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;
            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 {
                   //對(duì)元素進(jìn)行再散列
                    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;
        }

啟發(fā)式地清理: i對(duì)應(yīng)是非無(wú)效slot(slot為空或者有效) n是用于控制控制掃描次數(shù) 正常情況下如果log n次掃描沒有發(fā)現(xiàn)無(wú)效slot,函數(shù)就結(jié)束了。 但是如果發(fā)現(xiàn)了無(wú)效的slot,將n置為table的長(zhǎng)度len,做一次連續(xù)段的清理,再?gòu)南乱粋€(gè)空的slot開始繼續(xù)掃描。

這個(gè)函數(shù)有兩處地方會(huì)被調(diào)用,一處是插入的時(shí)候可能會(huì)被調(diào)用,另外個(gè)是在替換無(wú)效slot的時(shí)候可能會(huì)被調(diào)用, 區(qū)別是前者傳入的n為實(shí)際元素個(gè)數(shù),后者為table的總?cè)萘俊?/p>

private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    do {
        // i在任何情況下自己都不會(huì)是一個(gè)無(wú)效slot,所以從下一個(gè)開始判斷
        i = nextIndex(i, len);
        Entry e = tab[i];
        if (e != null && e.get() == null) {
            // 擴(kuò)大掃描控制因子
            n = len;
            removed = true;
            // 清理一個(gè)連續(xù)段
            i = expungeStaleEntry(i);
        }
    } while ((n >>>= 1) != 0);
    return removed;
}

接著看set函數(shù),如果循環(huán)過(guò)程中沒有返回,找到合適的位置,插入元素,表的size增加1。這個(gè)時(shí)候會(huì)做一次啟發(fā)式清理,如果啟發(fā)式清理沒有清理掉任何無(wú)效元素,判斷清理前表的大小大于閾值threshold的話,正常就要進(jìn)行擴(kuò)容了,但是表中可能存在無(wú)效元素,先把它們清除掉,然后再判斷。

private void rehash() {
    // 全量清理
    expungeStaleEntries();
    //因?yàn)樽隽艘淮吻謇?,所以size可能會(huì)變小,這里的實(shí)現(xiàn)是調(diào)低閾值來(lái)判斷是否需要擴(kuò)容。 threshold默認(rèn)為len*2/3,所以這里的threshold - threshold / 4相當(dāng)于len/2。
    if (size >= threshold - threshold / 4) {
        resize();
    }
}

作用即清除所有無(wú)效slot

private void expungeStaleEntries() {
    Entry[] tab = table;
    int len = tab.length;
    for (int j = 0; j < len; j++) {
        Entry e = tab[j];
        if (e != null && e.get() == null) {
            expungeStaleEntry(j);
        }
    }
}

保證table的容量len為2的冪,擴(kuò)容時(shí)候要擴(kuò)大2倍

private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    int newLen = oldLen * 2;
    Entry[] newTab = new Entry[newLen];
    int count = 0;
    for (int j = 0; j < oldLen; ++j) {
        Entry e = oldTab[j];
        if (e != null) {
            ThreadLocal<?> k = e.get();
            if (k == null) {
                e.value = null; 
            } else {
                // 擴(kuò)容后要重新放置元素
                int h = k.threadLocalHashCode & (newLen - 1);
                while (newTab[h] != null) {
                    h = nextIndex(h, newLen);
                }
                newTab[h] = e;
                count++;
            }
        }
    }
    setThreshold(newLen);
    size = count;
    table = newTab;
}

get方法:

private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    // 對(duì)應(yīng)的entry存在且key未被回收
    if (e != null && e.get() == key) {
        return e;
    } else {
        // 繼續(xù)往后查找
        return getEntryAfterMiss(key, i, e);
    }
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
    // 不斷向后探測(cè)直到遇到空entry
    while (e != null) {
        ThreadLocal<?> k = e.get();
        // 找到
        if (k == key) {
            return e;
        }
        if (k == null) {
            // 該entry對(duì)應(yīng)的ThreadLocal實(shí)例已經(jīng)被回收,調(diào)用expungeStaleEntry來(lái)清理無(wú)效的entry
            expungeStaleEntry(i);
        } else {
            // 下一個(gè)位置
            i = nextIndex(i, len);
        }
        e = tab[i];
    }
    return null;
}

remove方法,比較簡(jiǎn)單,在table中找key,如果找到了斷開弱引用,做一次連續(xù)段清理。

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();
            // 連續(xù)段清理
            expungeStaleEntry(i);
            return;
        }
    }
}

ThreadLocal與內(nèi)存泄漏

從上文我們知道當(dāng)調(diào)用ThreadLocalMap的set或者getEntry方法時(shí)候,有很大概率會(huì)去自動(dòng)清除掉key為null的Entry,這樣就可以斷開value的強(qiáng)引用,使對(duì)象被回收。

但是如果如果我們之后再也沒有在該線程操作過(guò)任何ThreadLocal實(shí)例的set或者get方法,那么就只能等線程死亡才能回收無(wú)效value。

因此當(dāng)我們不需要用ThreadLocal的變量時(shí)候,顯示調(diào)用ThreadLocal的remove方法是一種好的習(xí)慣。

小結(jié)

  • ThredLocal為每個(gè)線程保存一個(gè)自己的變量,但其實(shí)ThreadLocal本身并不存儲(chǔ)變量,變量存儲(chǔ)在線程自己的實(shí)例變量ThreadLocal.ThreadLocalMap threadLocals
  • ThreadLocal的設(shè)計(jì)并不是為了解決并發(fā)問題,而是解決一個(gè)變量在線程內(nèi)部的共享問題,在線程內(nèi)部處處可以訪問
  • 因?yàn)槊總€(gè)線程都只會(huì)訪問自己ThreadLocalMap 保存的變量,所以不存在線程安全問題

以上就是ThreadLocal作用原理與內(nèi)存泄露示例解析的詳細(xì)內(nèi)容,更多關(guān)于ThreadLocal內(nèi)存泄露的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • springboot實(shí)現(xiàn)配置兩個(gè)parent的方法

    springboot實(shí)現(xiàn)配置兩個(gè)parent的方法

    這篇文章主要介紹了springboot實(shí)現(xiàn)配置兩個(gè)parent的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2021-12-12
  • java中jdk的下載和安裝全過(guò)程

    java中jdk的下載和安裝全過(guò)程

    這篇文章主要給大家介紹了關(guān)于java中jdk的下載和安裝的相關(guān)資料,文中通過(guò)圖文介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2020-11-11
  • Java實(shí)現(xiàn)對(duì)一行英文進(jìn)行單詞提取功能示例

    Java實(shí)現(xiàn)對(duì)一行英文進(jìn)行單詞提取功能示例

    這篇文章主要介紹了Java實(shí)現(xiàn)對(duì)一行英文進(jìn)行單詞提取功能,結(jié)合實(shí)例形式分析了java基于StringTokenizer類進(jìn)行字符串分割的相關(guān)操作技巧,需要的朋友可以參考下
    2017-10-10
  • Java之Swagger配置掃描接口以及開關(guān)案例講解

    Java之Swagger配置掃描接口以及開關(guān)案例講解

    這篇文章主要介紹了Java之Swagger配置掃描接口以及開關(guān)案例講解,本篇文章通過(guò)簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下
    2021-08-08
  • java Arrays快速打印數(shù)組的數(shù)據(jù)元素列表案例

    java Arrays快速打印數(shù)組的數(shù)據(jù)元素列表案例

    這篇文章主要介紹了java Arrays快速打印數(shù)組的數(shù)據(jù)元素列表案例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧
    2020-09-09
  • 使用Java實(shí)現(xiàn)qq郵箱發(fā)送郵件

    使用Java實(shí)現(xiàn)qq郵箱發(fā)送郵件

    這篇文章主要為大家詳細(xì)介紹了使用Java實(shí)現(xiàn)qq郵箱發(fā)送郵件,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2010-05-05
  • Java實(shí)現(xiàn)推箱子游戲

    Java實(shí)現(xiàn)推箱子游戲

    這篇文章主要為大家詳細(xì)介紹了Java實(shí)現(xiàn)推箱子游戲,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2020-05-05
  • 淺談Java實(shí)現(xiàn)回溯算法之八皇后問題

    淺談Java實(shí)現(xiàn)回溯算法之八皇后問題

    八皇后問題是一個(gè)古老而又著名的問題,是學(xué)習(xí)回溯算法的一個(gè)經(jīng)典案例。今天我們就一起來(lái)探究一下吧
    2021-06-06
  • 淺談Java向下轉(zhuǎn)型的意義

    淺談Java向下轉(zhuǎn)型的意義

    這篇文章主要介紹了淺談Java向下轉(zhuǎn)型的意義,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2020-02-02
  • Springboot?整合?RocketMQ?收發(fā)消息的配置過(guò)程

    Springboot?整合?RocketMQ?收發(fā)消息的配置過(guò)程

    這篇文章主要介紹了Springboot?整合?RocketMQ?收發(fā)消息,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2021-12-12

最新評(píng)論