ThreadLocal详解

参考代码:openjdk-11
参考文档:
面试官连环炮轰炸的ThreadLocal 吃透源码的每一个细节和设计原理
面试官:小伙子,听说你看过ThreadLocal源码?(万字图文深度解析ThreadLocal)

基本概念

ThreadLocal

java.lang.ThreadLocal 类提供线程局部变量。

每个访问一个线程(通过其 get/set 方法)都有自己的、独立初始化的变量副本。

ThreadLocal 的实例通常是“希望将状态与线程相关联”的类中的私有静态字段(例如用户 ID 或事务 ID)。

举个🌰,下面的类保存着每个线程本地的 token,即登录凭证。

public class TokenHolder {
    
    private static ThreadLocal<String> tokenThreadLocal = new ThreadLocal<>();
    
    public static void setToken(String token) {
        tokenThreadLocal.set(token);
    }
    
    public static String getToken() {
        return tokenThreadLocal.get();
    }
    
    public static void clear() {
        tokenThreadLocal.remove();
    }
}

ThreadLocalMap

在 ThreadLocal 类中维护了一个静态内部类 ThreadLocalMap。

虽然名为 Map,但与 java.util.Map 接口无关。

ThreadLocalMap 是一种定制的哈希映射,仅适用于维护线程本地值。不会在 ThreadLocal 类之外导出任何操作。
该类是包私有的,以允许在类 Thread 中声明字段。
为了帮助处理非常大且长期存在的使用,哈希表条目使用 WeakReference 类型的对象作为 key。但由于不使用引用队列,只有在表开始耗尽空间时才能保证删除陈旧条目。

映射中的条目也是定制的:

static class ThreadLocalMap {
    
	static class Entry extends WeakReference<ThreadLocal<?>> {

        Object value;

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

	/** The initial capacity -- MUST be a power of two. */
    private static final int INITIAL_CAPACITY = 16;

    /** The table, resized as necessary.
     * table.length MUST always be a power of two. */
    private Entry[] table;

    /** The number of entries in the table. */
    private int size = 0;

    /** The next size value at which to resize. */
    private int threshold; // Default to 0
    
    // ...省略代码
}

这个哈希映射中的条目继承了 WeakReference,使用它所引用的对象作为键(总是一个 ThreadLocal 对象)。请注意,空键(entry.get() == null 也即 k == null)意味着不再引用该键,因此可以从表中删除该条目,称之为“陈旧条目”,具体参见 ThreadLocalMap#set() 方法。

线程是如何与线程局部变量相关联的?

只要线程处于活动状态并且 ThreadLocal 实例可访问时,每个线程都持有对其线程局部变量副本的隐式引用;线程消失后,它的所有线程本地实例副本都将进行垃圾回收(除非存在对这些副本的其他引用)。

Thread 类中声明的字段如下:

// 线程对局部变量副本的引用
ThreadLocal.ThreadLocalMap threadLocals = null;
// 继承自其他线程的局部变量初始值
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

简单来说:Thread 中声明了 ThreadLocal.ThreadLocalMap 类型的引用,ThreadLocalMap 中有一个 Entry<ThreadLocal<?>> 数组。
ThreadLocal

使用

在上述 TokenHolder 的例子中,调用了 ThreadLocal 的三个常用方法:set、get、remove。
在此之前,需要先讲讲 ThreadLocalMap 中使用的哈希算法。

哈希算法

ThreadLocalMap 中存储着 Entry 类型的数组,如何确定每个元素的数组下标?

Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);

i 即为 ThreadLocal 类型的对象 key 在散列表 table 中对应的数组下标。

使用开放定址法:当冲突发生时,使用某种探测技术在散列表中形成一个探测序列。沿此序列逐个单元地查找,直到找到给定的关键字,或者碰到一个开放的地址(即该地址单元为空)为止。

开放定址法的缺点:

  1. 容易产生堆积问题,不适于大规模的数据存储。
  2. 散列函数的设计对冲突会有很大的影响,插入时可能会出现多次冲突的现象。
  3. 删除的元素是多个冲突元素中的一个,需要对后面的元素作处理,实现较复杂。

这里采用开放定址法的原因:

  1. 0x61c88647 让哈希码能均匀的分布在 2 的 N 次方的数组里。
  2. ThreadLocal 往往存放的数据量不会特别大(而且 key 是弱引用又会及时被垃圾回收),开放地址法简单的结构会更省空间,查询效率更高,采用的散列函数也能使冲突概率变低。

如何降低冲突,看 threadLocalHashCode 值的计算方式:

public class ThreadLocal<T> {
    
    /**
     * ThreadLocal依赖于附加到每个线程的线性探针哈希映射
     * ThreadLocal对象充当键,通过该字段值进行搜索
     */
    private final int threadLocalHashCode = nextHashCode();

    /** 要给出的下一个哈希码,原子更新,从零开始 */
    private static AtomicInteger nextHashCode = new AtomicInteger();

    /**
     * 连续生成的哈希码之间的差异
     * 将隐式顺序的线程本地ID转换为接近最优分布的乘法哈希值,用于2次方大小的表(参考下面的例子)
     */
    private static final int HASH_INCREMENT = 0x61c88647;

    /** 返回下一个哈希码 */
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
    
    static class ThreadLocalMap {
        
        /** 初始容量,必须是2的幂 */
        private static final int INITIAL_CAPACITY = 16;
        
        // 惰性构造
        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);
        }
    }
}

每创建一个 ThreadLocal 对象,nextHashCode 就会增加 0x61c88647。

0x61c88647 是斐波那契数,也叫黄金分割数。增量为这个数字,好处就是 hash 的分布非常均匀。
测试一下:

public class ThreadLocalTest {

    private static final int HASH_INCREMENT = 0x61c88647;
    private static final int INITIAL_CAPACITY = 16;

    public static void main(String[] args) {
        int hashCode = 0;
        for (int n = 0; n < INITIAL_CAPACITY; n++) {
            hashCode = n * HASH_INCREMENT + HASH_INCREMENT;
            int index = hashCode & (INITIAL_CAPACITY -1);
            System.out.println("第 " + n + " 个哈希码在数组中的位置:" + index);
        }
    }
}

打印结果:

第 0 个哈希码在数组中的位置:7
第 1 个哈希码在数组中的位置:14
第 2 个哈希码在数组中的位置:5
第 3 个哈希码在数组中的位置:12
第 4 个哈希码在数组中的位置:3
第 5 个哈希码在数组中的位置:10
第 6 个哈希码在数组中的位置:1
第 7 个哈希码在数组中的位置:8
第 8 个哈希码在数组中的位置:15
第 9 个哈希码在数组中的位置:6
第 10 个哈希码在数组中的位置:13
第 11 个哈希码在数组中的位置:4
第 12 个哈希码在数组中的位置:11
第 13 个哈希码在数组中的位置:2
第 14 个哈希码在数组中的位置:9
第 15 个哈希码在数组中的位置:0

table 中存储的值将如下所示:

 key   k15  k6  k13  k4  k11  k2  k9  k0  k7  k14  k5  k12  k3  k10  k1  k8
	  ┌─────────────────────────────────────────────────────────────────────┐
value │ 15 │ 6 │ 13 │ 4 │ 11 │ 2 │ 9 │ 0 │ 7 │ 14 │ 5 │ 12 │ 3 │ 10 │ 1 │ 8 │
      └─────────────────────────────────────────────────────────────────────┘        
index    0   1   2    3    4   5   6   7   8   9   10   11   12  13   14  15

此处的散列函数为:f(x) = x & (table.length - 1)。

例如,假设初始哈希码对应条目的 value = 0,下一个哈希码对应条目的 value = 1,以此类推,第 n 个哈希码对应条目的 value = n。若要向初始数组中写入 key = k1 & value = 27 的条目,计算可得下标为 4,而此时该槽位已存在 key = k2 & value = 11 的条目,故向后线性探测,会有三个结果:

  • 找到一个空槽存入;
  • 找到之前已存在的 key = k1 的条目进行更新;
  • 找到一个 key = null 的条目(陈旧条目)进行替换。

set

由我们直接调用的 ThreadLocal.set() 方法。

// 将此线程局部变量的当前线程副本设置为指定值
public void set(T value) {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 从线程获取局部变量映射
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // this即ThreadLocal类对象
        map.set(this, value);
    } else {
        createMap(t, value);
    }
}

其中的 getMap() 方法:

ThreadLocalMap getMap(Thread t) {
    // 获取线程对局部变量副本的引用
    return t.threadLocals;
}

其中的 createMap() 方法:

void createMap(Thread t, T firstValue) {
    // 放入ThreadLocal类对象与相关联的值
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

由此也可知:ThreadLocalMap 是惰性构造的,只有在至少有一个条目可以放入时才会被创建。

下面着重来看 ThreadLocalMap#set() 方法:

private void set(ThreadLocal<?> key, Object value) {

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

继续看 replaceStaleEntry() 方法:

将陈旧条目替换为指定键的条目。将 value 参数传递的值存储在条目中,无论指定键的条目是否已经存在。
副作用是,此方法会清除“运行”中的所有陈旧条目。 (“运行”是指两个空槽之间的一系列条目。)
注释的说法很抽象,看源码。

private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;

    // 设置待删除槽位值为初始陈旧槽
    int slotToExpunge = staleSlot;
    // 从初始槽向前遍历,找到最前面的陈旧条目(遇到空槽为止),更新待删除槽位值
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;

    // 从初始槽向后遍历(遇到空槽为止)
    for (int i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();

        // 匹配到key,需将初始陈旧槽与该陈旧条目交换以维护哈希表顺序(使偏移数据的位置更合理)
        // (在此处匹配到了,说明tab[i]的条目当时在插入时目标槽被占用了,向后线性探测到此的)
        if (k == key) {
            e.value = value;
			
            // 交换至初始陈旧槽的位置(相当于将其rehash到合适的位置)
            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;

            // 如果待删除槽就是初始陈旧槽(说明陈旧槽之前没有更多的陈旧条目了)
            if (slotToExpunge == staleSlot)
                // 更新待删除槽位值为交换后的陈旧槽位置
                slotToExpunge = i;
            // 将待删除槽传入expungeStaleEntry以清理或重新散列运行中的所有其他条目
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }

        // 如果发现新的陈旧条目,且待删除槽就是初始陈旧槽(说明陈旧槽之前没有更多的陈旧条目了)
        if (k == null && slotToExpunge == staleSlot)
            // 将待删除槽位值更新为新发现的
            slotToExpunge = i;
    }

    // 如果未找到key或新的陈旧槽,则将新条目放入初始陈旧槽中
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);

    // 如果运行中有任何其他陈旧条目,则清理它们
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

清理

ThreadLocalMap 中的陈旧条目,有探测式和启发式清理,对应 expungeStaleEntry() 和 cleanSomeSlots() 方法。

探测式清理

从当前槽位向后清理,遇到值为 null 则结束,属于线性探测清理。

/** 
 * 通过重新散列位于staleSlot和下一个空槽之间的任何可能冲突的条目来清除陈旧的条目
 * 这也会清除在尾随空值之前遇到的任何其他陈旧条目
 * @param 已知具有空键的槽索引
 * @return staleSlot后下一个空槽的索引(所有在staleSlot和这个槽之间的都将被检查以进行清除)
 */
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--;
        } else {
            // 计算当前条目的对应下标h
        	int h = k.threadLocalHashCode & (len - 1);
            // 不等于当前条目的槽位下标i,说明该条目是被偏移过的
            if (h != i) {
                // 置空当前槽位
            	tab[i] = null;

                // 从”正确“下标向后遍历,找到空槽存放当前条目,使之更接近”正确“位置
                while (tab[h] != null)
                	h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}

经过一轮探测式清理后,陈旧条目会被清理掉,正常的条目经过 rehash 后所处的位置理论上更接近 i= key.hashCode & (tab.len - 1) 的位置,这种优化会提高整个散列表查询性能。

启发式清理

启发式扫描一些单元格以查找过时的条目。

在添加新元素或删除另一个陈旧元素时调用。它执行对数扫描,作为不扫描(快速但保留垃圾)和 扫描次数与元素数量成正比 之间的平衡,这将找到所有垃圾但会导致某些插入花费 O(n) 时间。

参数 i 是已知不会持有过时条目的位置。
参数 n 是扫描控制:扫描 log2(n) 单元格,除非找到过时的条目,在这种情况下,将扫描 log2(table.length) - 1 附加单元格。从插入调用时,此参数是元素数,但从 replaceStaleEntry 调用时,它是表长度。(注意:所有这些都可以通过对 n 进行加权而不是仅使用直接对数 n 来更变得或多或少的激进。但此版本简单、快速,并且似乎运行良好。)

// 如果删除了任何陈旧条目,则返回true
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];
		if (e != null && e.get() == null) {
			n = len;
			removed = true;
            i = expungeStaleEntry(i);
    	}
    } while ( (n >>>= 1) != 0);
		return removed;
} 

比如对以下数据执行 cleanSomeSlots(0, 16):

 key   k15  k6           k11      k9      k7       k5       k3       k1    
	  ┌─────────────────────────────────────────────────────────────────────┐
value │ 15 │ 6 │ 13 │   │ 11 │   │ 9 │   │ 7 │    │ 5 │    │ 3 │    │ 1 │   │
      └─────────────────────────────────────────────────────────────────────┘        
index    0   1   2    3    4   5   6   7   8   9   10   11   12  13   14  15

第一轮 n >>>= 1 的值是 8,i 的下一个位置是 1,槽位 1 中不是陈旧条目,跳过;第二轮 n >>>= 1 的值是 4,i 的下一个位置是 2,槽位 2 中是陈旧条目,则将 n 重置为表长度 16,并对槽位 2 进行探测式清理,将最终被清理的槽位 2 的下标赋给 i;第二轮 n >>>= 1 的值又是 8,i 的下一个位置是 3,空槽跳过;第四轮 n >>>= 1 的值又是 4,i 的下一个位置是 4,不是陈旧条目,跳过;以此类推,4 >>>= 1 是 2, 2 >>>= 1 是 1,1 >>>=1 是 0 止。

扩容

在 ThreadLocalMap.set() 方法的最后,启发式清理未删除任何条目,且散列数组中条目的数量已经达到扩容阈值,就开始执行 rehash() 逻辑。

if (!cleanSomeSlots(i, sz) && sz >= threshold)
	rehash();

rehash():重新包装或调整表的大小。首先扫描整个表,删除陈旧条目。若这不能充分缩小表的大小,请将表大小加倍。

private void rehash() {
    // 探测式清理所有陈旧条目
	expungeStaleEntries();

 	// size >= threshold * 3/4,使用较低的加倍阈值以避免滞后
	if (size >= threshold - threshold / 4)
		resize();
}
// 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;

    for (Entry e : oldTab) {
        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;
}

get

由我们直接调用的 ThreadLocal.get() 方法:返回此线程局部变量的当前线程副本中的值。如果该变量对于当前线程没有值,则首先将其初始化为 initialValue() 方法返回的值。

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() 方法是用于建立初始值的 set() 变体,代替 set() 以防用户覆盖 set() 方法。

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);
    }
    if (this instanceof TerminatingThreadLocal) {
        TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
    }
    return value;
}

initialValue() 的默认实现仅返回 null。如果开发者希望线程局部变量具有除 null 以外的初始值,则必须对 ThreadLocal 进行子类化,并覆盖此方法。通常使用匿名内部类。

initialValue() 方法将在线程第一次使用 get 方法访问变量时调用,除非线程之前调用了 set 方法。通常,每个线程最多调用此方法一次,但在后续调用 remove 后跟 get 的情况下,它可能会再次调用。

也就是说,要调用 get 方法,需先赋值:调用 set 方法;或手动覆盖 initialValue 方法。

ThreadLocal<String> threadLocalStr = new ThreadLocal<String>() {
	@Override
	protected String initialValue() {
		return "hello world"; 
	}
};

接着看 ThreadLocalMap.getEntry 方法,此方法本身仅处理快速路径:直接命中现有key。否则它会中继到 getEntryAfterMiss。这旨在最大限度地提高直接命中的性能,部分原因是使该方法易于内联。

// 获取与key关联的条目
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;
}

remove

由我们直接调用的 ThreadLocal.remove() 方法:删除此线程局部变量在当前线程中的值。如果此线程局部变量随后再被当前线程读取,它的值将通过调用 initialValue() 方法被重新初始化,除非它的值在过渡期被当前线程 set。这可能会导致 initialValue() 方法在当前线程中被多次调用。

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

具体逻辑位于 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;
		}
	}
}

弱引用与内存泄漏

弱引用

判定对象是否存活,与对象是否被引用有关。程序员无法控制垃圾回收线程,什么时候释放内存,销毁哪些对象,这些都是都 jvm 自己控制的,但是随着 java.lang.ref 这个包下的类的引进,程序员拥有了一点点控制创建的对象何时释放,销毁的权利。

在 jdk1.2 之前,对引用的定义:如果 Reference 类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。
这个定义很狭窄,我们希望能描述这样一类对象:当内存空间还足够时,则能保留在内存中,当内存空间在进行垃圾收集后还是很紧张,则可以抛弃。

于是 jdk1.2 之后,Java 对引用的概念进行了扩充,由强到弱分为 4 类:

  • 强引用(Strong Reference):在程序代码中普遍存在的,类似 “Object obj = new Object()” 中 obj 这类的引用,垃圾收集器永远不会回收被强引用关联的对象。
  • 软引用(Soft Reference):用来描述一些还有用但并非必需的对象,在系统将要发生内存溢出异常前,会把被软引用关联的对象列入回收范围中进行第二次回收,若这次还没有足够的内存,则抛出内存溢出异常。
  • 弱引用(Weak Reference):用来描述非必需的对象,被弱引用关联的对象,只能生存到下一次垃圾回收之前,即 GC 工作时,无论当前内存是否足够,被弱引用关联的对象都会被回收。
  • 虚引用(Phantom Reference):也称幽灵引用或幻影引用,完全不会对被关联对象的生存时间构成影响,无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的,就是能在这个这个对象被垃圾收集器回收时收到一个系统通知。

ThreadLocal 中的弱引用

ThreadLocalMap 中的 Entry 继承了 WeakReference,也就是 Entry 对象中持有的是对 ThreadLocal 对象的弱引用。

再贴一下示例和图:

public class TokenHolder {
    
    private static ThreadLocal<String> tokenThreadLocal = new ThreadLocal<>();
    
    public static void setToken(String token) {
        tokenThreadLocal.set(token);
    }
    
    public static String getToken() {
        return tokenThreadLocal.get();
    }
    
    public static void clear() {
        tokenThreadLocal.remove();
    }
}

ThreadLocal
如图所示,一个 ThreadLocal 对象,它同时受到来自两处的引用:一是我们直接声明的 tokenThreadLocal 字段(作为 key),二是当前线程局部变量副本 ThreadLocalMap 对象 中的 Entry 对象(存储 value)。

我们对 ThreadLocal 对象的操作,都是通过直接声明的 tokenThreadLocal 进行的,故此处使用了强引用;而程序在执行时,会通过当前线程获取到 ThreadLocalMap 对象,进而操作其中 Entry 存储的 value 值,那这里为什么使用了弱引用?

假设 Entry 中的是强引用,当我们不再需要使用 tokenThreadLocal 字段时,比如直接将 tokenThreadLocal 设为 null,此时堆中的 ThreadLocal 实例,会因为受到来自当前线程中 Entry 的引用而无法被 GC,这就造成了内存泄漏(暂时的),只有当线程被销毁时,ThreadLocal 实例才能随之被 GC。但在使用线程池的时候,线程结束是不会被销毁的,这就会导致真正的(长期近乎永久)内存泄漏了。

关于使用规范:当 ThreadLocal 对象使用完毕后,应该手动调用其 remove() 方法。原因如下。
当直接把 ThreadLocal 对象的引用置为 null 后,堆中的 ThreadLocal 实例被 GC,而由于来自当前线程的强引用,Entry 中存储的 value 对象未被回收,且无法再通过 ThreadLocal 对象的 get() 方法被访问到,变为陈旧条目(k == null),这也造成了内存泄漏(当然也是暂时的,之后陈旧条目会被清理)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值