Threadlocal
作用
ThreadLocal用来每个线程存储自己一份,在整个线程声明周期都可以访问到。
基本使用
三个基本方法set/get/remove。set初始时候放值,get获取设置的值,remove清除设置的值。
ThreadLocal<String> threadLocal = new ThreadLocal<>();
new Thread(()->{
threadLocal.set(Thread.currentThread().getName());
System.out.println( Thread.currentThread().getName()+":"+threadLocal.get());
threadLocal.remove();
}).start();
new Thread(()->{
threadLocal.set(Thread.currentThread().getName());
System.out.println( Thread.currentThread().getName()+":"+threadLocal.get());
threadLocal.remove();
}).start();
每个线程存储通过set设置该线程的ThreadLocal。get获取设置的值。
单次使用最好执行remove操作将其清空。否则如果使用线程池时会存在可能的内存泄露。
内部结构
ThreadLocal set的数据最终是存放在Thread中的,Thread使用ThreadLocalMap存放多个ThreadLocal。
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocalMap <key,value> key是ThreadLocal本身,value是set的值。ThreadLocalMap中使用引用使存储key(也就是对ThreadLocal的引用)
static class ThreadLocalMap {
/**
*
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//存储set的值
private Entry[] table;
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//创建ThreadLocalMap的时候初始化table大小
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
}
这样就存在一个这样的引用链
Thread->ThreadLocalMap ->Entry->(ThreadLocal,Value),其中Entry对ThreadLocal是弱引用。
弱引用的特点:只会存在一个垃圾回收周期,一执行垃圾回收弱引用就会被回收掉。这里如果是每次new一个新线程,线程结束后所有上面的引用链都销毁,都可以被垃圾回收。问题是如果是线程池中。一般线程池中线程声明周期较长,随应用结束而结束。这个时候就会发生一种情况,弱引用被回收掉,即ThreadLocal被回收(没有被其它引用),这个时候就会存在一条Entry中key为null,的value。这个value永远无法被回收。造成可能的内存泄漏。所以使用线程池时候最好最后调用下remove方法释放value引用。
为什么要使用弱引用?
如果使用强引用:我们知道,ThreadLocalMap的生命周期基本和Thread的生命周期一样,当前线程如果没有终止,那么ThreadLocalMap始终不会被GC回收,而ThreadLocalMap持有对ThreadLocal的强引用,那么ThreadLocal也不会被回收,当线程生命周期长,如果没有手动删除,则会造成kv累积,从而导致OOM
如果使用弱引用:弱引用中的对象具有很短的声明周期,因为在系统GC时,只要发现弱引用,不管堆空间是否足够,都会将对象进行回收。而当ThreadLocal的强引用被回收时,ThreadLocalMap所持有的弱引用也会被回收,如果没有手动删除kv,那么会造成value累积,也会导致OOM
总结起来弱引用还可以被一定回收,强引用更不能被回收。
线程池内存泄漏测试:
准备以下程序:
//初始化线程池
static ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>());
public static void main(String[] args) throws InterruptedException {
ThreadLocalTest test = new ThreadLocalTest();
for (int i = 0; i < 20; i++) {
test.exec();
System.out.println(i);
Thread.sleep(8000);
}
System.out.println("end");
Thread.sleep(1000000);
poolExecutor.shutdown();
}
public void exec(){
ThreadLocal<int[]> threadLocal = new ThreadLocal<>();
for (int i = 0; i < 50; i++) {
poolExecutor.execute(()->{
//设置一个大对象
threadLocal.set(new int[1024*1024]);
//可选操作,执行remove和不执行remove分两次观察GC情况。
threadLocal.remove();
try {
Thread.sleep(new Random().nextInt(100));
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
配置java启动参数,设置jvm大小,打印GC日志。
-verbose:gc -Xmx500m -Xms500m -XX:+PrintGCDetails -Xloggc:D:/gc.log
1、执行remove操作
set的大对象会在minor gc 时候就被清理掉。不会带入到老年代。
2、不执行remove操作
minor gc时无法将大对象回收掉。会进入老年代。在full gc时候老年代的大对象会被清理掉。
这里为什么老年代的会被回收呢?不是说失去引用,失去了回收能力了吗。
应该时因为ThreadLoca在每次执行set、get、remove方法内部都会执行entry key为null的数据清理,移除key为null的entry。这时entry的value(大对象)就完全没有引用,可以被清理掉。
参考:
https://blog.csdn.net/apple_52109766/article/details/125556875