一、ThreadLocal内存泄漏分析
1.1 ThreadLocal实现原理
1.1.1、set(T value)方法
查看ThreadLocal源码的 **set(T value)**方法,可以发现数据是存在了ThreadLocalMap的静态内部类Entry里面
其中key为使用弱引用的ThreadLocal实例,value为set传入的值。核心源代码:
public void set(T value) {
Thread t = Thread.currentThread();
// ThreadLocalMap跟当前线程对象绑定,是线程对象中的一个成员属性
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
// 第一次调用的时候,将当前线程和value往下传。
createMap(t, value);
}
void createMap(Thread t, T firstValue) {
// 当前线程内部的变量 ThreadLocal.ThreadLocalMap threadLocals 设置为新建出来的对象。
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
...
}
1.2 ThreadLocal 内存泄漏的原因
引用原理图如下,实心箭头 表示强引用 ,虚线箭头 表示弱引用 。
1、图中所示,当前线程强引用了ThreadLocalMap,而ThreadLocal为ThreadLocalMap的弱引用Key。
2、结合1.2节知识背景,如果ThreadLocal没有被外部强引用 ,当系统触发GC时,会将ThreadLocal对象 回收掉,会导致ThreadLocalMap的Key为null ,但是value 还是被当前线程强引用,只有当Thread线程退出后,value的强引用链才会断开。
3、如果线程不结束(比如使用了线程池),则引用链(Thread -> ThreadLocalMap -> Entry -> value)一直存在,永远不会被回收,从而造成内存泄漏。
1.2.1 代码演示内存泄漏
public class ThreadLocalTest {
public static void main(String[] args) {
//二、TheadLocal内存泄漏
//2.1、局部代码块中创建ThreadLocal后不引用它。
createThreadLocal();
//2.2、让GC回收不再被强引用,只有弱引用的TheadLocal对象
System.gc();
//2.3、查看线程成员属性ThreadLocalMap中 存入的键值对(key为null,而value还在,出现内存泄漏问题)
Thread thread = Thread.currentThread();
}
public static ThreadLocal<String> createThreadLocal(){
ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("zs");
return threadLocal;
}
}
debug运行代码,查看当前线程的ThreadLocalMap中的数据,可以发现引用的Key已经被GC回收了,造成了内存泄漏
1.3 key为什么使用弱引用
即使没有手动删除key和value,ThreadLocal在没有被引用的时候也会被回收。即ThreadLocalMap的key为null,下一次ThreadLocalMap调用set()、get()、remove()方法的时候会清除没被回收的value。
1.3.1 代码演示清除没被回收的value
package com.adolesce.server.mutithread;
/**
* @author Administrator
* @version 1.0
* @description: TODO
* @date 2023/7/1 9:52
*/
public class ThreadLocalTest {
public static void main(String[] args) {
//三、TheadLocal没被强引用后,触发System.gc(),将key回收,设为null
//3.1、创建ThreadLocal对象
ThreadLocal threadLocal = createThreadLocal();
//3.2、模拟threadLocal没被引用:断点时手动将thread中ThreadLocalMap value为【zs】对应的key设为null
Thread thread = Thread.currentThread();
//3.3、测试get、set、remove方法将key为null的value清除
threadLocal.get();
//3.4、查看ThreadLocalMap中value是否为null了
thread = Thread.currentThread();
}
public static ThreadLocal<String> createThreadLocal(){
ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("zs");
return threadLocal;
}
}
调用get方法的时候,由于map中的value对应的key为null,通过当前ThreadLocal对象去获取是获取value是获取不到Entry,于是调用初始化value的方法,清除原来的value,如get源码:
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) { // 为null跳出判断
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue(); // 调用初始化value的方法,清除原来的value
}
二、ThreadLocal的正确使用方法
1、每次使用完ThreadLocal都调用它的remove()方法清除数据。
2、将ThreadLocal变量定义成 private static ,这样就一直存在 ThreadLocal的强引用,也就能保证任何时候都能
通过 ThreadLocal的弱引用访问到Entry的value值,进而清除掉 。
package com.adolesce.server.mutithread;
/**
* @author Administrator
* @version 1.0
* @description: TODO
* @date 2023/7/1 9:52
*/
public class ThreadLocalTest {
private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
//一、TheadLocal使用
System.out.println("主线程开启,线程ID:" + Thread.currentThread().getId());
//1.1、向ThreadLocalMap中存入键值对
setData();
//1.2、从ThreadLocal中获取数据
getData();
//1.3、清除(通常在拦截器的afterCompletion()方法中进行清除)
removeData();
}
private static void setData() {
threadLocal.set("zhangsan");
System.out.println("在线程"+Thread.currentThread().getId()+"中向ThreadLocal中存入了姓名");
}
private static void getData() {
String name = threadLocal.get();
System.out.println("在线程"+Thread.currentThread().getId()+"中从ThreadLocal中取了姓名:" + name);
}
private static void removeData() {
threadLocal.remove();
}
}
三、总结
1、内存泄漏原因:我们使用ThreadLocal过程中,如果ThreadLocal对象强引用断掉后,只剩弱引用,ThreadLocal对象会被回收,此时ThreadLocal中的key会变为null,而value没有被回收,同时又由于ThreadLocalMap是Thread中的成员属性,与Thread对象的生命周期是一样长,如果当前线程一直未被销毁,又没有手动删除对应key,这样就会导致value内存泄漏。
2、使用弱引用可以多一层value回收的保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set()、get()、remove()的时候会被清除。
3、因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。