Java API -- ThreadLocal

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

java 复制代码
public static final ThreadLocal<Integer> threadLocal1 = new ThreadLocal<>();
public static final ThreadLocal<Integer> 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();
​
}
bash 复制代码
Thread-0threadLocal1 value is 10
Thread-0threadLocal2 value is 20
Thread-1threadLocal1 value is 30
Thread-1threadLocal2 value is 40

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

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

原理

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

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

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

java 复制代码
@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());
​
}
bash 复制代码
true
java.lang.ref.WeakReference@61f8bee4
null

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

再来看一下例子

java 复制代码
public class MyThread extends Thread {
​
    public static ThreadLocal<Integer> threadLocal1 = new ThreadLocal<>();
    public static ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>();
    public static ThreadLocal<Integer> threadLocal3 = new ThreadLocal<>();
    public static ThreadLocal<Integer> threadLocal4 = new ThreadLocal<>();
    public static ThreadLocal<Integer> threadLocal5 = new ThreadLocal<>();
    public static ThreadLocal<Integer> threadLocal6 = new ThreadLocal<>();
    public static ThreadLocal<Integer> threadLocal7 = new ThreadLocal<>();
    public static ThreadLocal<Integer> threadLocal8 = new ThreadLocal<>();
    public static ThreadLocal<Integer> threadLocal9 = new ThreadLocal<>();
    public static ThreadLocal<Integer> 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 实例。

当程序执行到断点 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 找到内存泄露

测试代码如下:

csharp 复制代码
/**
 *
 * -Xms50m -Xmx50m
 **/
public class ThreadLocalMemoryLeak extends Thread {
​
    public static ThreadLocal<byte[]> 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%\bin\jvisualvm.exe 启动 VisualVM,然后选择 Junit 程序。

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

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

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

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

相关推荐
cooldream200910 分钟前
SpringMVC 执行流程详解
java·spring·springmvc
redemption_212 分钟前
SpringMVC-01-回顾MVC
java
techdashen14 分钟前
Go context.Context
开发语言·后端·golang
凡人的AI工具箱16 分钟前
40分钟学 Go 语言高并发:Select多路复用
开发语言·后端·架构·golang
redemption_217 分钟前
Spring-02-springmvc
java
GGBondlctrl19 分钟前
【Spring MVC】初步了解Spring MVC的基本概念与如何与浏览器建立连接
java·spring boot·spring mvc·requestmapping·restcontroller
ModelBulider22 分钟前
SpringMVC应用专栏介绍
java·开发语言·后端·spring·springmvc
恬淡虚无真气从之24 分钟前
go 结构体方法
开发语言·后端·golang
努力的Java程序员25 分钟前
后端接受大写参数(亲测能用)
java·开发语言
唐僧洗头爱飘柔95271 小时前
(Java并发编程——JUC)常见的设计模式概念分析与多把锁使用场景!!理解线程状态转换条件!带你深入JUC!!文章全程笔记干货!!
java·设计模式·并发编程·juc·reentrantlock·顺序控制·生产者与消费者