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操作

image

set的大对象会在minor gc 时候就被清理掉。不会带入到老年代。

2、不执行remove操作

image

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

posted @ 2023-06-09 13:53  朋羽  阅读(7)  评论(0编辑  收藏  举报