多线程编程 —— ThreadLocal

 2023-01-30
原文作者:蒋先森 原文地址:https://jlj98.top/

今天在重新看阿里Java手册的时候,看到了ThreadLocal,就想对ThreadLocal进一步了解下。

202212301148212251.png

在讲ThreadLocal之前,先去了解了下SimpleDateFormat为什么不是线程安全的。先来看下SimpleDateFormat的部分源码,这个在网上应该也有讲解。

202212301148223952.png

202212301148232303.png

202212301148239704.png

可以看这个 **原因**,讲解的挺详细的。

ThreadLocal

ThreadLocal为变量在每个线程中都创建了一个副本,所以每个线程可以访问自己内部的副本变量,不同线程之间不会互相干扰。Java中的ThreadLocal类允许我们创建只能被同一个线程读写的变量。因此,如果一段代码含有一个ThreadLocal变量的引用,即使两个线程同时执行这段代码,它们也无法访问到对方的ThreadLocal变量。

202212301148247355.png

实现原理

每一个线程持有一个ThreadLocalMap。

202212301148256006.png

一个Thread中只有一个 ThreadLocalMap,一个 ThreadLocalMap 中可以有多个 ThreadLocal 对象,其中一个 ThreadLocal 对象对应一个ThreadLocalMap中的一个Entry(也就是说:一个Thread可以依附有多个ThreadLocal对象)。

ThreadLocalMap 和 WeakReference

    static class ThreadLocalMap {
            static class Entry extends WeakReference<ThreadLocal<?>> {
                Object value;
                Entry(ThreadLocal<?> k, Object v) {
                    super(k);
                    value = v;
                }
            }
    private static final int INITIAL_CAPACITY = 16;//默认初始化16容量
    }

ThreadLocalMap 从字面上可以看出是保存 ThreadLocal 对象的 map(其实是以 ThreadLocal 为 key),不过是经过两层包装:

  • 第一次使用 WeakReference<ThreadLocal<?>> 将 ThreadLocal 对象变成一个弱引用对象。
  • 第二层是定义一个专门的类 Entry 来扩展WeakReference<ThreadLocal<?>>

类 Entry 保存 map 键值对的实体,ThreadLocal<?> 为 key,保存的线程局部变量值为 value。super(k) 调用的是 WeakReference 的构造函数,表示将 ThreadLocal<?> 变为弱引用。

TreadLocal 构造函数

202212301148267927.png

TreadLocal 的 set 方法

202212301148276038.png

table扩容

如果table中的元素数量达到阈值threshold的3/4,会进行扩容操作

    private void resize() {
        Entry[] oldTab = table;
        int oldLen = oldTab.length;
        int newLen = oldLen * 2; //旧的大小的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) { //如果key为null,回收value
                    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;
    }

ThreadLocal 内存回收

ThreadLocal 涉及到两个层面的内存回收。

ThreadLocal 层面的内存回收

当线程死亡,所有的保存的线程局部变量就会被回收,其实这里只线程 Thread 对象中的 ThreadLocal.ThreadLocalMap threadLocals 会被回收。

ThreadLocalMap 层面的内存回收

当线程存活的时间够长,并且该线程保存的线程局部变量很多,就需要在线程的生命期内进行 ThreadLocalMap 的内存回收。

Entry 对象的key 是WeakReference 的包装,当 ThreadLocalMap 的 private Entry[] table,已经被占用达到 2/3 (线程拥有的局部变量超过10个)时,就会尝试回收。在 ThreadLocalMap.set 方法中有回收的代码:

    if (!cleanSomeSlots(i, sz) && sz >= threshold)
                    rehash();

cleanSomeSlots 具体回收代码:

202212301148284129.png

ThreadLocal 可能引起的 OOM 问题

在一个线程结束的时候,Thread 会调用 exit 方法进行回收。

2022123011482935410.png

但是,当我们使用线程池的时候,这意味着当前线程未必会退出。这可能使得一些大的对象设置到 ThreadLocal 中,导致出现 OOM。比如当线程是设置固定值,第一次处理业务时,向 ThreadLocalMap 中存放了一个很大的对象,第二次,第三次。。。,线程一直在运行,这会导致这个线程的出现 OOM。

ThreadLocalMap的key为弱引用

关于弱引用、强引用这些,可以看深入理解虚拟机——垃圾收集器,这里面稍微讲解了下这方面。

ThreadLocalMap 会在下一次GC的时候,回收掉 key,而ThreadLocal 在下一次调用 get、set 和 remove,会清除并重构 ThreadLocalMap,其方法是 expungeStaleEntry

Reference