颓哀 发表于 2025-9-14 19:06:12

Java并发编程(2)

ThreadLocal

1、ThreadLocal是什么

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

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

//读取:线程在任何地方读取的都是它写入的变量
localVariable.get();      //xxxx2、你在工作中用到过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是什么?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变成,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对象,都会分配一个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;

    for (int j = 0; j < oldLen; ++j) {
      Entry e = oldTab;
      if (e != null) {
            ThreadLocal<?> k = e.get();
            if (k == null) {
                e.value = null;// key 已被回收,帮助 GC
            } else {
                // 重新计算哈希位置
                int h = k.threadLocalHashCode & (newLen - 1);
                while (newTab != null) {// 开放地址法,找下一个空位
                  h = nextIndex(h, newLen);
                }
                newTab = 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();
    }
}<br><br>//子线程获取:父线程的值

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

沉默王二公众号

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

喳谍 发表于 5 天前

懂技术并乐意极积无私分享的人越来越少。珍惜

褥师此 发表于 18 小时前

用心讨论,共获提升!
页: [1]
查看完整版本: Java并发编程(2)