JUC并发编程 ThreadLocal解析

ThreadLocal解析

1. ThreadLocal 简介

1.1 什么是 ThreadLocal

ThreadLocal提供线程局部变量。这些变量与正常的变量不同,因为每一个线程在访问ThreadLocal实例的时候(通过其get或set方法)都有自己的、独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态字段,使用它的目的是希望将状态(例如,用户ID或事务ID)与线程关联起来。

1.2 ThreadLocal 能解决什么问题

实现每一个线程都有自己专属的本地变量副本(自己用自己的变量不麻烦别人,不和其他人共享,人人有份,人各一份),主要解决了让每个线程绑定自己的值,通过使用get()和set()方法,获取默认值或将其值更改为当前线程所存的副本的值从而避免了线程安全问题。

graph TB A["多线程共享变量问题"] --> B["线程1访问共享变量"] A --> C["线程2访问共享变量"] A --> D["线程3访问共享变量"] B --> E["数据竞争"] C --> E D --> E E --> F["线程安全问题"] G["ThreadLocal解决方案"] --> H["线程1拥有变量副本1"] G --> I["线程2拥有变量副本2"] G --> J["线程3拥有变量副本3"] H --> K["线程隔离"] I --> K J --> K K --> L["避免线程安全问题"]

1.3 阿里巴巴开发规范

根据阿里巴巴Java开发手册的规范要求:

  • 必须回收自定义的ThreadLocal变量,尤其在线程池场景下,线程经常会被复用
  • 如果不清理自定义的ThreadLocal变量,可能会影响后续业务逻辑和造成内存泄露等问题
  • 尽量在代理中使用try-finally块进行回收

2. ThreadLocal 源码分析

2.1 Thread、ThreadLocal、ThreadLocalMap 关系

flowchart TD subgraph Thread["Thread对象"] T["Thread实例"] TLM["threadLocals字段
ThreadLocalMap类型"] end subgraph ThreadLocal["ThreadLocal对象"] TL["ThreadLocal实例"] SET["set()方法
🔍 hash冲突处理"] GET["get()方法"] REMOVE["remove()方法"] end subgraph Map["ThreadLocalMap结构 - 线性探测法处理hash冲突"] TABLE["Entry[] table数组
开放地址法"] subgraph HashDemo["Hash冲突示例"] E0["Entry[0]: null"] E1["Entry[1]: TL1→Value1"] E2["Entry[2]: TL2→Value2
🔄 hash冲突后探测"] E3["Entry[3]: TL3→Value3
🔄 继续探测"] E4["Entry[4]: null"] end end subgraph Entry["Entry内部结构"] KEY["WeakReference key
指向ThreadLocal"] VALUE["Object value
存储的实际值"] end subgraph HashProcess["Hash冲突处理流程"] HASH["1. 计算hash值
threadLocalHashCode & (len-1)"] CHECK["2. 检查位置是否占用"] PROBE["3. 线性探测下一位置
nextIndex(i, len)"] INSERT["4. 找到空位置插入"] end %% 关系连接 T --> TLM TL --> SET TL --> GET TL --> REMOVE TLM --> TABLE TABLE --> HashDemo HashDemo --> E0 HashDemo --> E1 HashDemo --> E2 HashDemo --> E3 HashDemo --> E4 E1 --> KEY E1 --> VALUE KEY -."弱引用".-> TL %% Hash处理流程 SET --> HASH HASH --> CHECK CHECK -->|"位置被占用"| PROBE PROBE --> CHECK CHECK -->|"位置空闲"| INSERT %% 样式设置 classDef threadStyle fill:#e3f2fd,stroke:#1976d2,stroke-width:2px classDef tlStyle fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px classDef mapStyle fill:#e8f5e8,stroke:#388e3c,stroke-width:2px classDef entryStyle fill:#fff3e0,stroke:#f57c00,stroke-width:2px classDef hashStyle fill:#ffebee,stroke:#d32f2f,stroke-width:2px classDef conflictStyle fill:#fff9c4,stroke:#f57f17,stroke-width:3px class T,TLM threadStyle class TL,GET,REMOVE tlStyle class SET conflictStyle class TABLE,HashDemo mapStyle class E0,E1,E4 mapStyle class E2,E3 conflictStyle class KEY,VALUE entryStyle class HASH,CHECK,PROBE,INSERT hashStyle

2.2 核心原理解析

2.2.1 Thread与ThreadLocalMap的关系

首先我们来看Thread类的定义,每个Thread对象都包含一个ThreadLocalMap:

java 复制代码
public class Thread implements Runnable {
    // 每个线程都有自己的ThreadLocalMap实例
    // 这个字段存储了该线程所有的ThreadLocal变量
    ThreadLocal.ThreadLocalMap threadLocals = null;
    
    // 继承的ThreadLocalMap,用于InheritableThreadLocal
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    
    // 其他Thread的字段和方法...
}
2.2.2 ThreadLocal的getMap方法详解
java 复制代码
/**
 * 获取当前线程的ThreadLocalMap
 * 这是ThreadLocal实现线程隔离的关键方法
 */
ThreadLocalMap getMap(Thread t) {
    // 直接返回线程对象的threadLocals字段
    // 每个线程都有自己独立的ThreadLocalMap实例
    return t.threadLocals;
}

/**
 * 为指定线程创建ThreadLocalMap
 * 当线程第一次使用ThreadLocal时会调用此方法
 */
void createMap(Thread t, T firstValue) {
    // 创建新的ThreadLocalMap实例,并赋值给线程的threadLocals字段
    // this表示当前ThreadLocal实例作为第一个key
    // firstValue是第一个存储的值
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
2.2.3 Entry结构与弱引用实现

ThreadLocalMap的Entry是实现弱引用的核心:

java 复制代码
/**
 * ThreadLocalMap的内部类Entry
 * 继承自WeakReference,实现对ThreadLocal的弱引用
 */
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** 
     * 与ThreadLocal关联的值
     * 注意:这里是强引用,只有key是弱引用
     */
    Object value;

    /**
     * Entry构造函数
     * @param k ThreadLocal对象,作为弱引用的key
     * @param v 存储的值,作为强引用的value
     */
    Entry(ThreadLocal<?> k, Object v) {
        // 调用WeakReference的构造函数,建立对ThreadLocal的弱引用
        // 当ThreadLocal对象没有其他强引用时,GC可以回收它
        super(k);
        value = v;
    }
}
2.2.4 ThreadLocal核心方法源码详解
java 复制代码
/**
 * ThreadLocal的set方法 - 存储值到当前线程
 * 🔍 关键:这里会调用ThreadLocalMap.set()方法处理hash冲突
 */
public void set(T value) {
    // 1. 获取当前执行的线程对象
    Thread t = Thread.currentThread();
    
    // 2. 获取当前线程的ThreadLocalMap
    //    如果线程第一次使用ThreadLocal,这里可能返回null
    ThreadLocalMap map = getMap(t);
    
    if (map != null)
        // 3. 🔍 关键调用:ThreadLocalMap.set()方法
        //    this作为key(当前ThreadLocal实例)
        //    value作为要存储的值
        //    内部使用线性探测法处理hash冲突
        map.set(this, value);
    else
        // 4. 如果ThreadLocalMap不存在,创建新的map并设置初始值
        createMap(t, value);
}

🔍 ThreadLocalMap.set()方法 - Hash冲突处理核心逻辑:

java 复制代码
/**
 * ThreadLocalMap的set方法 - 处理hash冲突的核心实现
 * 🔍 使用线性探测法(Linear Probing)解决hash冲突
 */
private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    
    // 🔍 步骤1:计算hash值,使用ThreadLocal的threadLocalHashCode
    //         通过位运算快速取模:hash & (len-1) 等价于 hash % len
    int i = key.threadLocalHashCode & (len-1);

    // 🔍 步骤2:线性探测法处理hash冲突
    //         从计算出的位置开始,逐个检查数组位置
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get(); // 获取弱引用指向的ThreadLocal

        // 🔍 情况1:找到相同的key,直接更新value
        if (k == key) {
            e.value = value;
            return;
        }

        // 🔍 情况2:遇到stale entry(key被GC回收,k为null)
        //         调用replaceStaleEntry进行清理和替换
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
        
        // 🔍 情况3:当前位置被其他ThreadLocal占用
        //         继续线性探测下一个位置:nextIndex(i, len)
        //         nextIndex实现:(i + 1 < len) ? i + 1 : 0
    }

    // 🔍 步骤3:找到空位置,创建新的Entry
    tab[i] = new Entry(key, value);
    int sz = ++size;
    
    // 🔍 步骤4:清理过期Entry并检查是否需要扩容
    //         cleanSomeSlots:启发式清理算法
    //         threshold:扩容阈值,通常为数组长度的2/3
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash(); // 扩容并重新hash
}

/**
 * 线性探测的下一个索引位置
 * 🔍 环形数组:到达末尾后回到开头
 */
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

/**
 * 线性探测的上一个索引位置
 * 🔍 用于向前查找和清理
 */
private static int prevIndex(int i, int len) {
    return ((i - 1 >= 0) ? i - 1 : len - 1);
}

/**

  • ThreadLocal的get方法 - 从当前线程获取值 */ public T get() { // 1. 获取当前执行的线程对象 Thread t = Thread.currentThread();

    // 2. 获取当前线程的ThreadLocalMap ThreadLocalMap map = getMap(t);

    if (map != null) { // 3. 如果map存在,尝试获取Entry // this作为key查找对应的Entry ThreadLocalMap.Entry e = map.getEntry(this);

    kotlin 复制代码
     if (e != null) {
         // 4. 如果找到Entry,返回其value
         @SuppressWarnings("unchecked")
         T result = (T)e.value;
         return result;
     }

    }

    // 5. 如果没有找到值,返回初始值并设置到map中 return setInitialValue(); }

/**

  • 设置初始值的方法 */ private T setInitialValue() { // 1. 调用initialValue()方法获取初始值 // 默认返回null,可以通过重写此方法或使用withInitial()来自定义 T value = initialValue();

    // 2. 获取当前线程 Thread t = Thread.currentThread();

    // 3. 获取当前线程的ThreadLocalMap ThreadLocalMap map = getMap(t);

    if (map != null) // 4. 如果map存在,设置初始值 map.set(this, value); else // 5. 如果map不存在,创建map并设置初始值 createMap(t, value);

    return value; }

/**

  • ThreadLocal的remove方法 - 移除当前线程的值 */ public void remove() { // 1. 获取当前线程的ThreadLocalMap ThreadLocalMap m = getMap(Thread.currentThread());

    if (m != null) // 2. 如果map存在,移除当前ThreadLocal对应的Entry // 这里会触发清理key为null的Entry m.remove(this); }

scss 复制代码
#### 2.2.5 ThreadLocalMap构造函数

```java
/**
 * ThreadLocalMap构造函数 - 创建线程专属的存储空间
 * @param firstKey 第一个ThreadLocal实例
 * @param firstValue 第一个要存储的值
 */
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    // 1. 初始化Entry数组,默认大小为16
    //    INITIAL_CAPACITY = 16,必须是2的幂次方
    //    这样可以使用位运算进行快速取模操作
    table = new Entry[INITIAL_CAPACITY];
    
    // 2. 计算第一个元素的存储位置
    //    使用ThreadLocal的threadLocalHashCode进行hash
    //    & (INITIAL_CAPACITY - 1) 等价于 % INITIAL_CAPACITY
    //    但位运算更快
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    
    // 3. 创建第一个Entry并存储到计算出的位置
    //    Entry继承自WeakReference,key是弱引用
    table[i] = new Entry(firstKey, firstValue);
    
    // 4. 设置当前存储的元素数量
    size = 1;
    
    // 5. 设置扩容阈值
    //    setThreshold(INITIAL_CAPACITY) 设置为容量的2/3
    //    当元素数量达到阈值时触发扩容
    setThreshold(INITIAL_CAPACITY);
}
2.2.6 Hash冲突处理机制总结

线性探测法工作流程:

flowchart TD START(["开始:set(key, value)"]) --> HASH["计算hash值
i = key.threadLocalHashCode & (len-1)"] HASH --> CHECK{"检查table[i]"} CHECK -->|"位置为空"| INSERT["直接插入
table[i] = new Entry(key, value)"] CHECK -->|"位置被占用"| GETKEY["获取当前Entry的key
k = table[i].get()"] GETKEY --> KEYCHECK{"检查key"} KEYCHECK -->|"k == key"| UPDATE["更新值
table[i].value = value"] KEYCHECK -->|"k == null"| STALE["处理过期Entry
replaceStaleEntry(key, value, i)"] KEYCHECK -->|"k != key && k != null"| NEXT["线性探测下一位置
i = nextIndex(i, len)"] NEXT --> CHECK INSERT --> CLEANUP["清理过期Entry
cleanSomeSlots(i, sz)"] UPDATE --> END(["结束"]) STALE --> END CLEANUP --> SIZECHECK{"检查是否需要扩容
sz >= threshold"} SIZECHECK -->|"需要扩容"| REHASH["扩容重hash
rehash()"] SIZECHECK -->|"不需要扩容"| END REHASH --> END classDef startEnd fill:#e1f5fe,stroke:#01579b,stroke-width:2px classDef process fill:#f3e5f5,stroke:#4a148c,stroke-width:2px classDef decision fill:#fff3e0,stroke:#e65100,stroke-width:2px classDef conflict fill:#ffebee,stroke:#c62828,stroke-width:3px class START,END startEnd class HASH,INSERT,UPDATE,STALE,CLEANUP,REHASH process class CHECK,KEYCHECK,SIZECHECK decision class GETKEY,NEXT conflict

Hash冲突处理的三种情况:

  1. 找到相同key:直接更新value值
  2. 遇到过期Entry :调用replaceStaleEntry进行清理和替换
  3. 位置被其他key占用 :使用nextIndex继续线性探测

线性探测法的优势:

  • 实现简单,无需额外的数据结构
  • 缓存友好,连续内存访问
  • 自动处理过期Entry的清理
  • 结合弱引用机制,有效防止内存泄漏

2.3 ThreadLocalMap 内部结构

graph TB subgraph "Thread对象" A["threadLocals: ThreadLocalMap"] end subgraph "ThreadLocalMap" B["Entry[] table"] B --> C["Entry[0]"] B --> D["Entry[1]"] B --> E["Entry[2]"] B --> F["..."] end subgraph "Entry结构" G["WeakReference key"] H["Object value"] end A --> B C --> G C --> H

当我们为ThreadLocal变量赋值,实际上就是以当前ThreadLocal实例为key,值为value的Entry往这个ThreadLocalMap中存放。

2.4 ThreadLocalMap的清理机制源码分析

2.4.1 ThreadLocalMap的set方法清理机制
java 复制代码
/**
 * ThreadLocalMap的set方法
 * 在设置值的同时会检查并清理过期的Entry
 */
private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    
    // 1. 计算key的hash值,确定在数组中的位置
    int i = key.threadLocalHashCode & (len-1);

    // 2. 使用线性探测法处理hash冲突
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) {
            // 3. 如果找到相同的key,直接更新value
            e.value = value;
            return;
        }

        if (k == null) {
            // 4. 关键:如果发现key为null的Entry(弱引用被回收)
            //    调用replaceStaleEntry方法替换过期Entry并清理其他过期Entry
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    // 5. 如果没有找到位置,创建新的Entry
    tab[i] = new Entry(key, value);
    int sz = ++size;
    
    // 6. 清理一些过期的Entry,如果没有清理任何Entry且达到阈值,则扩容
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}
2.4.2 ThreadLocalMap的getEntry方法清理机制
java 复制代码
/**
 * ThreadLocalMap的getEntry方法
 * 获取值的同时会检查并清理过期的Entry
 */
private Entry getEntry(ThreadLocal<?> key) {
    // 1. 计算key的hash值
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    
    // 2. 如果直接命中且key匹配,直接返回
    if (e != null && e.get() == key)
        return e;
    else
        // 3. 否则调用getEntryAfterMiss进行进一步查找和清理
        return getEntryAfterMiss(key, i, e);
}

/**
 * 在直接查找失败后的处理方法
 * 会在查找过程中清理过期的Entry
 */
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    // 4. 使用线性探测继续查找
    while (e != null) {
        ThreadLocal<?> k = e.get();
        
        if (k == key)
            // 5. 找到匹配的key,返回Entry
            return e;
            
        if (k == null)
            // 6. 关键:发现过期Entry,调用expungeStaleEntry清理
            //    这个方法会清理从当前位置开始的连续过期Entry
            expungeStaleEntry(i);
        else
            // 7. 继续向后查找
            i = nextIndex(i, len);
            
        e = tab[i];
    }
    return null;
}
2.4.3 核心清理方法expungeStaleEntry详解
java 复制代码
/**
 * 清理过期Entry的核心方法
 * 从指定位置开始清理连续的过期Entry,并重新整理后续Entry的位置
 * @param staleSlot 过期Entry的位置
 * @return 下一个null位置的索引
 */
private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // 1. 清理指定位置的过期Entry
    //    将value设为null,帮助GC回收
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // 2. 重新hash后续的Entry,直到遇到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) {
            // 3. 发现另一个过期Entry,清理它
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            // 4. 重新计算hash值,看是否需要移动位置
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                // 5. 需要移动,先清空当前位置
                tab[i] = null;

                // 6. 找到新的合适位置
                while (tab[h] != null)
                    h = nextIndex(h, len);
                    
                // 7. 将Entry移动到新位置
                tab[h] = e;
            }
        }
    }
    return i;
}
2.4.4 cleanSomeSlots启发式清理方法
java 复制代码
/**
 * 启发式地清理一些过期Entry
 * 这个方法会扫描log2(n)个位置,寻找过期Entry进行清理
 * @param i 开始扫描的位置
 * @param n 用于控制扫描范围的参数
 * @return 是否清理了任何Entry
 */
private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    
    do {
        // 1. 移动到下一个位置
        i = nextIndex(i, len);
        Entry e = tab[i];
        
        // 2. 检查当前Entry是否过期(key为null)
        if (e != null && e.get() == null) {
            // 3. 发现过期Entry,重置扫描范围并清理
            n = len;
            removed = true;
            // 4. 调用expungeStaleEntry进行彻底清理
            i = expungeStaleEntry(i);
        }
    } while ( (n >>>= 1) != 0);  // 5. 扫描log2(n)次
    
    return removed;
}
2.4.5 replaceStaleEntry替换过期Entry方法
java 复制代码
/**
 * 替换过期Entry的方法
 * 在set操作中发现过期Entry时调用,会进行更全面的清理
 */
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;

    // 1. 向前扫描,寻找更早的过期Entry
    //    这样可以在一次清理中处理更多的过期Entry
    int slotToExpunge = staleSlot;
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len)) {
        if (e.get() == null)
            slotToExpunge = i;
    }

    // 2. 向后扫描,寻找key匹配的Entry或更多过期Entry
    for (int i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        
        ThreadLocal<?> k = e.get();

        // 3. 如果找到相同的key,进行位置交换优化
        if (k == key) {
            e.value = value;
            
            // 4. 将找到的Entry与过期Entry交换位置
            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;

            // 5. 如果前面没有找到过期Entry,从当前位置开始清理
            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
                
            // 6. 执行清理操作
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }

        // 7. 记录遇到的第一个过期Entry位置
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }

    // 8. 如果没有找到相同的key,在过期位置创建新Entry
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);

    // 9. 如果发现了其他过期Entry,进行清理
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
2.4.6 remove方法的清理机制
java 复制代码
/**
 * ThreadLocalMap的remove方法
 * 移除指定key的Entry并触发清理
 */
private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    
    // 1. 计算key的hash值
    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) {
            // 3. 找到目标Entry,清除其引用
            e.clear();  // 调用WeakReference的clear方法
            
            // 4. 调用expungeStaleEntry清理这个位置及后续过期Entry
            expungeStaleEntry(i);
            return;
        }
    }
}

2.5 防止内存泄漏的完整机制总结

flowchart TD A["ThreadLocal操作"] --> B{"操作类型"} B -->|set| C["计算hash位置"] B -->|get| D["查找Entry"] B -->|remove| E["定位并删除"] C --> F{"位置是否有Entry"} F -->|有且key匹配| G["更新value"] F -->|有但key为null| H["调用replaceStaleEntry"] F -->|无| I["创建新Entry"] D --> J{"直接命中"} J -->|是| K["返回Entry"] J -->|否| L["调用getEntryAfterMiss"] E --> M["找到Entry后调用clear()"] M --> N["调用expungeStaleEntry"] H --> O["向前后扫描过期Entry"] L --> P{"遍历中发现key为null"} P -->|是| N O --> N I --> Q["调用cleanSomeSlots"] N --> R["清理当前过期Entry"] R --> S["重新hash后续Entry"] S --> T["优化存储结构"] Q --> U["启发式扫描log2(n)个位置"] U --> V{"发现过期Entry"} V -->|是| N V -->|否| W["扫描结束"] style H fill:#ffeb3b style N fill:#4caf50 style Q fill:#2196f3

2.6 核心总结

JVM内部维护了一个线程版的Map<ThreadLocal,Value> (通过ThreadLocal对象的set方法,结果把ThreadLocal对象自己当做key,放进了ThreadLocalMap中),每个线程要用到这个T的时候,用当前的线程去Map里面获取,通过这样让每个线程都拥有了自己独立的变量,人手一份,竞争条件被彻底消除,在并发模式下是绝对安全的变量。

ThreadLocal的内存泄漏防护是多层次的:

  1. 弱引用设计:Entry的key使用弱引用,允许ThreadLocal对象被GC回收
  2. 主动清理:在每次set、get、remove操作时都会检查并清理过期Entry
  3. 启发式清理:cleanSomeSlots方法会定期扫描并清理过期Entry
  4. 彻底清理:expungeStaleEntry方法会清理连续的过期Entry并优化存储结构
  5. 用户清理:提供remove方法供用户主动清理

3. ThreadLocal 的内存泄露问题

3.1 什么是内存泄露

不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露。

3.2 强引用、软引用、弱引用、虚引用

graph TB A["Java引用类型"] --> B["强引用 Strong Reference"] A --> C["软引用 Soft Reference"] A --> D["弱引用 Weak Reference"] A --> E["虚引用 Phantom Reference"] B --> B1["OOM也不回收"] C --> C1["内存不足时回收"] D --> D1["GC时就回收"] E --> E1["配合引用队列使用"]
3.2.1 强引用(Strong Reference)

当内存不足,JVM开始垃圾回收,对于强引用的对象,就算是出现了OOM也不会对该对象进行回收,死都不收。

3.2.2 软引用(Soft Reference)

对于只有软引用的对象来说,当系统内存充足时它不会被回收,当系统内存不足时它会被回收

3.2.3 弱引用(Weak Reference)

弱引用需要用java.lang.ref.WeakReference类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,都会回收该对象占用的内存。

3.2.4 虚引用(Phantom Reference)
  • 虚引用必须和引用队列(ReferenceQueue)联合使用
  • PhantomReference的get方法总是返回null
  • 主要用于处理监控通知使用

3.3 为什么要使用弱引用

sequenceDiagram participant T as Thread participant TL as ThreadLocal participant TLM as ThreadLocalMap participant E as Entry Note over T,E: 方法执行中 T->>TL: 强引用 TL->>TLM: set操作 TLM->>E: 创建Entry E->>TL: key弱引用 Note over T,E: 方法执行完毕 T--xTL: 强引用消失 Note over TL: ThreadLocal对象可被GC回收 E->>E: key变为null Note over T,E: 调用get/set/remove时 TLM->>E: 清理key为null的Entry
3.3.1 使用弱引用的原因

当function01方法执行完毕后,栈帧销毁强引用t1也就没有了。但此时线程的ThreadLocalMap里某个entry的key引用还指向这个对象:

  • 若这个key引用是强引用,就会导致key指向的ThreadLocal对象及v指向的对象不能被gc回收,造成内存泄漏
  • 若这个key引用是弱引用大概率 会减少内存泄漏的问题(还有一个key为null的雷

使用弱引用,就可以使ThreadLocal对象在方法执行完毕后顺利被回收且Entry的key引用指向为null

此后我们调用get,set或remove方法时,就会尝试删除key为null的entry,可以释放value对象所占用的内存

3.3.2 弱引用就万事大吉了吗?

答案是否定的!

当我们为threadLocal变量赋值,实际上就是当前的Entry(threadLocal实例为key,值为value)往这个threadLocalMap中存放。Entry中的key是弱引用,当threadLocal外部强引用被置为null(t1=null),那么系统GC的时候,根据可达性分析,这个threadLocal实例就没有任何一条链路能够引用到它,这个ThreadLocal势必会被回收。

这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreadLocalMap -> Entry -> value永远无法回收,造成内存泄漏。

3.4 内存泄露的解决方案

3.4.1 自动清理机制

ThreadLocal的set、get、remove方法都会检查所有键为null的Entry对象

3.4.2 最佳实践
  1. 使用ThreadLocal.withInitial(() -> 初始化值)
  2. 建议把ThreadLocal修饰成static
    • ThreadLocal能实现线程的数据隔离,不在于它自己本身,而在于Thread的ThreadLocalMap
    • 所以ThreadLocal可以只初始化一次,只分配一块存储空间就足以了,没必要作为成员变量多次被初始化
  3. 用完记得手动remove
java 复制代码
// 推荐的使用方式
public class ThreadLocalExample {
    // 1. 使用static修饰
    private static final ThreadLocal<String> threadLocal = 
        // 2. 使用withInitial初始化
        ThreadLocal.withInitial(() -> "默认值");
    
    public void doSomething() {
        try {
            // 使用ThreadLocal
            threadLocal.set("业务值");
            String value = threadLocal.get();
            // 业务逻辑...
        } finally {
            // 3. 手动清理
            threadLocal.remove();
        }
    }
}

4. 面试题解析

4.1 ThreadLocal中ThreadLocalMap的数据结构和关系?

答案:

  • ThreadLocalMap是ThreadLocal的静态内部类
  • 内部使用Entry数组存储数据,Entry继承自WeakReference<ThreadLocal<?>>
  • 每个Thread对象都有一个threadLocals字段,类型为ThreadLocalMap
  • ThreadLocalMap以ThreadLocal对象作为key,实际存储的值作为value
  • 采用开放地址法解决hash冲突

4.2 ThreadLocal的key是弱引用,这是为什么?

答案:

  • 防止内存泄露:如果key是强引用,当ThreadLocal对象的外部强引用被置为null时,由于ThreadLocalMap中的Entry还持有ThreadLocal的强引用,会导致ThreadLocal对象无法被GC回收
  • 使用弱引用后,当ThreadLocal对象的外部强引用消失时,GC可以回收ThreadLocal对象,Entry的key会变为null
  • 虽然不能完全避免内存泄露,但可以通过ThreadLocal的get/set/remove方法自动清理key为null的Entry

4.3 ThreadLocal内存泄露问题你知道吗?

答案: 内存泄露的根本原因:

  • ThreadLocalMap的Entry的key是弱引用,value是强引用
  • 当ThreadLocal对象被GC回收后,Entry的key变为null,但value仍然被Entry强引用
  • 如果线程长时间不结束(如线程池中的线程),这些key为null的Entry的value就无法被回收

解决方案:

  • ThreadLocal提供了自动清理机制,在get/set/remove时会清理key为null的Entry
  • 最重要的是手动调用remove()方法
  • 使用try-finally确保清理

4.4 ThreadLocal中最后为什么要加remove方法?

答案:

  • 防止内存泄露:手动清理ThreadLocalMap中的Entry,避免value对象无法被GC回收
  • 避免数据污染:特别是在线程池环境中,线程会被复用,如果不清理ThreadLocal,下一个任务可能会获取到上一个任务遗留的数据
  • 最佳实践:阿里巴巴开发规范强制要求使用ThreadLocal后必须手动remove

5. 总结

5.1 核心特性总结

  • ThreadLocal 并不解决线程间共享数据的问题
  • ThreadLocal 适用于变量在线程间隔离且在方法间共享的场景
  • ThreadLocal 通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题
  • 每个线程持有一个只属于自己的专属Map并维护了ThreadLocal对象与具体实例的映射
  • 该Map由于只被持有它的线程访问,故不存在线程安全以及锁的问题

5.2 内存管理总结

  • ThreadLocalMap的Entry对ThreadLocal的引用为弱引用,避免了ThreadLocal对象无法被回收的问题
  • 都会通过expungeStaleEntry、cleanSomeSlots、replaceStaleEntry这三个方法回收键为null的Entry对象的值(即为具体实例)以及Entry对象本身从而防止内存泄漏,属于安全加固的方法

5.3 使用建议

graph TB A["ThreadLocal使用建议"] --> B["声明为static final"] A --> C["使用withInitial初始化"] A --> D["及时调用remove()"] A --> E["使用try-finally保证清理"] B --> B1["避免重复创建"] C --> C1["提供默认值"] D --> D1["防止内存泄露"] D --> D2["避免数据污染"] E --> E1["确保资源释放"]

5.4 经典总结

群雄逐鹿起纷争,人各一份天下安

这句话完美诠释了ThreadLocal的核心思想:在多线程环境中,与其让多个线程争夺共享资源,不如让每个线程都拥有自己的资源副本,从而实现线程间的数据隔离,避免线程安全问题。

ThreadLocal不是用来解决共享对象的多线程访问问题的,而是为每个线程提供了变量的独立副本,从根本上隔离了多线程对数据的访问冲突。这种设计思想在很多场景下都非常有用,比如数据库连接、用户会话信息、事务上下文等。

相关推荐
TT哇17 分钟前
【Java EE初阶】计算机是如何⼯作的
java·redis·java-ee
paopaokaka_luck21 分钟前
基于SpringBoot+Vue的电影售票系统(协同过滤算法)
vue.js·spring boot·后端
IT_10246 小时前
Spring Boot项目开发实战销售管理系统——系统设计!
大数据·spring boot·后端
Fireworkitte7 小时前
Apache POI 详解 - Java 操作 Excel/Word/PPT
java·apache·excel
weixin-a153003083167 小时前
【playwright篇】教程(十七)[html元素知识]
java·前端·html
DCTANT7 小时前
【原创】国产化适配-全量迁移MySQL数据到OpenGauss数据库
java·数据库·spring boot·mysql·opengauss
ai小鬼头7 小时前
AIStarter最新版怎么卸载AI项目?一键删除操作指南(附路径设置技巧)
前端·后端·github
Touper.7 小时前
SpringBoot -- 自动配置原理
java·spring boot·后端
黄雪超8 小时前
JVM——函数式语法糖:如何使用Function、Stream来编写函数式程序?
java·开发语言·jvm
ThetaarSofVenice8 小时前
对象的finalization机制Test
java·开发语言·jvm