ThreadLocal

同一個threadlocal變量所包含的對象,在不一樣的線程中是不一樣的副本。數組

既然是不一樣的線程擁有不一樣的副本且不容許其餘線程訪問,因此不存在共享變量的問題。安全

解決的是變量在線程間隔離在方法或類之間共享的問題。函數

解決這個問題可能有的兩種方案this

第一種:一個threadlocal對應一個map,map中以thread爲key
imagespa

這種方法多個線程針對同一個threadlocal1是同一個map,新增線程或減小線程都須要改動map,這個map就變成了多個線程之間的共享變量,須要額外機制好比鎖保證map的線程安全。線程

線程結束時,須要保證它所訪問的全部 ThreadLocal 中對應的映射均刪除,不然可能會引發內存泄漏。-----爲何會致使內存泄漏呢?見下文code

第二種:一個thread對應一個map,map中以threadlocal爲key區分對象

image

Thread中有一個變量blog

ThreadLocal.ThreadLocalMap threadLocals = null;//每一個線程維護一個map

ThreadLocalMap索引

static class Entry extends WeakReference<ThreadLocal> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal k, Object v) {
                super(k);
                value = v;
            }
        }
        
 private Entry[] table;

ThreadLocalMap維護了一個table數組,存儲Entry類型對象,Entry類型對象以ThreadLocal爲key,任意對象爲值的健值對

ThreadLocal的get set

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);//返回當前線程維護的threadlocals變量
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null)
                return (T)e.value;
        }
        return setInitialValue();
    }
    
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
    
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

Entry key的弱引用以及內存泄漏
image

爲何說threadlocal會存在內存泄漏:

每一個thread維護的threadlocalmap key是指向threadlocal的弱引用,當沒有任何其餘強引用指向threadlocal的時候,gc會把key回收。

但value是是thread指向的強引用,thread不結束,value不會被回收。

因此當threadlocal不可用但thread還在的這段時間內,會存在所說的內存泄漏。尤爲當使用線程池的時候,線程被複用。

jdk有沒有相應的處理:

再回到threadlocalmap的get set方法

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t); //獲取當前線程的map
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this); //找到當前的threadlocal
            if (e != null)
                return (T)e.value;//取值
        }
        return setInitialValue();// map爲null或者map找不到指定key時,初始化基本值,不展開
    }
    
    //getEntry函數
      private Entry getEntry(ThreadLocal key) {
            int i = key.threadLocalHashCode & (table.length - 1);//根據key計算索引
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);//table中該索引位置對象e爲null 或者 索引位置key不符進入getEntryAfterMiss
        }
        
   //getEntryAfterMiss函數
     private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;
            //遍歷table一直到找到了k=key的位置,返回相應對象e
            //遍歷過程當中若是遇到了k爲null,即調用expungeStaleEntry清理該entry,即前面所說的內存泄漏,這裏是處理的一個時機
            while (e != null) {
                ThreadLocal k = e.get();
                if (k == key)
                    return e;
                if (k == null)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len); //循環遍歷table,ThreadLocal採用的是開放地址法,即有衝突後,把要插入的元素放在要插入的位置後面爲null的地方
                e = tab[i];
            }
            return null;//若是是e爲null  返回null
        }
   //expungeStaleEntry 函數
  private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            // expunge entry at staleSlot:key爲null的索引位置的對象.value置爲null,對象也置爲null
            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;  //遍歷是從staleSlot以後到遇到的第一個e爲null
                 i = nextIndex(i, len)) {
                ThreadLocal k = e.get();
                if (k == null) {//遍歷的過程當中遇到key爲null作和上面一樣的處理
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else { //key不爲null的從新hash
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;

                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }
 
        //再看set
     public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
    
    //重點在map.set函數
      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)]) { //若是根據索引找到的entry不是空的
                ThreadLocal k = e.get();
                if (k == key) {  //key相同,value直接覆蓋
                    e.value = value;
                    return;
                }

                if (k == null) {  //遍歷過程當中key爲null,清除
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
                        //上面沒有處理掉,找到第一個爲null的能用的坑位,new一個entry放入
            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

能大體看到,上面的代碼中,threadlocalmap的get set都會作對key爲null的清除工做,從而解決了上面說的內存泄漏問題,只是這種處理依賴對set get的調用

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

綜上所述,不少地方會看到有這樣的兩條建議:

1 .使用者須要手動調用remove函數,刪除再也不使用的ThreadLocal.

2 .還有儘可能將ThreadLocal設置成private static的,這樣ThreadLocal會盡可能和線程自己一塊兒消亡。

相關文章
相關標籤/搜索