ThreadLocal 源码分析

这是我参与更文挑战的第7天,活动详情查看: 更文挑战

基本介绍

ThreadLocal类是java.lang包下的一个类,用于线程内部的数据存储,通过它可以在指定的线程中存储数据,本文针对该类进行原理分析。

使用场景

我们一般用ThreadLocal来提供线程局部变量。线程局部变量会在每个Thread内拥有一个副本,Thread只能访问自己的那个副本。

用例

public class ThreadLocalTest {

    private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){
        protected Integer initialValue(){
            return 0;
        }
    };
    
    // 值传递
    @Test
    public void testValue(){
        for (int i = 0; i < 5; i++){
            new Thread(() -> {
                Integer temp = threadLocal.get();
                threadLocal.set(temp + 5);
                System.out.println("current thread is " + Thread.currentThread().getName() + " num is " + threadLocal.get());
            }, "thread-" + i).start();
        }
    }
}
复制代码

以上程序的输出结果是:

current thread is thread-1 num is 5
current thread is thread-3 num is 5
current thread is thread-0 num is 5
current thread is thread-4 num is 5
current thread is thread-2 num is 5
复制代码

我们可以看到,每一个线程打印出来的都是5,哪怕我是先通过ThreadLocal.get()方法获取变量,然后再set进去,依然不会进行重复叠加。

这就是线程隔离。

源码分析

ThreadLocal

set

/**
 * Sets the current thread's copy of this thread-local variable
 * to the specified value.  Most subclasses will have no need to
 * override this method, relying solely on the {@link #initialValue}
 * method to set the values of thread-locals.
 *
 * @param value the value to be stored in the current thread's copy of
 *        this thread-local.
 */
public void set(T value) {
    //获得当前线程
    Thread t = Thread.currentThread();
    //获取该线程的ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);
    if (map != null)
       //将数据放入ThreadLocalMap中,key是当前ThreadLocal对象,值是我们传入的value。
        map.set(this, value);
    else
        //初始化ThreadLocalMap,并以当前ThreadLocal对象为Key,value为值存入map中。
        createMap(t, value);
}
复制代码

ThreadLocal#set()方法主要是通过当前线程的ThreadLocalMap实现的。ThreadLocalMap是一个Map,它的key是ThreadLoacl,value是Object

get

/**
 * Returns the value in the current thread's copy of this
 * thread-local variable.  If the variable has no value for the
 * current thread, it is first initialized to the value returned
 * by an invocation of the {@link #initialValue} method.
 *
 * @return the current thread's value of this thread-local
 */
public T get() {
    Thread t = Thread.currentThread();
    //获取当前线程的ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);
    //若该线程的ThreadLocalMap对象已存在,则直接获取该Map里的值;否则则通过初始化函数创建1个ThreadLocalMap对象
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}
复制代码

大体上与set方法类似,就是先获取到当前线程的ThreadLocalMap,然后以this为key可以取得value

remove

/**
 * Removes the current thread's value for this thread-local
 * variable.  If this thread-local variable is subsequently
 * {@linkplain #get read} by the current thread, its value will be
 * reinitialized by invoking its {@link #initialValue} method,
 * unless its value is {@linkplain #set set} by the current thread
 * in the interim.  This may result in multiple invocations of the
 * {@code initialValue} method in the current thread.
 *
 * @since 1.5
 */
 public void remove() {
     ThreadLocalMap m = getMap(Thread.currentThread());
     if (m != null)
         m.remove(this);
 }
复制代码

到这里我们基本上明白了ThreadLocal的工作原理,我们总结一下

  1. 每个Thread实例内部都有一个ThreadLocalMapThreadLocalMap是一种Map,它的key是ThreadLocal,value是Object。

  2. ThreadLocal#set()方法其实是往当前线程的ThreadLocalMap中存入数据,其key是当前ThreadLocal对象,value是set方法中传入的值。

  3. 使用数据时,以当前ThreadLocal为key,从当前线程的ThreadLocalMap中取出数据。

ThreadLocalMap

ThreadLocal的核心就是ThreadLocalMap,它是维护我们线程与变量之间关系的一个类。

Entry内部类

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;
        }
    }
    /**
         * The initial capacity -- MUST be a power of two.
         * Entry数组的初始化大小
         */
        private static final int INITIAL_CAPACITY = 16;

        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         * <ThreadLocal, 保存的泛型值>数组
         * 长度必须是2的N次幂
         * 这个可以参考为什么HashMap里维护的数组也必须是2的N次幂
         * 主要是为了减少碰撞,能够让保存的元素尽量的分散
         * 关键代码还是hashcode & table.length - 1
         */
        private Entry[] table;

        /**
         * The number of entries in the table.
         * table里的元素个数
         */
        private int size = 0;

        /**
         * The next size value at which to resize.
         * 扩容的阈值
         */
        private int threshold; // Default to 0

        /**
         * Set the resize threshold to maintain at worst a 2/3 load factor.
         * 根据长度计算扩容的阈值
         */
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }

        /**
         * Increment i modulo len.
         * 获取下一个索引,超出长度则返回0
         */
        private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

        /**
         * Decrement i modulo len.
         * 返回上一个索引
         */
        private static int prevIndex(int i, int len) {
            return ((i - 1 >= 0) ? i - 1 : len - 1);
        }

        /**
         * Construct a new map initially containing (firstKey, firstValue).
         * ThreadLocalMaps are constructed lazily, so we only create
         * one when we have at least one entry to put in it.
         */
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            // 初始化table的大小为16
            table = new Entry[INITIAL_CAPACITY];
            // 通过hashcode & (长度-1)的位运算,确定键值对的位置
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            // 创建一个新节点保存在table当中
            table[i] = new Entry(firstKey, firstValue);
            // 设置table内元素为1
            size = 1;
            // 设置扩容阈值
            setThreshold(INITIAL_CAPACITY);
        }
         /**
         * Construct a new map including all Inheritable ThreadLocals
         * from given parent map. Called only by createInheritedMap.
         *
         * ThreadLocal本身是线程隔离的,按道理是不会出现数据共享和传递的行为的
         * 这是InheritableThreadLocal提供了了一种父子间数据共享的机制
         * @param parentMap the map associated with parent thread.
         */
        private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];

            for (int j = 0; j < len; j++) {
                Entry e = parentTable[j];
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                    if (key != null) {
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value);
                        int h = key.threadLocalHashCode & (len - 1);
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }
}
复制代码
  • 我们可以看到,在ThreadLocalMap这个内部类当中,又定义了一个Entry内部类,它是一个WeakReference准去的说继承自弱引用
  • ThreadLocal对象的引用被传到WeakReferencereference中,entry.get()被当作map元素的key,而Entry还多了一个字段value,用来存放ThreadLocal变量实际的值。
  • 至于为什么要用弱引用呢?我想我源码上面的注释其实也写得很明白了,这ThreadLocal实际上就是个线程本地变量隔离作用的工具类而已,当线程走完了,肯定希望能回收这部分产生的资源,所以就用了弱引用。
  • 由于是弱引用,若ThreadLocal对象不再有普通引用,GC发生时会将ThreadLocal对象清除。而Entry的key,即entry.get()会变为null。然而,GC只会清除被引用对象,Entry还被线程的ThreadLocalMap引用着,因而不会被清除。因而,value对象就不会被清除。除非线程退出,造成该线程的ThreadLocalMap整体释放,否则value的内存就无法释放,内存泄漏
  • JDK的作者自然想到了这一点,因此在ThreadLocalMap的很多方法中,调用expungeStaleEntries()清除entry.get() == null 的元素,将Entry的value释放。所以,只要线程还在使用其他ThreadLocal,已经无效的ThreadLocal内存就会被清除。
  • 然而,我们大部分的使用场景是,ThreadLocal是一个静态变量,因此永远有普通引用指向每个线程中的ThreadLocalMap的该entry。因此该ThreadLocal的Entry永远不会被释放,自然expungeStaleEntries()就无能为力,value的内存也不会被释放。所以在我们确实用完了ThreadLocal后,可以主动调用remove()方法,主动删掉entry。

我相信有人会有疑问,如果在我要用的时候,被回收了怎么办?下面的代码会一步步地让你明白,你考虑到的问题,这些大牛都已经想到并且解决了。接着看

getEntry && getEntryAfterMiss

/**
 * Get the entry associated with key.  This method
 * itself handles only the fast path: a direct hit of existing
 * key. It otherwise relays to getEntryAfterMiss.  This is
 * designed to maximize performance for direct hits, in part
 * by making this method readily inlinable.
 *
 * @param  key the thread local object
 * @return the entry associated with key, or null if no such
 */
private Entry getEntry(ThreadLocal<?> key) {
    // 通过hashcode确定下标
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    // 如果找到则直接返回
    if (e != null && e.get() == key)
        return e;
    else
        // 找不到的话接着从i位置开始向后遍历,基于线性探测法,是有可能在i之后的位置找到的
        return getEntryAfterMiss(key, i, e);
}

/**
 * Version of getEntry method for use when key is not found in
 * its direct hash slot.
 *
 * @param  key the thread local object
 * @param  i the table index for key's hash code
 * @param  e the entry at table[i]
 * @return the entry associated with key, or null if no such
 */
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
    // 循环向后遍历
    while (e != null) {
         // 获取节点对应的k
        ThreadLocal<?> k = e.get();
        // 相等则返回
        if (k == key)
            return e;
        // 如果为null,触发一次连续段清理
        if (k == null)
            expungeStaleEntry(i);
        else
            // 获取下一个下标接着进行判断
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}
复制代码

expungeStaleEntry

/**
 * Expunge a stale entry by rehashing any possibly colliding entries
 * lying between staleSlot and the next null slot.  This also expunges
 * any other stale entries encountered before the trailing null.  See
 * Knuth, Section 6.4
 *
 * @param staleSlot index of slot known to have null key
 * @return the index of the next null slot after staleSlot
 * (all between staleSlot and this slot will have been checked
 * for expunging).
 * 这个函数可以看做是ThreadLocal里的核心清理函数,它主要做的事情就是
 * 1、从staleSlot开始,向后遍历将ThreadLocal对象被回收所在Entry节点的value和Entry节点本身设置null,方便GC,并且size自减1
 * 2、并且会对非null的Entry节点进行rehash,只要不是在当前位置,就会将Entry挪到下一个为null的位置上
 * 所以实际上是对从staleSlot开始做一个连续段的清理和rehash操作
 */
private int expungeStaleEntry(int staleSlot) {
     // 新的引用指向table
    Entry[] tab = table;
    int len = tab.length;

    // expunge entry at staleSlot
     // 先将传过来的下标置null
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;// table的size-1

    // Rehash until we encounter null
    Entry e;
    int i;
     // 遍历删除指定节点所有后续节点当中,ThreadLocal被回收的节点
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
         // 获取entry当中的key
        ThreadLocal<?> k = e.get();
        // 如果ThreadLocal为null,则将value以及数组下标所在位置设置null,方便GC
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            // 重新计算key的下标
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                tab[i] = null;
				// 如果是当前位置则遍历下一个
         	    // 不是当前位置,则重新从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;
}
复制代码

这可以说是ThreadLocal非常核心的一个清理方法,为什么会需要清理呢?或许很多人想不明白,我们用List或者是Map也好,都没有说要清理里面的内容。

但是这里是对于线程来说的隔离的本地变量,并且使用的是弱引用,那便有可能在GC的时候就被回收了。

  • 如果有很多Entry节点已经被回收了,但是在table数组中还留着位置,这时候不清理就会浪费资源

  • 在清理节点的同时,可以将后续非空的Entry节点重新计算下标进行排放,这样子在get的时候就能快速定位资源,加快效率。

set

/**
 * Set the value associated with key.
 * ThreadLocalMap的set方法,这个方法还是挺关键的
 * 通过这个方法,我们可以看出该哈希表是用线性探测法来解决冲突的
 * @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.
	
    // 新开一个引用指向table
    Entry[] tab = table;
    // 获取table的长度
    int len = tab.length;
    // 获取对应ThreadLocal在table当中的下标
    int i = key.threadLocalHashCode & (len-1);

    /**
     * 从该下标开始循环遍历
     * 1、如遇相同key,则直接替换value
     * 2、如果该key已经被回收失效,则替换该失效的key
     */
    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,则替换当前失效的k所在Entry节点
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    
	// 找到空的位置,创建Entry对象并插入
    tab[i] = new Entry(key, value);
    // table内元素size自增
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}


复制代码
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                               int staleSlot) {
    // 新开一个引用指向table
    Entry[] tab = table;
    int len = tab.length;
    Entry e;

    // Back up to check for prior stale entry in current run.
    // We clean out whole runs at a time to avoid continual
    // incremental rehashing due to garbage collector freeing
    // up refs in bunches (i.e., whenever the collector runs).
    // 记录当前失效的节点下标
    int slotToExpunge = staleSlot;
     /**
     * 通过这个for循环的prevIndex(staleSlot, len)可以看出
     * 这是由staleSlot下标开始向前扫描
     * 查找并记录最前位置value为null的下标
     */
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;

    // Find either the key or trailing null slot of run, whichever
    // occurs first
     /**
     * 通过for循环nextIndex(staleSlot, len)可以看出
     * 这是由staleSlot下标开始向后扫描
     */
    for (int i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();

        // If we find key, then we need to swap it
        // with the stale entry to maintain hash table order.
        // The newly stale slot, or any other stale slot
        // encountered above it, can then be sent to expungeStaleEntry
        // to remove or rehash all of the other entries in run.
        /**
         * 如果与新的key对应,直接赋值value
         * 则直接替换i与staleSlot两个下标
         */
        if (k == key) {
            e.value = value;

            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;

            // Start expunge at preceding stale entry if it exists
             // 通过注释看出,i之前的节点里,没有value为null的情况
            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            /**
             * 在调用cleanSomeSlots进行启发式清理之前
             * 会先调用expungeStaleEntry方法从slotToExpunge到table下标所在为null的连续段进行一次清理
             * 返回值便是table[]为null的下标
             * 然后以该下标--len进行一次启发式清理
             * 最终里面的方法实际上还是调用了expungeStaleEntry
             * 可以看出expungeStaleEntry方法是ThreadLocal核心的清理函数
             */
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }

        // If we didn't find stale entry on backward scan, the
        // first stale entry seen while scanning for key is the
        // first still present in the run.
         /**
         * 如果当前下标所在已经失效,并且向后扫描过程当中没有找到失效的Entry节点
         * 则slotToExpunge赋值为当前位置
         */
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }

    // If key not found, put new entry in stale slot
    // 如果并没有在table当中找到该key,则直接在当前位置new一个Entry
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);
	
    /**
     * 在上面的for循环探测过程当中
     * 如果发现任何无效的Entry节点,则slotToExpunge会被重新赋值
     * 就会触发连续段清理和启发式清理
     */
    
    // If there are any other stale entries in run, expunge them
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
复制代码
/**     
 * 启发式地清理被回收的Entry
 * i对应的Entry是非无效的,有可能是失效被回收了,也有可能是null
 * 会有两个地方调用到这个方法
 * 1、set方法,在判断是否需要resize之前,会清理并rehash一遍
 * 2、替换失效的节点时候,也会进行一次清理
 */
private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    do {
        i = nextIndex(i, len);
        Entry e = tab[i];
        // Entry对象不为空,但是ThreadLocal这个key已经为null
        if (e != null && e.get() == null) {
            n = len;
            removed = true;
             /**
             * 调用该方法进行回收
             * 实际上不是只回收 i 这一个节点而已
             * 而是对 i 开始到table所在下标为null的范围内,对那些节点都进行一次清理和rehash
             */
            i = expungeStaleEntry(i);
        }
    } while ( (n >>>= 1) != 0);
    return removed;
}
复制代码
/**
 * Double the capacity of the table.
 */
private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    int newLen = oldLen * 2;
    Entry[] newTab = new Entry[newLen];
    int count = 0;

    /**
     * 从下标0开始,逐个向后遍历插入到新的table当中
     * 1、如遇到key已经为null,则value设置null,方便GC回收
     * 2、通过hashcode & len - 1计算下标,如果该位置已经有Entry数组,则通过线性探测向后探测插入
     */
    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
    size = count;
    // 指向新的Entry数组
    table = newTab;
}
复制代码

以上的代码就是调用set方法往ThreadLocalMap当中保存K-V关系的一系列代码,我就不分开再一个个讲了,这样大家看起来估计也比较方便,有连续性。

​ 我们可以来看看一整个的set流程:

​ 1、先通过hashcode & (len – 1)来定位该ThreadLocal在table当中的下标i

​ 2、从i开始for循环向后遍历

​ 1)如果获取Entry节点的key与我们需要操作的ThreadLocal相等,则直接替换value

​ 2)如果遍历的时候拿到了key为null的情况,则调用replaceStaleEntry方法进行与之替换。

​ 3、如果上述两个情况都是,则直接在计算的出来的下标当中new一个Entry阶段插入。

​ 4、进行一次启发式地清理并且如果插入节点后的size大于扩容的阈值,则调用resize方法进行扩容。

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) {
            // 将引用设置null,方便GC
            e.clear();
            // 从该位置开始进行一次连续段清理
            expungeStaleEntry(i);
            return;
        }
    }
}
复制代码

我们可以看到,remove节点的时候,也会使用线性探测的方式,当找到对应key的时候,就会调用clear将引用指向null,并且会触发一次连续段清理。

总结

借用张流程图,来自 ThreadLocal理解

image.png

内存分析

不知从何时起,网上开始流传ThreadLocal有内存泄漏的问题。下面我们从ThreadLocal的内存入手,分析一下这种说法是否正确。话不多说直接上图。

image.png
现在,我们假设ThreadLocal完成了自己的使命,与ThreadLocalRef断开了引用关系。此时内存图变成了这样。

image.png

系统GC发生时,由于Heap中的ThreadLocal只有来自key的弱引用,因此ThreadLocal内存会被回收到。

image.png

到这里,value被留在了Heap中,而我们没办法通过引用访问它。value这块内存将会持续到线程结束。导致内存泄漏。

其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。这样就能 一定情况下避免内存泄漏

private void set(ThreadLocal<?> key, Object value) {
		
	for (Entry e = tab[i];
     	e != null;
     	e = tab[i = nextIndex(i, len)]) {
    	ThreadLocal<?> k = e.get();
  
		...
  
    	if (k == null) {
      	  replaceStaleEntry(key, value, i);
          return;
   	 	}
	}
}
复制代码

ThreadLocal get方法获取时,有一段如果Entry的key为null,移除Entry和Entry.value的代码

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--;
        }
      ...
    }
    return i;
}
复制代码

但是这些被动的预防措施并不能保证不会内存泄漏:

  • 使用static的ThreadLocal,延长了ThreadLocal的生命周期,可能导致的内存泄漏。
  • 分配使用了ThreadLocal又不再调用get(),set(),remove()方法,那么就会导致内存泄漏。

如果不想依赖线程的生命周期,那就调用remove方法来释放value的内存吧。个人认为,这种设计应该也是JDK开发大佬的无奈之举。

还可以参考Java进阶(七)正确理解Thread Local的原理与适用场景

其他知识点

为什么使用弱引用

从表面上看内存泄漏的根源在于使用了弱引用。网上的文章大多着重分析ThreadLocal使用了弱引用会导致内存泄漏,但是另一个问题也同样值得思考:为什么使用弱引用而不是强引用?

我们先来看看官方文档的说法:

To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys. 为了应对非常大和长时间的用途,哈希表使用弱引用的 key。**

下面我们分两种情况讨论:

  • key 使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
  • key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。
    比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除

因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。

ThreadLocal 最佳实践

综合上面的分析,我们可以理解ThreadLocal内存泄漏的前因后果,那么怎么避免内存泄漏呢?

  • 每次使用完ThreadLocal,都调用它的remove()方法,清除数据。

在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。

跨线程传递

参考揭开ThreadLocal的面纱#跨线程传递

ThreadLocal 如何解决 Hash 冲突?

我们知道,HashMap是一种get、set都非常高效的集合,它的时间复杂度只有O(1)。但是如果存在严重的Hash冲突,那HashMap的效率就会降低很多。

与 HashMap 不同,ThreadLocalMap 结构非常简单,没有 next 引用,也就是说 ThreadLocalMap 中解决 Hash 冲突的方式并非链表的方式,而是采用线性探测的方式。所谓线性探测,就是根据初始 key 的 hashcode 值( key.threadLocalHashCode & (len-1))确定元素在 table 数组中的位置,如果发现这个位置上已经被其他的 key 值占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。len是当前Entry[]的长度,这没什么好说的。那看来秘密就在threadLocalHashCode中了。我们来看一下threadLocalHashCode是如何产生的。

 public class ThreadLocal<T> {

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

这段代码非常简单。有个全局的计数器nextHashCode,每有一个ThreadLocal产生这个计数器就会加0x61c88647,然后把当前值赋给threadLocalHashCode。关于0x61c88647这个神奇的常量,可以点这里

为什么ThreadLocal类内部的ThreadLocalMap要用Entry数组实现?

set方法只会设置一个值,直接用一个Entry不就解决问题了?为什么会涉及到数组,还会有数组扩容的问题?

首先明确一下ThreadLocalsetget的过程

  • 调用get()

    • 获取当前线程Thread对象,进而获取此线程对象中维护的ThreadLocalMap对象。
    • 判断当前的ThreadLocalMap是否存在,如果存在,则以当前的ThreadLocalkey,调用ThreadLocalMap中的getEntry方法获取对应的存储实体 e。找到对应的存储实体 e,获取存储实体 e 对应的 value值,即为我们想要的当前线程对应此ThreadLocal的值,返回结果值。
    • 如果不存在,则证明此线程没有维护的ThreadLocalMap对象,调用setInitialValue方法进行初始化。返回setInitialValue初始化的值。
  • 调用set(T value)

    • 获取当前线程Thread对象,进而获取此线程对象中维护的ThreadLocalMap对象。
    • 判断当前的ThreadLocalMap是否存在:
    • 如果存在,则调用map.set设置此实体entry
    • 如果不存在,则调用createMap进行ThreadLocalMap对象的初始化,并将此实体entry作为第一个值存放至ThreadLocalMap中。

总结起来就是
ThreadLocal#get() 方法中首先取当前线程
然后从当前线程对象中获取 threadLocals 属性
这个属性就是 ThreadLocalMap 的一个对象
既然一个线程只对应一个 Thread 对象
那么一个线程中也就只有一个 ThreadLocalMap 对象
而一个程序中可以创建多个 ThreadLocal 对象
在同一个线程中都会访问到这个 Map
key就是一个指向threadLocal实例的一个弱引用
多个ThreadLocal实例的话,那么就key必然是不同的
那么这个 Map 就必须存储多个 value 值并考虑扩容

此题来自知乎www.zhihu.com/question/27…

ThreadLocal实现原理是什么

通常,如果我不去看源代码的话,我猜ThreadLocal是这样子设计的:每个ThreadLocal类都创建一个Map,然后用线程的ID threadID作为Mapkey,要存储的局部变量作为Mapvalue,这样就能达到各个线程的值隔离的效果。这是最简单的设计方法,JDK最早期的ThreadLocal就是这样设计的。

但是,JDK后面优化了设计方案,现时JDK8 ThreadLocal的设计是:每个Thread维护一个ThreadLocalMap哈希表,这个哈希表的keyThreadLocal实例本身,value才是真正要存储的值Object

这个设计与我们一开始说的设计刚好相反,这样设计有如下几点优势:

  1. 这样设计之后每个Map存储的Entry数量就会变小,因为之前的存储数量由Thread的数量决定,现在是由ThreadLocal的数量决定。
  2. Thread销毁之后,对应的ThreadLocalMap也会随之销毁,能减少内存的使用。

与同步机制区别

类型 实现机制 同步共享 应用场景
ThreadLocal 为每个线程都提供一个变量的副本,从而实现同时访问,而互不影响。
(以空间换时间)
无需对该变量进行同步
(每个线程都拥有自己的变量副本)
隔离多个线程的数据共享
同步机制 提供一个变量,让不同的线程排队访问
(以时间换空间)
被作用”锁机制”的变量是多个线程共享的
– 通过对象的锁机制保证同一时间只有一个线程访问变量
– synchronized = 1个保留字,依靠JVM的锁机制来实现临界区的函数or变量的访问中的原子性
同步多个线程对相同资源的并发访问,防止并发冲突

更多参考ThreadLocal和synchronized的区别?

参考

ThreadLocal用法及原理

Java多线程:神秘的线程变量 ThreadLocal 你了解吗?

深入理解ThreadLocal

ThreadLocal和ThreadLocalMap源码分析

揭开ThreadLocal的面纱

ThreadLocal内存泄漏问题及如何解决

Java 200+ 面试题补充 ThreadLocal 模块

JAVA并发-自问自答学ThreadLocal

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享