Java API ThreadLocal

2023年 10月 7日 57.0k 0

ThreadLocal 提供线程局部变量,使用它保存的变量在每个线程中都是独立的变量副本,ThreadLocal 通常是类中的私有静态字段,用于将状态与线程相关联。如下所示:

public static final ThreadLocal threadLocal1 = new ThreadLocal();
public static final ThreadLocal threadLocal2 = new ThreadLocal();
​
@Test
public void test04() throws InterruptedException {
    Thread thread1 = new Thread(() -> {
        threadLocal1.set(10);
        threadLocal2.set(20);
​
        System.out.println(Thread.currentThread().getName() + "threadLocal1 value is " + threadLocal1.get());
        System.out.println(Thread.currentThread().getName() + "threadLocal2 value is " + threadLocal2.get());
    });
​
    Thread thread2 = new Thread(() -> {
        threadLocal1.set(30);
        threadLocal2.set(40);
​
        System.out.println(Thread.currentThread().getName() + "threadLocal1 value is " + threadLocal1.get());
        System.out.println(Thread.currentThread().getName() + "threadLocal2 value is " + threadLocal2.get());
    });
​
​
    thread1.start();
    thread2.start();
​
    thread1.join();
    thread2.join();
​
}
Thread-0threadLocal1 value is 10
Thread-0threadLocal2 value is 20
Thread-1threadLocal1 value is 30
Thread-1threadLocal2 value is 40

在上面的例子中,不同的线程使用同一个 ThreadLocal 实例只能取到在自己线程设置的值,ThreadLocal 可以设置初始值,它提供了两种设置初始值的方法,分别是重写 protected initialValue() 方法和链式调用,示例如下:

// 重写 protected initialValue() 方法
public static final ThreadLocal threadLocal = new ThreadLocal(){
    @Override
    protected Integer initialValue() {
        return Integer.valueOf(40);
    }
};
​
// 链式调用
public static final ThreadLocal threadLocal = ThreadLocal.withInitial(() -> Integer.valueOf(40));

原理

涉及三个类 ThreadLocal、ThreadLocalMap 和 Thread,ThreadLocalMap 是 ThreadLocal 的内部类,Thread 类中定义了一个 ThreadLocalMap 成员变量,ThreadLocalMap 类中定义了一个 Entry 数组,Entry 是 ThreadLocalMap 的内部类,它是真正保存局部变量的地方,同时它也是 ThreadLocal 类型的弱引用,用户调用 ThreadLocal#get 方法发生了事情如下:

public T get() {
    Thread t = Thread.currentThread();
    // 1.获取当前 Thread 的 ThreadLocalMap 变量 threadLocals,threadLocals 是延迟初始化的
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 2.如果 threadLocals 已经初始化了,那么就通过 ThreadLocal 实例从 threadLocals 获取对应的变量
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // 2.如果 threadLocals 还没有初始化,那么初始化 threadLocals
    return setInitialValue();
}
​
private T setInitialValue() {
    // 1.如果你重写了 initialValue() 方法会返回指定的初始值,否则初始值是 null
    T value = initialValue();
    Thread t = Thread.currentThread();
    // 2.获取线程的 threadLocals,和上一个方法一样
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // 3.如果 threadLocals 已经初始化了,那么调用 threadLocals 的 set 方法设置局部变量
        map.set(this, value);
    else
        // 3.创建并初始化 ThreadLocalMap 变量
        createMap(t, value);
    return value;
}
​
void createMap(Thread t, T firstValue) {
    // 调用 ThreadLocalMap 的构造函数实例化
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
​
ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
    // 1.初始化 Entry 数组,Entry 是 ThreadLocal 中的一个静态内部类,它代表 ThreadLocal 的弱引用,同时存储局部变脸
    table = new Entry[INITIAL_CAPACITY];
    // 2.计算 key(ThreadLocal)的 hashcode
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    // 3.将 Entry 填充到数组中
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

内存泄露

在讲解内存泄露之前,需要先了解 Java 中的四种引用类型,分别是强引用(Strong Reference)、软引用(Soft Reference(Weak Reference)和虚引用(Phantom Reference)。

  • 强引用(Strong Reference):普通的对象引用,如果一个对象具有强引用,垃圾回收器绝不会回收它,即使内存不足,系统宁愿抛出 OOM 也不会回收具有强引用的对象
Object obj = new Object(); // 强引用
  • 软引用(Soft Reference):软引用用来描述一些还有用但并非必须的对象。在系统将要发生内存溢出之前,会把这些对象列进回收范围之中,进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。
SoftReference softRef = new SoftReference(new Object()); // 软引用
  • 弱引用(Weak Reference):弱引用也是用来描述非必须对象的,但是它的强度比软引用更弱,被弱引用关联的对象只能生存到下一次垃圾回收发生之前。
WeakReference weakRef = new WeakReference(new Object()); // 弱引用
  • 虚引用(Phantom Reference):虚引用是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响。如果一个对象仅持有虚引用,那么它和没有引用几乎是一样的,随时可能被垃圾回收器回收。
ReferenceQueue referenceQueue = new ReferenceQueue();
PhantomReference phantomRef = new PhantomReference(new Object(), referenceQueue); // 虚引用

而 ThreadLocalMap 中的静态内部类 Entry 就是 ThreadLocal 的弱引用,我们看下面的例子

@Test
public void test02() throws InterruptedException {
​
    Object obj = new Object();
    WeakReference weakReference = new WeakReference(obj);
    Object o = weakReference.get();
    System.out.println(o.equals(obj));
    obj = null;
    o = null;
​
    System.gc();
    System.out.println(weakReference);
    System.out.println(weakReference.get());
​
}
true
java.lang.ref.WeakReference@61f8bee4
null

在 test02() 测试方法中,给 obj 对象创建了一个弱引用 weakReference,通过调用 WeakReference#get 可以得到引用对象,然后切断 obj 的强引用(obj = null; o = null;),触发 Full GC,再次调用 WeakReference#get 方法可以发现引用对象为 null,说明引用对象已经被回收了,但是 weakReference 是一个独立的对象,它有一个强引用关系,所以它不会被回收。

再来看一下例子

public class MyThread extends Thread {
​
    public static ThreadLocal threadLocal1 = new ThreadLocal();
    public static ThreadLocal threadLocal2 = new ThreadLocal();
    public static ThreadLocal threadLocal3 = new ThreadLocal();
    public static ThreadLocal threadLocal4 = new ThreadLocal();
    public static ThreadLocal threadLocal5 = new ThreadLocal();
    public static ThreadLocal threadLocal6 = new ThreadLocal();
    public static ThreadLocal threadLocal7 = new ThreadLocal();
    public static ThreadLocal threadLocal8 = new ThreadLocal();
    public static ThreadLocal threadLocal9 = new ThreadLocal();
    public static ThreadLocal threadLocal10 = new ThreadLocal();
​
    @Override
    public void run() {
        System.out.println("begin set threadlocal...");
        threadLocal1.set(1);
        threadLocal2.set(2);
        threadLocal3.set(3);
        threadLocal4.set(4);
        threadLocal5.set(5);
        threadLocal6.set(6);
        threadLocal7.set(7);
        threadLocal8.set(8);
        threadLocal9.set(9);
        threadLocal10.set(10);
        // 1
        System.out.println("end set threadlocal...");
​
        threadLocal1 = null;
        threadLocal2 = null;
        threadLocal3 = null;
        threadLocal4 = null;
        threadLocal5 = null;
        threadLocal6 = null;
        threadLocal7 = null;
        threadLocal8 = null;
        threadLocal9 = null;
        threadLocal10 = null;
​
        System.gc();
​
        // 2
        Scanner in = new Scanner(System.in);
        in.nextInt();
    }
}
​
@Test
public void test01() throws InterruptedException {
​
    Thread thread = new MyThread();
    thread.start();
    thread.join();
​
}

在 1 和 2 处打断点,当程序执行到断点 1 的时候,可以发现 threadLocals 中的 Entry 数组有 10 个元素,Entry 的引用对象是 ThreadLocal 实例。

image.png
当程序执行到断点 2 的时候,threadLocals 中的 Entry 数组仍然有 10 个元素,但是其中每个 Entry 的引用对象(即 ThreadLocal 实例)已经变成 null 了,因为在引用对象只有一个弱引用的情况下,GC 线程会回收掉弱引用对象。

既然 ThreadLocal 都被回收了,那么它保存的局部变量就不会再被使用,而局部变量实际存储在 Entry 对象中,而 ThreadLocalMap 中的 Entry 数组有到 Entry 的强引用,所以 Entry 不会被回收,由此内存泄露就产生了。由于 Thread 有到 ThreadLocalMap 的强引用,所以只要 Thread 不被回收,那么 Entry 就不会回收。

在上面的例子中只有 10 个 ThreadLocal,如果有非常多的 ThreadLocal,或者 ThreadLocal 存储的对象非常大的话,那么浪费的内存空间还是很可观的。而且如今的应用程序都使用线程池来管理线程,线程池中的线程可能有用不会被回收,那么在线程执行了很多任务之后可能会残留很多的 Entry 在 ThreadLocalMap 中。所以建议在使用了之后手动调用 ThreadLocal#remove 方法来删除线程局部变量。

由于 Entry 就是 ThreadLocal 的弱引用,所以我们在将 ThreadLocal 置为 null 之后,ThreadLocal 只有 Entry 这个弱引用,在下次 gc 的时候 ThreadLocal 会被回收,但是 Entry 不会,它依旧保存着之前哪个 ThreadLocal 的变量副本,这个变量副本不会在被使用,但是它也没有被释放掉,所以就造成了内存泄露。

使用 VisualVM 找到内存泄露

测试代码如下:

/**
 *
 * -Xms50m -Xmx50m
 **/
public class ThreadLocalMemoryLeak extends Thread {
​
    public static ThreadLocal threadLocal = new ThreadLocal();
​
    @Override
    public void run() {
        // 线程局部变量占用20M
​
        byte[] placeholder = new byte[20 * 1024 * 1024];
        threadLocal.set(placeholder);
        placeholder = null;
​
        System.gc();
​
        Scanner in = new Scanner(System.in);
        in.nextInt();
    }
}
​
@Test
public void test03() throws InterruptedException {
​
    Thread thread = new ThreadLocalMemoryLeak();
    thread.start();
    thread.join();
​
}

在上面的例子中,指定了 jvm 参数 -Xms50m -Xmx50m 设置堆内存大小为 50m,先用 ThreadLocal 实例保存 20m 的字节数组,然后切断字节数组的强引用,触发 gc,最后程序会停止等待输入。

VisualVM 是 JDK 自带的性能监控和故障处理工具,双击 %JAVA_HOME%binjvisualvm.exe 启动 VisualVM,然后选择 Junit 程序。

image.png

在 “监视” 页签点击 “堆 Dump” 按钮生成 HeapDump 文件,VisualVM 会自动打开这个文件。然后在 heapdump 页面的 “类” 页签,选择按 “大小” 从大到小的顺序排列类,发现 byte[] 类占了 90% 多的内存空间。

image.png

选中它右击,选择 “在实例视图中显示”

image.png

会有很多 byte[] 实例,选择最大的那个,右击之后在弹出菜单中选择 “显示最近的垃圾回收根节点”,查找这个 byte[] 实例的引用路径。

image.png

发现它被 Thread 对象中的 threadLocals 变量引用。

image.png

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论