ThreadLocal笔记

ThreadLocal 笔记

image

  • 上图中的threadLocal只有一个对象实例,应该是设置了static

QA

ThreadLocal解决了什么问题,如何解决的

  • 多线程并发场景下数据安全问题,一般说来,并发情况下,需要用锁保证数据修改的准确性,效率低,难度大
  • ThreadLocal将变量副本封闭到自己的内存空间,对其他线程不可见

ThreadLocal是每个线程都会保留一个变量副本,为什么还会造成冲突呢

  • threadLocal的hashCode在threadLocal对象创建时就已经计算好了,hashCode & (len - 1) 本身就会造成冲突
  • 也可能是用法不规范,线程内部对同一个threadLocal疯狂set,也不调用remove

ThreadLocalMap是什么

  • 是存放ThreadLocal对象的Map容器,实际存放单元为Entry[]
  • 类关系为:ThreadLocalThreadLocalMapEntry,Thread#ThreadLocal.ThreadLocalMap

ThreadLocalMap冲突时采用的寻址方式

  • 开放寻址:同一个下标位置发生冲突时,则+1向后寻址,直到找到空位置或过期位置进行存储

ThreadLocalMap使用的hash算法

  • Fibonacci散列:nextHashCode.getAndAdd(HASH_INCREMENT)
    • HASH_INCREMENT = 0x61c88647,0x61c88647 = 2 ^ 32 * 0.6180339887
    • Fibonacci散列公式:f(k)=(k2654435769)>>28(TODO:待补充)

InheritableThreadLocal是什么

  • InheritableThreadLocal扩展了 ThreadLocal,为子线程提供从父线程那里继承的值:在创建子线程时,子线程会接收所有可继承的线程局部变量的初始值,以获得父线程所具有的值。
  • 当必须将变量(如用户 ID 和 事务 ID)中维护的每线程属性(per-thread-attribute)自动传送给创建的所有子线程时,应尽可能地采用可继承的线程局部变量,而不是采用普通的线程局部变量。
  • InheritableThreadLocal 重写了 ThreadLocal 中的 childValue, createMap, getMap 三个方法

为何会内存泄漏

image

  • threadLocal变量使用结束后,threadLocalRef -> threadLocalObj 强引用消失
  • 没有调用remove(),threadLocalMapObj -> entryObj -> valueObj 强引用仍然存在
  • Thread仍然存活,threadRef -> threadObj -> threadLocalMapObj 强引用仍然存在
  • GC发生后,threadLocalMap中的对应entry会变为[null, value],此时value无法被访问到,也没有被回收

为什么要使用WeakReference,强引用不行吗

使用强引用

  • threadLocal变量使用结束后,threadLocalRef -> threadLocalObj 强引用消失
  • threadRef -> threadObj -> threadLocalMapObj -> entryObj -> threadLocalObj 仍然存在,且都为强引用
  • 直接造成内存泄漏(threadLocalObj + valueObj),比弱引用更糟(只有valueObj)
  • 后续使用中无法判断该entry是否过期,也就无法回收了

使用弱引用

  • 内存泄漏(valueObj)
  • 后续使用中可以回收泄漏的资源
    • get(): getEntry未命中,向后查找,遇到过期entry触发删除-expungeStaleEntry
    • set():cleanSomeSlots必定触发,rehash可能触发
    • remove():将当前entry标记为过期,使用expungeStaleEntry处理

已泄漏的内存还有机会被清理吗

有机会被清理,分别是

  • 探测式清理-expungeStaleEntry(int staleSlot)
    • get()触发,如果对应下标位不是目标key,则向后查找,遇到过期的entry调用expungeStaleEntry进行删除
    • remove()触发,将当前entry标记为删除,用expungeStaleEntry处理(代码复用,很巧妙)
    • 从staleSlot位置开始向后查找,如果遇到过期entry则删除,未过期entry进行搬移,遇到entry==null停止处理,null说明后续位置顺序没问题,不用搬移
    • ThreadLocalMap的开放链式寻址决定了如果删除元素必须将错位entry复位,因此不得不向后处理
  • 启发式清理-cleanSomeSlots(int i, int n)
    • set()触发
      1. setValue结束后主动调用
      2. set到一个过期entry时调用replaceStaleEntry(key, value, i),内部会主动调用
    • 试探的扫描一些slots,寻找过期元素,扫描次数为 log(n),找到则调用expungeStaleEntry

为什么建议将ThreadLocal变量设为static

  • 确保threadLocalRef -> threadLocalObj始终为强引用,且threadLocalRef作为GCRoot不会被回收,始终可以获取到value并调用remove()
  • 确保 threadLocalObj 只创建一个实例,所有的entry共享该对象,减少内存占用

扩容何时触发

  • set()时,如果cleanSomeSlots清理之后的size >= threshold,会触发rehash,threshold = len * 2 / 3
  • rehash会清理所有过期entry,如果清理后size >= threadhold - threadhold / 4,会触发resize,扩容为2倍大小

扩容是如何操作的

  1. 首先把数组长度扩容到原来的2倍,oldLen * 2,实例化新数组。
  2. 遍历for,所有的旧数组中的元素,重新放到新数组中。
  3. 在放置数组的过程中,如果发生哈希碰撞,则链式法顺延。
  4. 如果entry已过期,释放entryObj->valueObj强引用(e.value = null)

Netty中的FastThreadLocal是什么

  • Netty为了提高ThreadLocal的速度,进行了优化,主要思想是空间换时间
  • JDK的ThreadLocal可能会冲突导致线性向后查找,FastThreadLocal直接使用下标位置,避免了冲突
  • 代价是需要预先分配一个很大的数组

TerminatingThreadLocal是什么(答案来自GPT)

  • TerminatingThreadLocal 是一个 Java 类,用于处理线程本地变量(ThreadLocal)的清理工作。它的作用是在线程结束时自动清理 ThreadLocal 变量,防止内存泄漏。这对于长时间运行的应用程序中使用 ThreadLocal 变量的情况尤为重要。
  • 例如,有一个使用 ThreadLocal 变量来存储数据库连接的类。如果不及时清理,这些线程本地的数据库连接引用可能会导致内存泄漏。通过使用 TerminatingThreadLocal,可以确保在线程结束时清理这些数据库连接,从而避免潜在的内存泄漏问题。
  • 这个类位于 jdk.internal.misc 包下

源码注释中常提到一个词run,它是什么含义

image

  • run是Entry[]中被两个null夹在中间的子序列
  • 探测式清理expungeStaleEntry的作用范围是run的子序列:[run的某个过期entry,run的结束]
  • 因为开放链式寻址,expungeStaleEntry删除过期entry后,必须要后续错位的entry进行搬移,遇到null结束

源码解析

初始化

传统的new ThreadLocal<>();

// 构造方法什么都没有做
// 但有部分属性直接被赋值 
public ThreadLocal() {
}

// ThreadLocal的hashCode在创建对象时设置,用于计算Entry[]下标
private final int threadLocalHashCode = nextHashCode();

private static AtomicInteger nextHashCode = new AtomicInteger();

// 固定值2 ^ 32 * 0.6180339887(黄金分割点)
private static final int HASH_INCREMENT = 0x61c88647;

// Fibonacci散列函数
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

1.8新增的withInitial

使用示例

private static final ThreadLocal<SimpleDateFormat> simpledateformatPool = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd"));
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
    return new SuppliedThreadLocal<>(supplier);
}
// SuppliedThreadLocal定义
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
    private final Supplier<? extends T> supplier;
    SuppliedThreadLocal(Supplier<? extends T> supplier) {
        this.supplier = Objects.requireNonNull(supplier);
    }
    // ThreadLocal#get()时,如果getMap(t) == null,会调用setInitialValue(),其中会使用initialValue()的结果
    @Override
    protected T initialValue() {
        return supplier.get();
    }
}

set()

set可能遇到以下几种情况

  1. 对应slot为空,直接设值即可
  2. 对应slot不为空,但key相同,说明是更新操作,直接更新
  3. 对应slot不为空,key不相同(不为空),说明遇到了hash冲突,继续向后查找
  4. 对应slot不为空,且key为空,说明该slot处的key已经被GC清理,对应entry已经过期,此时进行替换操作
    1. 将valueObj设置到slot位置
    2. 如果slot所在的“run”中有其他过期entry,则清理整个“run”
    3. 启发式清理
  5. 启发式清理(log(n))
  6. 清理之后如果 size >= threshold,触发rehash
    • rehash会清理所有过期的entry,如果清理后size >= threshold - threshold / 4 则触发扩容
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // map如果设置过,直接设置值
        map.set(this, value);
    } else {
        // 第一次set时创建threadLocalMap对象
        createMap(t, value);
    }
}

// 获取线程的threadLocalMap
// inheritableThreadLocal返回的是t.inheritableThreadLocals;
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

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

    // set() 不像 get() 那样使用快速路径,因为使用 set() 创建新entry至少与替换现有entry一样常见,发生替换时,快速路径通常会失败 。

    Entry[] tab = table;
    int len = tab.length;

    // 直接取模,区别于HashMap的扰动
    int i = key.threadLocalHashCode & (len-1);

    // 如果该位置e==null,直接设置值(不走循环)
    for (Entry e = tab[i];
            e != null;
            // case3 key不同且未过期,继续向后寻找(拉链法寻址)
            e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();
        if (k == key) {
            // case2 key相同,覆盖原来的值
            e.value = value;
            return;
        }

        if (k == null) {
            // case4 key已过期,替换过期的entry,涉及到清理,放到清理那块吧
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    // case1 空位置直接插入
    tab[i] = new Entry(key, value);
    int sz = ++size;
    // 清理
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

// 找下个索引,类似循环数组
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);
}


get()

  • 如果slot处的key==目标值,说明直接命中,返回对应的valueObj即可
  • 如果未命中则继续向后找,顺便清理过期的entry
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;
        }
    }
    // 使用ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd"));会触发
    return setInitialValue();
}
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)
            // 找到对应entry
            return e;
        if (k == null)
            // entry已过期(key被gc回收),删除过期entry
            expungeStaleEntry(i);
        else
            // 向后寻找
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

remove()

remove 并不是简单将位置 i 释放,而是先释放软引用(将当前entry标记为过期),然后调用expungeStaleEntry处理

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null) {
        m.remove(this);
    }
}
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) {
            // 释放threadLocal对象,标记为已过期
            e.clear();
            // 处理当前entry
            expungeStaleEntry(i);
            return;
        }
    }
}

扩容

private void rehash() {
    // 删除所有过期entry
    expungeStaleEntries();

    // Use lower threshold for doubling to avoid hysteresis
    if (size >= threshold - threshold / 4)
        // 清理之后的 size >= threshold * 0.75,执行扩容
        resize();
}
// 对每个过期的entry位置都调用expungeStaleEntry
private void expungeStaleEntries() {
    Entry[] tab = table;
    int len = tab.length;
    for (int j = 0; j < len; j++) {
        Entry e = tab[j];
        if (e != null && e.get() == null)
            expungeStaleEntry(j);
    }
}

private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    int newLen = oldLen * 2;
    Entry[] newTab = new Entry[newLen];
    int count = 0;

    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 = count;
    table = newTab;
}
// 设置阈值为长度的 2/3
private void setThreshold(int len) {
    threshold = len * 2 / 3;
}

清理

  • set操作时,如果遇到已过期的entry,会调用replaceStaleEntry,该方法会清理部分过期的entry
/**
 * set()时如果遇到过期entry,将value设置到该位置(即使有其他entry也是这个key)
 * 方法副作用:删除staleSlot所在“run”的所有过期entry
 * “run” 是两个空slot(entry==null)之间的子序列
 */
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
        int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;

    // 往前找,找到"run"中第一个过期的slot
    // 本方法会一次性检查并清理整个"run",避免GC清理大量过期slot导致的频繁rehash
    int slotToExpunge = staleSlot;
    for (int i = prevIndex(staleSlot, len);
            (e = tab[i]) != null;
            i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;

    // 找到key所在的entry,或者"run"中下一个null位置(不管是不是第一个)
    for (int i = nextIndex(staleSlot, len);
            (e = tab[i]) != null;
            i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();

        // 如果找到key,需要将key所在entry和过期entry互换位置,以保证hash table的顺序
        // 对于新检测到的过期entry,可以随后使用expungeStaleEntry删除
        if (k == key) {
            e.value = value;

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

            // 如果slotToExpunge("run"的第一个过期slot,清理起始位置)==staleSlot,因为已经将 i 和 staleSlot 位置互换,所以从 i 清理就可以了
            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            // 清理"run",顺便再随机清理下,然后返回
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }

        // staleSlot之前没有其他的过期slot(仍从staleSlot开始清理),当前i位置的slot已经过期,改为从i位置开始清理
        // 因为staleSlot位置要设置valueObj,所以就不需要被清理了
        // 如果staleSlot之前仍有过期的slot,就无需操作,因为清理会将本"run"的过期slot都处理掉(包括当前的过期位置i)
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }

    // 找不到key,将过期entry的value置为null,并在该slot放一个新entry
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);

    // 如果"run"中存在其他过期entry,直接干掉,顺便启发式清理一波
    // !=说明只有staleSlot过期了,但它已经设置了新entry,所以就不用再清理了
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

/**
 * 删除过期entry
 * 参数 staleSlot 为过期entry的下标
 * 返回 下一个可用位置i(i>staleSlot)
 * 简单删除是不够的,还需要冲突的链式entry前移,因此需要继续向后查找
 * 直到遇到tab[i]==null停止(也就是一个完整的"run",staleSlot会被标记为过期,也就是作为"run"的开始),null说明后续位置正常
 * [staleSlot,i]之间的数据都需要修复:删除过期entry,搬移正常entry
 */
private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // 删除staleSlot位的valueObj强引用
    tab[staleSlot].value = null;
    // 删除staleSlot位
    tab[staleSlot] = null;
    size--;

    // 继续向后处理entry,直到遇到null,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) {
            // 删除过期entry
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            // 尚未过期,向前搬移
            // 原始下标位h
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                // e应该在h处,现在位于i,需要搬移,将i位置空出来
                tab[i] = null;

                // 从原始位置开始,向后找到第一个空位,放置e
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}

/**
 * 参数 i:尚未过期的entrySlot,清理将从i+1处开始
 * 参数 n: 限制执行次数为 log(n)
 * 返回: 是否有元素删除
 * 抽取部分元素检查是否过期,如果已过期则删除
 * 添加新元素,或者删除过期元素时调用
 * 不限制 log(n) 的话只能逐元素检查,复杂度为 O(n),无法接受
 * 完全不检查的话,垃圾就太多了
 * 因此采用折中方式 log(n)
 */
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) {
            // e已过期
            n = len;
            removed = true;
            i = expungeStaleEntry(i);
        }
    } while ( (n >>>= 1) != 0);
    return removed;
}

典型场景

  • SimpleDateFormat
  • 链路追踪
  • Http请求
  • 连接池信息

使用示例

@Slf4j
public class DateFormatter {
    private static final ThreadLocal<SimpleDateFormat> simpledateformatPool = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd"));
    public static String formatSimple(Date date) {
        if (date == null) {
            return "";
        }
        String result = "";
        try {
            result = simpledateformatPool.get().format(date);
        } catch (Exception e) {
            logger.error("happen error when format date : {}", date);
        }
        return result;
    }
}

参考

posted @   郑恒冰  阅读(23)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· DeepSeek在M芯片Mac上本地化部署
点击右上角即可分享
微信分享提示