Java并发编程(2)


ThreadLocal

1、ThreadLocal是什么

  ThreadLocal就是线程本地变量,若创建了一个ThreadLocal变量,那访问这个变量的每个线程都会有这个变量的本地拷贝,但多个线程操作这个变量时,实际是操作自己本地内存里的变量,可以起到线程隔离的作用,避免了线程安全问题。

 

//创建一个ThreadLocal变量localVariable
/ /创建⼀个ThreadLocal变 量
public static ThreadLocal < String > localVariable = new ThreadLocal < > ();

//写入:线程可以在任何地方使用localVariable
localVariable.set("xxxx");

//读取:线程在任何地方读取的都是它写入的变量
localVariable.get();        //xxxx

2、你在工作中用到过ThreadLocal吗

   用到过,比如在登陆的时候,用户每次访问接口在请求头都会携带一个token,在控制层可以根据这个token,解析出用户的基本信息。由于在后面的服务层、持久层都会用到责怪用户信息,这时候就可以用到ThreadLocal,在控制层拦截请求把用户信息存入ThreadLocal,这样在其他任何地方都可以取出T和read Local中存的用户数据。

  很多其他场景如cookie、session、数据库连接池都可以用ThreadLocal。

3、ThreadLocal怎么实现的

   每个Thread对象里,有一个成员变量ThreadLocal.ThreadLocalMap threadLocals = null; 说明每个线程都有一个属于自己的ThreadLocalMap。当调用threadLocal.set(value) 时,会发生:

  • 先获取当前线程 Thread t = Thread.currentThread();
  • 再拿到该线程ThreadLocalMap
  • 把数据存进去,形式是 <key, value>

  那这里的key和value是什么?value就是set进去的对象。key不是ThreadLocal本身,而是ThreadLocal 的一个 弱引用

  那为什么是弱引用呢?假如key是强引用,若某个ThreadLocal 对象没有外部引用了(ThreadLocal = null),但ThreadLocalMap还持有它,那它就永远不会被GC,造成内存泄露。用了弱引用之后,一旦外部不再持有ThreadLocal,GC就会把它回收。ThreadLocalMap中的key会变成null,只剩下value。JVM之后会清理这些key为null的Entry,避免泄露。

4、ThreadLocal内存泄露是怎么回事

  •  key是弱引用:
    • 外部不再引用ThreadLocal 对象,GC 会回收它。ThreadLocalMap里的entry变成<null, value>,value还在,但程序员无法通过ThreadLocal拿到这份数据。若线程是线程池里的长生命周期线程,这块value会一直留在内存,直到线程结束才可能释放-->内存泄露
  • key是强引用:
    • 即使外部不再引用 ThreadLocal,它也不会被 GC,因为 map 还持有强引用。
    • 弱引用可以减轻泄露风险。
  • 如何避免内存泄露(最佳实践)
try {
    local.set(new User("Alice"));
    // 业务逻辑
} finally {
    local.remove(); // ✅ 主动清理,避免泄漏
}

5、ThreadLocalMap的结构了解吗

   ThreadLocalMap是一个定制化的Map,存放在Thread对象里,每个Thread维护一个自己的ThreadLocalMap,里面的key就是弱引用ThreadLocal。它没有实现Map接口(是内部类,只服务于ThreadLocal),主要是一个Entry[] table数组(每个 Entry 保存 <ThreadLocal弱引用, value>)。

  每次创建新的ThreadLocal对象,都会分配一个threadLocalHashCode值。这个值不是简单的1,2,3...自增,而是每次递增一个特殊的常数0x61c88647。这个数来自黄金分割数(√5 - 1) / 2 ≈ 0.618...。这样可以让哈希值分布更均匀,避免冲突集中。

6、ThreadLocalMap怎么结局hash冲突的

   ThreadLocalMap使用开放定址法,这个坑被人占了就去接着找空着的坑。若插入一个value,通过hash计算后应该落入某个槽位,但这个坑已经被占了,且Entry数据的key和当前不相等,此时会线性向后查找,一直找到为null的槽位才会停止。

  get的时候,也会根据ThreadLocal对象的hash值定位到table中的位置,然后判断该槽位Entry对象中的key是否和get的key一致,若不一致,就判断下一个位置。

7、ThreadLocal扩容机制了解吗

   在ThreadLocalMap.set() 里,若存入元素时发现表里的Entry数量达到阈值(len*2/3),就会触发rehash()。

  • 清理掉已经失效(key=null)的Entry
  • 如果清理后size依然>=3/4 * threshold,就触发resize()扩容。
private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    int newLen = oldLen * 2;   // 新数组长度翻倍
    Entry[] newTab = new Entry[newLen];

    for (int j = 0; j < oldLen; ++j) {
        Entry e = oldTab[j];
        if (e != null) {
            ThreadLocal<?> k = e.get();
            if (k == null) {
                e.value = null;  // key 已被回收,帮助 GC
            } else {
                // 重新计算哈希位置
                int h = k.threadLocalHashCode & (newLen - 1);
                while (newTab[h] != null) {  // 开放地址法,找下一个空位
                    h = nextIndex(h, newLen);
                }
                newTab[h] = e; // 放到新数组
            }
        }
    }

    table = newTab; // 指向新数组
}
  • 新数组翻倍:N->2N,降低负载因子
  • 遍历老数组:把旧数组里的Entrty一个个搬到新数组。若key已经被GC,就清理掉value
  • 重新计算位置:用新数组长度newLen重新取模
  • 冲突处理:若目标格子被占,就调用nextIndex()往后找下一个空位(开放地址法)
  • 更新引用:搬运完毕后,把table指向newTab。

8、父子线程怎么共享数据

  •  普通ThreadLocal不能传递给子线程,因为ThreadLocal的值存放在当前对象的ThreadLocals变量里,就算是父线程,也不算是同一个线程。
  • 解决办法:在Thread类里除了threadLocals之外,还有一个:ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; 关键点在于子线程初始化时,从父线程的InheritableThreadLocalMap拷贝了一份数据。
public class InheritableThreadLocalTest {
    public static void main(String[] args) {
        ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();

        // 父线程设置值
        threadLocal.set("父线程的值");

        // 子线程
        new Thread(() -> {
            System.out.println("子线程获取:" + threadLocal.get());
        }).start();
    }
}

//子线程获取:父线程的值
  • 限制:只是在创建子线程那一刻复制,后续修改不同步。
  • 线程池问题:线程池里的线程是复用的,子线程不会每次都重新init(),所以默认的InheritableThreadLocal在线程池场景可能会出问题。为解决这个,阿里开源了TransmittableThreadLocal (TTL),专门用于线程池下传递上下文。

 

参考

[1] 沉默王二公众号

posted @ 2025-09-14 19:23  筱倩  阅读(57)  评论(0)    收藏  举报