ThreadLocal内部结构深度解析

目录

一、ThreadLocal

 例子

内部结构分析

ThreadLocal.get()源码解析

 图示详解

 二、ThreadLocal.set()

源码流程图

 源码解析

思考

 三、ThreadLocal.remove()

流程

 源码解析

四、ThreadLocal的内存泄露

延伸--Java的四种引用类型

发生内存泄露的场景


ThreadLocal是Java中一个非常重要且常用的线程局部变量工具类,它使得每个线程可以独立地持有自己的变量副本,而不是共享变量,解决了多线程环境下变量共享的线程安全问题。下面我将从多个维度深入分析ThreadLocal的内部结构和工作原理。

一、ThreadLocal

// 1. 初始化:创建ThreadLocal变量
private static ThreadLocal<T> threadLocal = new ThreadLocal<>();

// 2. 设置值:为当前线程设置值
threadLocal.set(value);  // value为要存储的泛型对象

// 3. 获取值:获取当前线程的值
T value = threadLocal.get();  // 返回当前线程存储的值

// 4. 移除值:清除当前线程的ThreadLocal变量(防止内存泄漏)
threadLocal.remove();

【注】使用时,通常将ThreadLocal声明为static final以保证全局唯一性

private static ThreadLocal<T> threadLocal = ThreadLocal.withInitial(() -> initialValue);

【注:】 withInitial里面放的是任何能够返回 T 类型实例的 Lambda / Supplier
只要 Supplier 的逻辑最终能 new(或从缓存、工厂、单例池等)拿出一个 T,就合法。

 例子

例子中看似我是在讲add()方法,但是要知道ThreadLocal里面底层是没有add这个方法的,这只是实现的一个业务逻辑,而底层是用get()方法实现的,所以下面的源码解析,也是在解析get()方法。

package com.qcby.test;

import java.util.List;
import java.util.ArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class ThreadLocalTest {
    private List<String> messages = new ArrayList<>();

    public static final ThreadLocal<ThreadLocalTest> holder = ThreadLocal.withInitial(ThreadLocalTest::new);

    public static void add(String message) {
        holder.get().messages.add(message);
    }

    public static List<String> clear() {
        List<String> messages = holder.get().messages;
        holder.remove();
        return messages;
    }

    public static void main(String[] args) throws InterruptedException {
        // 创建线程池
        ExecutorService executor = Executors.newFixedThreadPool(10);

        // 提交10个任务
        for (int i = 0; i < 10; i++) {
            final int threadId = i;
            executor.submit(() -> {
                    ThreadLocalTest.add("线程" + threadId + "的消息" );
                // 打印当前线程的消息
                System.out.println("线程" + threadId + "的消息列表: " + holder.get().messages);
                // 清除当前线程的ThreadLocal
                ThreadLocalTest.clear();
            });
        }

        // 关闭线程池
        executor.shutdown();
        executor.awaitTermination(1, TimeUnit.SECONDS);

        // 主线程检查自己的ThreadLocal(应该是空的)
        System.out.println("主线程的消息列表: " + holder.get().messages);
    }
}

内部结构分析

根据这里get的源码追溯分析:

追溯到:

ThreadLocal.get()源码解析

/**
 * 获取当前线程的ThreadLocal变量值
 */
public T get() {
    // 1. 获取当前线程对象
    Thread t = Thread.currentThread();
    
    // 2. 获取当前线程的ThreadLocalMap(线程私有数据存储结构)
    ThreadLocalMap map = getMap(t);
    
    // 3. 如果map已存在
    if (map != null) {
        // 3.1 以当前ThreadLocal实例为key(也就是代码中的holder),获取对应的Entry
        ThreadLocalMap.Entry e = map.getEntry(this);
        
        // 3.2 如果Entry存在
        if (e != null) {
            // 3.2.1 强转为泛型类型并返回值
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    
    // 4. 如果map不存在或未找到值,初始化并返回默认值
    return setInitialValue();
}

/**
 * 获取线程的ThreadLocalMap(实际是Thread类的threadLocals字段)
 */
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals; // 直接返回线程对象的成员变量
}

/**
 * 初始化值并存入ThreadLocalMap
 */
private T setInitialValue() {
    // 1. 获取初始值(子类可重写initialValue()方法)
    T value = initialValue();
    
    // 2. 获取当前线程
    Thread t = Thread.currentThread();
    
    // 3. 获取线程的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    
    // 4. 如果map已存在,直接设置值
    if (map != null) {
        map.set(this, value);
    } else {
        // 5. 如果map不存在,创建新map并存入初始值
        createMap(t, value);
    }
    
    // 6. 返回初始值
    return value;
}

/**
 * 创建线程的ThreadLocalMap并存入第一个值
 */
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

/**
 * 默认初始值实现(可被withInitial覆盖)
 */
protected T initialValue() {
    return null; // 默认返回null
}
 图示详解

所以执行结果:

可以看见一个线程中只有一个信息,而不是它们统一堆砌在一起,原因就是底层是每个线程创建了一个Map对象,每个Map的value就是存入的messages本质是对象,也就是T--ThreadLocalTest对象们,并且它们Map中的Entry中的Key值都是一样的,都是这个ThreadLocal,也就是holder。

注】并不是每个线程的Map只能存放一个value对象,是这里我展示的例子里,一个线程只存了一条,完全可以存入很多条消息,然后add()时就会累加在Map已经创建好的Entry后面也就是:

当然既然是Map,存储Entry就涉及Hash了,这个以后再详谈。

 二、ThreadLocal.set()

源码流程图

 源码解析

/**
 * 设置当前线程的ThreadLocal变量值
 * @param value 要存储的值,将保存在当前线程的ThreadLocal副本中
 */
public void set(T value) {
    // 1. 委托给内部set方法,传入当前线程和值
    set(Thread.currentThread(), value);
    
    // 2. 如果启用了虚拟线程追踪标志,进行堆栈转储
    if (TRACE_VTHREAD_LOCALS) {
        dumpStackIfVirtualThread();
    }
}

/**
 * 设置载体线程的ThreadLocal变量值(专用于虚拟线程场景)
 * @param value 要存储的值,将保存在载体线程的ThreadLocal副本中
 */
void setCarrierThreadLocal(T value) {
    // 1. 断言当前对象必须是CarrierThreadLocal类型
    assert this instanceof CarrierThreadLocal<T>;
    
    // 2. 委托给内部set方法,传入当前载体线程和值
    set(Thread.currentCarrierThread(), value);
}

/**
 * 内部set方法,实际执行存储操作
 * @param t 目标线程对象
 * @param value 要存储的值
 */
private void set(Thread t, T value) {
    // 1. 获取目标线程的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    
    // 2. 如果map已存在
    if (map != null) {
        // 2.1 直接设置键值对(以当前ThreadLocal实例为key)
        map.set(this, value);
    } else {
        // 3. 如果map不存在,创建新map并存入初始值
        createMap(t, value);
    }
}

/**
 * 获取线程的ThreadLocalMap(实际是Thread类的threadLocals字段)
 * @param t 目标线程对象
 * @return 线程的ThreadLocalMap对象
 */
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals; // 直接返回线程对象的成员变量
}

/**
 * 创建线程的ThreadLocalMap并存入第一个值
 * @param t 目标线程对象
 * @param firstValue 要存储的第一个值
 */
void createMap(Thread t, T firstValue) {
    // 创建新的ThreadLocalMap,以当前ThreadLocal为key,firstValue为值
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

/**
 * 虚拟线程调试方法:如果是虚拟线程则转储堆栈
 */
private void dumpStackIfVirtualThread() {
    // 调试用方法,当TRACE_VTHREAD_LOCALS为true时调用
    if (Thread.currentThread().isVirtual()) {
        new Exception("VirtualThreadLocal set").printStackTrace();
    }
}

思考

看过源码,尤其是流程图后,会感觉set和get好像十分相像,都是会先判断有没有Map,没有就初始化一个新的,然后调用get时会通过withInitial获取一个ThreadLocalTest实例,放在一个Entry中,默认key是Thread Local,然后value是ThreadLocalTest,也就是message。

那么究竟有什么区别呢?或是换个问题,为什么我们在例子中add()方法中使用get,而不是set方法呢?这就涉及到它们的核心源码了:

// get() 的逻辑(保留旧值)
if (map != null) {
    Entry e = map.getEntry(this);
    if (e != null) {
        return (T)e.value; // 直接返回,不修改
    }
}
return setInitialValue(); // 懒初始化
==========================================
// set() 的逻辑(覆盖旧值)
if (map != null) {
    map.set(this, value); // 强制更新
} else {
    createMap(t, value); // 初始化新Map
}

总结而言:

行为get()set()
对已有值的处理保留旧值,直接返回覆盖旧值
初始化值的来源通过 withInitial 的 Supplier使用传入的新值
典型用途获取并操作线程本地变量强制更新线程本地变量
内存泄漏风险有(需手动清理旧值)

就可以形象理解为:

操作类比现实场景结果
get()打开抽屉,取出笔记本继续写笔记笔记内容不断累积
set()扔掉旧笔记本,换一本新的空本子旧笔记丢失,只能写新内容

 三、ThreadLocal.remove()

流程

调用remove()
  ↓
获取当前线程的ThreadLocalMap
  ↓
找到对应ThreadLocal的Entry
  ↓
清除Entry的Key弱引用
  ↓
expungeStaleEntry():
  1. 清理当前槽位
  2. 向后探测清理过期Entry
  3. 重新哈希非过期Entry
  ↓
减少Map的size计数

 源码解析

/**
 * 移除当前线程的ThreadLocal变量值。此操作会:
 * 1. 清除当前ThreadLocal实例关联的Entry
 * 2. 探测式清理哈希表中其他过期的Entry
 * 3. 避免内存泄漏(关键方法)
 */
public void remove() {
    // 1. 获取当前线程的ThreadLocalMap(可能为null)
    ThreadLocalMap m = getMap(Thread.currentThread());
    
    // 2. 如果Map存在,则移除当前ThreadLocal对应的Entry
    if (m != null) {
        m.remove(this);  // this指当前ThreadLocal实例
    }
}

/**
 * ThreadLocalMap内部移除Entry的核心方法
 * @param key 要移除的ThreadLocal实例(弱引用Key)
 */
private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    
    // 1. 计算初始哈希槽位置
    int i = key.threadLocalHashCode & (len-1);
    
    // 2. 线性探测查找目标Entry
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {  // 匹配Key
            
            // 3. 清除Key的弱引用
            e.clear();  // 调用WeakReference.clear()
            
            // 4. 探测式清理过期Entry(关键!)
            expungeStaleEntry(i);
            return;
        }
    }
}

/**
 * 探测式清理过期Entry(扩容和读取时也会触发此逻辑)
 * @param staleSlot 已知的过期Entry位置
 * @return 返回下一个可能为空的槽位索引
 */
private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // 1. 清理当前槽位的过期Entry
    tab[staleSlot].value = null;  // 释放Value强引用
    tab[staleSlot] = null;        // 清空槽位
    size--;                       // 更新大小

    // 2. 向后遍历进行探测式清理
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        
        if (k == null) {
            // 2.1 发现其他过期Entry,一并清理
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            // 2.2 对非过期Entry重新哈希
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {  // 如果不在最佳位置
                tab[i] = null;  // 清空当前位置
                
                // 2.3 将Entry移动到更接近最佳位置的空槽
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}

/**
 * 清除Entry的Key引用(继承自WeakReference)
 */
public void clear() {
    super.clear();  // 将referent(Key)置为null
}

/**
 * 获取下一个哈希槽位置(线性探测法)
 * @param i 当前索引
 * @param len table长度
 * @return 下一个索引
 */
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);  // 环形探测
}

四、ThreadLocal的内存泄露

在讲解ThreadLocal的内存泄漏前,先来讲解一下内存泄漏和内存溢出:

特征内存泄漏内存溢出
本质内存未释放,逐渐积累内存申请超过系统上限
过程渐进式(长时间运行后暴露)突发性(立即或短时间内)
结果可能最终导致溢出直接导致程序崩溃
解决方向找到未释放的引用或资源减少内存占用或增加系统内存

 下图是ThreadLocal相关的内存结构图,在栈区中有threadLocal对象和当前线程对象,分别指向堆区真正存储的类对象,这俩个指向都是强引用。在堆区中当前线程肯定是只有自己的Map的信息的,而Map中又存储着一个个的Entry节点;在Entry节点中每一个Key都是ThreadLocal的实例,同时Value又指向了真正的存储的数据位置,以上便是下图的引用关系。

延伸--Java的四种引用类型

  • 强引用:我们常常new出来的对象就是强引用类型,只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足的时候
  • 软引用:使用SoftReference修饰的对象被称为软引用,软引用指向的对象在内存要溢出的时候被回收
  • 弱引用:使用WeakReference修饰的对象被称为弱引用,只要发生垃圾回收,若这个对象只被弱引用指向,那么就会被回收
  • 虚引用:虚引用是最弱的引用,在 Java 中使用 PhantomReference 进行定义。虚引用中唯一的作用就是用队列接收对象即将死亡的通知

 ThreadLocal中Entry的设定:

static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value; // 实际存储的值(强引用)
    Entry(ThreadLocal<?> k, Object v) {
        super(k); // Key 是弱引用
        value = v;
    }
}

//所以可以看出,严格意义来说,key这里存储的是ThreadLocal的弱引用
  • 键(Key)ThreadLocal 实例(弱引用,防止内存泄漏)。

  • 值(Value):线程局部变量(强引用,需手动清理)。

发生内存泄露的场景

①ThreadLocal 被回收(没有外部强引用):

例如:ThreadLocal tl = new ThreadLocal(); tl = null;tl 被 GC 回收)。

②线程未结束(如线程池中的线程长期存活):

ThreadLocalMap 仍然持有 Entry,但 Entry 的 keyThreadLocal)已经是 null,而 value 仍然占用内存。

③未调用 remove()

如果业务代码没有显式调用 ThreadLocal.remove()value 会一直存在,导致内存泄漏。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值