原理图

ThreadLocal - 图1

ThreadLocalMap

是ThreadLocal中的内部类,key:当前ThreadLocal,value:Entry,也就是ThreadLocal对应的值。
源码:

  1. /**
  2. * ThreadLocalMap is a customized hash map suitable only for
  3. * maintaining thread local values. No operations are exported
  4. * outside of the ThreadLocal class. The class is package private to
  5. * allow declaration of fields in class Thread. To help deal with
  6. * very large and long-lived usages, the hash table entries use
  7. * WeakReferences for keys. However, since reference queues are not
  8. * used, stale entries are guaranteed to be removed only when
  9. * the table starts running out of space.
  10. */
  11. static class ThreadLocalMap {
  12. /**
  13. * The entries in this hash map extend WeakReference, using
  14. * its main ref field as the key (which is always a
  15. * ThreadLocal object). Note that null keys (i.e. entry.get()
  16. * == null) mean that the key is no longer referenced, so the
  17. * entry can be expunged from table. Such entries are referred to
  18. * as "stale entries" in the code that follows.
  19. */
  20. static class Entry extends WeakReference<ThreadLocal<?>> {
  21. /** The value associated with this ThreadLocal. */
  22. Object value;
  23. Entry(ThreadLocal<?> k, Object v) {
  24. super(k);
  25. value = v;
  26. }
  27. }
  28. /**
  29. * The initial capacity -- MUST be a power of two.
  30. */
  31. private static final int INITIAL_CAPACITY = 16;
  32. /**
  33. * The table, resized as necessary.
  34. * table.length MUST always be a power of two.
  35. */
  36. private Entry[] table;
  37. /**
  38. * The number of entries in the table.
  39. */
  40. private int size = 0;
  41. /**
  42. * The next size value at which to resize.
  43. */
  44. private int threshold; // Default to 0
  45. /**
  46. * Set the resize threshold to maintain at worst a 2/3 load factor.
  47. */
  48. private void setThreshold(int len) {
  49. threshold = len * 2 / 3;
  50. }
  51. /**
  52. * Increment i modulo len.
  53. */
  54. private static int nextIndex(int i, int len) {
  55. return ((i + 1 < len) ? i + 1 : 0);
  56. }
  57. /**
  58. * Decrement i modulo len.
  59. */
  60. private static int prevIndex(int i, int len) {
  61. return ((i - 1 >= 0) ? i - 1 : len - 1);
  62. }
  63. /**
  64. * Construct a new map initially containing (firstKey, firstValue).
  65. * ThreadLocalMaps are constructed lazily, so we only create
  66. * one when we have at least one entry to put in it.
  67. */
  68. ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
  69. table = new Entry[INITIAL_CAPACITY];
  70. int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
  71. table[i] = new Entry(firstKey, firstValue);
  72. size = 1;
  73. setThreshold(INITIAL_CAPACITY);
  74. }

从代码中,可以看到ThreadLocalMap的Entry继承了WeakReference。

set

    //ThreadLocal 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);
    }
    /**
    * Get the map associated with a ThreadLocal. Overridden in
    * InheritableThreadLocal.
    * @param  t the current thread
    * @return the map
    */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    public class Thread implements Runnable {

        /* ThreadLocal values pertaining to this thread. This map is maintained
        * by the ThreadLocal class. */

            ThreadLocal.ThreadLocalMap threadLocals = null;
        //...
    }
    /**
     * Create the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the map
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    /************************** ThreadLocalMap  *************************/
     /**
     * Set the value associated with key.
     *
     * @param key the thread local object
     * @param value the value to be 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)]) {
            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();
    }
    //hash冲突的时候如何处理
    private static int nextIndex(int i, int len) {
        return ((i + 1 < len) ? i + 1 : 0);
    }
    //扩容
    private void rehash() {
        expungeStaleEntries();

        // Use lower threshold for doubling to avoid hysteresis
        if (size >= threshold - threshold / 4)
            resize();
    }
    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; // Help the GC
                } else {
                    int h = k.threadLocalHashCode & (newLen - 1);
                    while (newTab[h] != null)
                        h = nextIndex(h, newLen);
                    newTab[h] = e;
                    count++;
                }
            }
        }

        setThreshold(newLen);
        size = count;
        table = newTab;
    }

hash冲突的解决方法

     和HashMap的最大的不同在于,ThreadLocalMap结构非常简单,没有next引用,也就是说ThreadLocalMap中解决Hash冲突的方式并非链表的方式,而是采用**线性探测的方式**,所谓线性探测,就是根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。

扩容

扩容的时机:size > 3/4的阀值
数组长度增长为原来的两倍。

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

remove

    public void remove() {
        ThreadLocal.ThreadLocalMap m = getMap(java.lang.Thread.currentThread());
        if (m != null)
            m.remove(this);
    }
    private void remove(ThreadLocal<?> key) {
        ThreadLocal.ThreadLocalMap.Entry[] tab = table;
        int len = tab.length;
        int i = key.threadLocalHashCode & (len-1);
        //只所以用for循环查找key,是因为要考虑hash冲突的情况
        for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
            if (e.get() == key) {
                e.clear();
                expungeStaleEntry(i);
                return;
            }
        }
    }
    public void clear() {
        this.referent = null;
    }

内存泄露

ThreadLocal - 图2

ThreadLocal的核心机制

  每个Thread内部维护着一个ThreadLocalMap,它是一个Map。这个映射表的Key是一个弱引用,其实就是ThreadLocal本身,Value是真正存的线程变量Object。<br />      也就是说ThreadLocal本身并不真正存储线程的变量值,它只是一个工具,用来维护Thread内部的Map,帮助存和取。所以对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。

根本原因

ThreadLocal 本身并不存储值,它只是作为一个 key保存到ThreadLocalMap中,但是这里要注意的是它作为一个key用的是弱引用,因为没有强引用链,弱引用在GC的时候可能会被回收。这样就会在ThreadLocalMap中存在一些key为null的键值对(Entry)。因为key变成null了,我们是没法访问这些Entry的,但是这些Entry本身是不会被清除的,为什么呢?因为存在一条强引用链。即线程本身->ThreadLocalMap->Entry也就是说,恰恰我们在使用线程池的时候,线程使用完了是会放回到线程池循环使用的。由于ThreadLocalMap的生命周期和线程一样长,如果没有手动删除对应key就会导致这块内存即不会回收也无法访问,也就是内存泄漏。
其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在 ThreadLocal的get(),set(),remove()【见下】的时候都会清除线程ThreadLocalMap里所有key为null的value。但是这些举动不能保证内存就一定会回收,因为可能这条线程被放回到线程池里后再也没有使用,或者使用的时候没有调用其get(),set(),remove()方法。
内存泄漏归根结底是由于ThreadLocalMap的生命周期跟Thread一样长

如何避免

    调用ThreadLocal的get()、set()方法后再调用remove方法,将Entry节点和Map的引用关系移除,这样整个Entry对象在GC Roots分析后就变成不可达了,下次GC的时候就可以被回收。<br />      如果使用ThreadLocal的set方法之后,没有显示的调用remove方法,就有可能发生内存泄露,所以养成良好的编程习惯十分重要,使用完ThreadLocal之后,记得调用remove方法。<br />      在不使用线程池的前提下,即使不调用remove方法,线程的"变量副本"也会被gc回收,即不会造成内存泄漏的情况。

jdk本身的防护措施

get

image.png

set

image.png

remove

image.png

参照:https://blog.csdn.net/lululove19870526/article/details/83688863