ThreadLocal

本文详细解析了JavaThreadLocal的工作原理,包括数据存放和获取流程,内存泄露问题以及在线程池中的注意事项。特别强调了在使用后调用remove()方法以防止内存泄漏和线程池问题。

一、序言

本文聊聊 ThreadLocal。

二、ThreadLocal 源码

package java.lang;
import jdk.internal.misc.TerminatingThreadLocal;

import java.lang.ref.*;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;

public class ThreadLocal<T> {

    private final int threadLocalHashCode = nextHashCode();

    private static AtomicInteger nextHashCode = new AtomicInteger();

    private static final int HASH_INCREMENT = 0x61c88647;

    public ThreadLocal() {
    }

    // 省略除成员变量和构造器之外的代码
}

上面是 ThreadLocal 的源码,省略了除成员变量和构造器之外的代码。当使用构造器实例化一个 ThreadLocal 对象时,除了成员变量 threadLocalHashCodenextHashCode 与常量 HASH_INCREMENT 均有默认值就没有做其他的动作了。

三、ThreadLocal#set()

public void set(T value) {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取当前线程的 ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    // 判断 ThreadLocalMap 是否存在
    if (map != null) {
        // 设置值到 ThreadLocalMap
        map.set(this, value);
    } else {
        // 否则,创建新的 ThreadLocalMap 并设置值
        createMap(t, value);
    }
}

上面的是 ThreadLocal#set() 方法的源码。源码中有一个 getMap() 方法和 createMap() 方法:

ThreadLocalMap getMap(Thread t) {
    // 通过调用当前线程的 threadLocals 成员变量获取 ThreadLocalMap 对象
    return t.threadLocals;
}

void createMap(Thread t, T firstValue) 
    // 创建一个 ThreadLocalMap 对象,并将值放入
    // 将新建的 ThreadLocalMap 对象赋给当前线程的 threadLocals 成员变量
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

ThreadLocal#set() 方法工作流程:
在这里插入图片描述

四、ThreadLocal#get()

public T get() {
    // 获取了当前线程的引用
    Thread t = Thread.currentThread();
    // 获取了当前线程的 ThreadLocalMap 对象
    ThreadLocalMap map = getMap(t);
    
    // 检查当前线程是否有 ThreadLocalMap
    if (map != null) {
        // 从 ThreadLocalMap 中获取了一个 Entry 对象
        // 获取 ThreadLocalMap 中,键为当前 ThreadLocal 的 Entry
        ThreadLocalMap.Entry e = map.getEntry(this);
        // 检查 Entry 对象是否存在
        if (e != null) {
            // 获取了 Entry 对象的值,并将其转型为 T 类型。
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            // 返回了线程局部变量的值
            return result;
        }
    }
    
    // 设置一个初始值(这个初始值为 null),并返回
    return setInitialValue();
}

上面是 ThreadLocal#get() 的源码。其工作流程如下:
在这里插入图片描述

五、ThreadLocal#remove()

public void remove() {
    // 获取了当前线程的 ThreadLocalMap 对象
    ThreadLocalMap m = getMap(Thread.currentThread());
    // 判断当前线程 ThreadLocalMap 对象是否存在
    if (m != null) {
        // 删除 ThreadLocalMap 中键为当前 ThreadLocal 的数据
        m.remove(this);
    }
}

六、ThreadLocal 工作流程

6.1 ThreadLocal 数据存放流程

在这里插入图片描述

  1. 向 ThreadLocal 中存放数据时,首先会判断线程绑定的 ThreadLocalMap 是否存在
  2. 如果存在,将 ThreadLocal 作为 key 存入 ThreadLocalMap 中
  3. 如果不存在,则先创建 ThreadLocalMap,然后执行第 2 步的操作

6.2 ThreadLocal 数据获取流程

在这里插入图片描述

  1. 向 ThreadLocal 中获取数据时,首先会判断线程绑定的 ThreadLocalMap 是否存在
  2. 如果存在,将获取 ThreadLocal 为 key 的值
  3. 如果不存在,null 将会作为默认值返回(在这之前同样会新建 ThreadLocalMap)

七、FAQ

7.1 ThreadLocalMap#set()

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

    // 获取了 ThreadLocalMap 的内部数组 table
    // 这个数组存储了所有的 Entry 对象。
    Entry[] tab = table;
    // 获取 tab 的长度
    int len = tab.length;
    // 计算 ThreadLocal 的 hash 值,并将其映射到 table 的索引范围内
    int i = key.threadLocalHashCode & (len-1);

    // 循环遍历 tab
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        
        // 从 ThreadLocalMap 中的 Entry 对象 e 中获取 ThreadLocal 对象 k
        ThreadLocal<?> k = e.get();

        // 检查当前的 Entry 对象的键是否等于 ThreadLocal 对象
        if (k == key) {
            // 将 Entry 对象的值设置为新的值
            e.value = value;
            return;
        }

        // 检查当前的 Entry 对象的键是否为 null
        if (k == null) {
            // 调用 replaceStaleEntry 方法来替换这个 Entry 对象
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    // 在 table 中创建了一个新的 Entry 对象
    // 键是 ThreadLocal 对象,值是新的值。
    tab[i] = new Entry(key, value);
    // Entry 对象的数量 +1
    int sz = ++size;

    // 检查 ThreadLocalMap 是否需要进行哈希表的再哈希操作
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        // 调用 rehash 方法来进行再哈希
        rehash();
}

7.2 ThreadLocal 内存泄露

之前 ThreadLocalMap#set() 源码中有这么一行代码 tab[i] = new Entry(key, value),我们看看 Entry 对象究竟是什么。

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

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

上述代码是 Entry 的源码。Entry 继承自 WeakReference,也就是说 Entry 是一个弱引用。ThreadLocal 的键是对 ThreadLocal 对象的弱引用,而值是强引用。这就导致了一个问题,ThreadLocal 在没有外部对象强引用时,发生 GC 时弱引用 key 会被回收,而 value 不会回收。如果线程没有结束,但 ThreadLocal 已经被回收,则可能导致线程中存在 ThreadLocalMap<null, Object> 的键值对,造成内存泄漏。

7.3 ThreadLocal 线程池问题

线程池中的核心线程一般是不会被销毁的。如果一个线程不销毁,那么跟随这个线程的 ThreadLocalMap 就一直存在,上次变量的变更,下次依然在使用。如果线程池中的一个线程执行了一次任务,而没有调用 remove() 方法,那么下次这个线程在执行任务的时候,就有可能会引发业务逻辑问题。所以,当不再需要 ThreadLocal 中的值进行业务逻辑计算的时候,就一定要调用 remove() 方法。这样可以避免内存泄漏和线程池中的问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值