1. ThreadLocal定义
ThreadLocal 是 Java 中的一个线程局部变量,它为每个使用该变量的线程都提供一个独立的变量副本,每个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。
java
Runnable task = () -> {
int value = (int) (Math.random() * 100); // 生成随机数
threadLocalVar.set(value); // 绑定到当前线程
System.out.println(Thread.currentThread().getName() + " 设置的值:" + threadLocalVar.get());
};
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
核心数据结构
ThreadLocal 的核心实现依赖于两个类:ThreadLocal 类本身和 ThreadLocalMap 类。ThreadLocalMap 是 ThreadLocal 的一个静态内部类,每个Thread对象都有一个ThreadLocalMap类型的成员变量threadLocals。
为什么内部要使用ThreadLocalMap来存储本地变量?
- 线程私有,避免线程之间的竞争
- Map类型可以支持多个本地变量的存储
- 定制hash表接口,key是threadLocal对象实例,为弱引用,当不再被外部强引用后,可以被垃圾回收。避免 ThreadLocal 本身无法被回收导致的内存泄漏。
2. 导致内存泄漏的原因
2.1 弱引用与强引用
ThreadLocalMap 里存储的键是 ThreadLocal 对象的弱引用,而值是强引用。当外部对 ThreadLocal 对象的强引用被移除之后,由于 ThreadLocal键是弱引用,在下一次垃圾回收时,这个 ThreadLocal键就会被回收掉,变成 null。不过,值依然是强引用,只要线程还在运行,ThreadLocalMap就不会被回收,那么这些键为null对应的值就没办法被访问到,却依旧占用着内存,最终造成内存泄漏。
引用类型简介
在 Java 里,有强引用、软引用、弱引用和虚引用这几种引用类型。其中和 ThreadLocal 内存泄漏问题相关的是强引用和弱引用:
- 强引用:平时最常用的引用方式,例如 Object obj = new Object();,只要强引用存在,对象就不会被垃圾回收。
- 弱引用:使用 WeakReference 类来表示,被弱引用关联的对象,在垃圾回收时,无论当前内存是否充足,都会被回收。
ThreadLocal 内存结构
Thread 类中有一个 ThreadLocalMap 类型的成员变量 threadLocals,ThreadLocalMap 是 ThreadLocal 的静态内部类,它以 ThreadLocal 对象为键,存储用户自定义的值。ThreadLocalMap 里的键是对 ThreadLocal 对象的弱引用。
内存泄漏产生过程
当代码中对 ThreadLocal 对象的强引用被移除后,由于 ThreadLocalMap 中的键是弱引用,在下次垃圾回收时,这个 ThreadLocal 键就会被回收,变成 null。 但是 ThreadLocalMap 中的值是强引用,只要线程还在运行,ThreadLocalMap 就不会被回收,那么这些键为 null 对应的值就无法被访问到,却依旧占用着内存,最终造成内存泄漏。
2.2 线程复用问题
在使用线程池的时候,线程会被复用。若 ThreadLocal 在使用完之后没有进行清理,当线程被下一次复用,之前的 ThreadLocal 变量依旧存在,可能会造成数据混乱,同时也会持续占用内存。
以上述的代码为例:
2.3 避免内存泄漏的方法
2.3.1 及时调用remove方法
java
public class ConcurrentTest {
private static final ThreadLocal<Integer> threadLocalVar = new ThreadLocal<>();
public static void main(String[] args) {
Runnable task = () -> {
int value = (int) (Math.random() * 100);
threadLocalVar.set(value);
System.out.println(Thread.currentThread().getName() + " 设置的值:" + threadLocalVar.get());
// 及时移除 ThreadLocal 中的值
threadLocalVar.remove();
};
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
}
}
2.3.2 使用try-finally块进行remove
java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ConcurrentTest {
// 创建一个 ThreadLocal 变量
private static final ThreadLocal<Integer> threadLocalVar = new ThreadLocal<>();
public static void main(String[] args) {
// 创建一个固定大小为 1 的线程池,保证线程复用
ExecutorService executorService = Executors.newFixedThreadPool(1);
// 定义任务
Runnable task = () -> {
try {
// 第一次执行任务时设置值
if (threadLocalVar.get() == null) {
int value = (int) (Math.random() * 100);
threadLocalVar.set(value);
System.out.println(Thread.currentThread().getName() + " 首次设置的值:" + threadLocalVar.get());
} else {
// 复用线程时获取之前设置的值
System.out.println(Thread.currentThread().getName() + " 复用线程获取的值:" + threadLocalVar.get());
}
} finally {
// 任务结束时清除 ThreadLocal 变量的值
threadLocalVar.remove();
}
};
// 提交任务到线程池
executorService.submit(task);
try {
// 等待第一个任务执行完成
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 再次提交任务,复用线程
executorService.submit(task);
// 关闭线程池
executorService.shutdown();
}
}