Java的ThreadLocal

环境

  • Java 25
  • Ubuntu 24.04.1
  • IntelliJ IDEA 2025.2.2 (Ultimate Edition)

ThreadLocal

顾名思义,ThreadLocal对象是线程本地的对象,这就意味着,在多线程应用中,每个线程都拥有自己独立的对象副本,以确保线程之间的数据隔离。

下面通过几个示例,来了解ThreadLocal的基本用法,注意事项,等等。

例1:基本用法

        ThreadLocal<String> threadLocal = new ThreadLocal<>();

        var t1 = new Thread(() -> {
            threadLocal.set("aaa");
            System.out.println("Thread1: " + threadLocal.get());
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("Thread1: " + threadLocal.get());
        });

        var t2 = new Thread(() -> {
            threadLocal.set("bbb");
            System.out.println("Thread2: " + threadLocal.get());
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("Thread2: " + threadLocal.get());
        });

        var t3 = new Thread(() -> {
            threadLocal.set("ccc");
            System.out.println("Thread3: " + threadLocal.get());
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("Thread3: " + threadLocal.get());
        });

        t1.start();
        t2.start();
        t3.start();

运行结果如下:

Thread1: aaa
Thread2: bbb
Thread3: ccc
Thread2: bbb
Thread3: ccc
Thread1: aaa

打印的顺序可能会变化,但无论是什么样的打印顺序, Thread1 总是和 aaa 绑定的,另外2个线程也同理。可见,3个线程拥有各自的 threadLocal 副本,修改自己的 threadLocal 副本,不会影响到其它线程。

注:本例中,主线程在启动子线程之后,无需显式等待(join)子线程结束,这是因为子线程不是daemon线程,主线程一定会等待其结束。

例2:注意事项

实际项目中,我们经常使用线程池来管理线程。由于线程池中的线程是可复用的,因此,要格外小心,一个task可能会“串用”到其它task的ThreadLocal对象。

        ThreadLocal<String> threadLocal = new ThreadLocal<>();

        try (ExecutorService executorService = Executors.newFixedThreadPool(1)) {
            var future1 = executorService.submit(() -> {
                threadLocal.set("aaa");
                System.out.println("Task1: " + threadLocal.get());
            });

            var future2 = executorService.submit(() -> {
                System.out.println("Task2: " + threadLocal.get());
            });
        }

本例中,在task future1 中把 threadlocal 设置为 "aaa" ,任务结束后,线程回到线程池。随后的task future2 复用了这个线程(本例中线程池里只有一个线程),由于 future1 没有清理 threadlocal 对象,导致 future2 可以直接取到 threadlocal

一种解决办法是, future2 在使用ThreadLocal对象前,先重新设置值。不过,更好的方式是“谁创建,谁负责”,也就说 future1 在结束时,主动释放ThreadLocal对象。

future1 负责释放ThreadLocal对象的另一个理由是,由于线程复用,如果 future1 结束时不释放ThreadLocal对象,该对象会一直被线程所持有,直到下一个task将其重新设置值,这显然会造成毫无必要的内存占用,而且可能是成本很高的资源,比如数据库连接、文件句柄等。

综上所述,应该尽早释放ThreadLocal对象:

            var future1 = executorService.submit(() -> {
                threadLocal.set("aaa");
                try {
                    System.out.println("Task1: " + threadLocal.get());
                } finally {
                    threadLocal.remove();
                }
            }); 

例3:remove() VS. set(null)

接上例,看下面的代码:

            var future1 = executorService.submit(() -> {
                threadLocal.set("aaa");
                try {
                    System.out.println("Task1: " + threadLocal.get());
                } finally {
                    threadLocal.set(null);
                }
            }); 

本例中,把 threadLocal.remove() 替换成 threadLocal.set(null) ,这样可以吗,有没有什么问题?

从运行结果来看,二者并没有太大区别,但从内存角度看,二者还是有一些差异的。

通过源码可知,线程维护了一个 ThreadLocalMap 对象,其entry是对ThreadLocal对象的弱引用(WeakReference),和对其值的强引用:

    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;
            }
        }
        // ......
        private Entry[] table;
        // ......

因此:

  • set(null) :在table里添加了一个entry,其key是ThreadLocal对象本身,其value是null
  • remove() :从table里把对应的entry移除

所以,ThreadLocalMap的table的entry的key是对ThreadLocal对象的弱引用。

假定存在一个entry,key是对 tl 对象的弱引用,而 tl 的value是null。

现在,table包含了1个entry,其key是tl弱引用,其value是null。

假定没有别的地方引用了 tl 。则在gc的时候, tl 对象就会被回收。

回收之后,table包含了1个enry,其key是null,value也是null。

可见,当后续不再用 tl 对象及其value时,如果采用 set(null) 来清理,则gc之后(把value对象回收了,并且把ThreadLocal对象回收了,因为是弱引用),table里仍然会留下一个key为null的entry。

如果有多个ThreadLocal对象发生这种情况,table里就会留下多个key为null的entry。这就造成了没必要的内存占用。

后续JVM会自动清理这些entry,但是总之还是有隐患。

所以,一定要用 remove() 来彻底移除entry。

事实上,在IntelliJ IDEA里,如果使用了 set(null) ,IDE会给出警报信息: 'ThreadLocal.set()' with null as an argument may cause memory leak

在这里插入图片描述

例4:虚拟线程

关于Java的虚拟线程简介,参见我另一篇文档 https://blog.csdn.net/duke_ding2/article/details/152010351

我们知道,虚拟线程是 Thread 类的子类,它运行在操作系统的线程上,后者也称为“载体线程”。但是,虚拟线程和载体线程之间没有绑定。那么,问题来了:ThreadLocal对象,是绑定到虚拟线程的,还是绑定到载体线程的?看下面的代码:

        ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

        for (int i = 0; i < 10; i++) {
            int j = i;
            var vt = Thread.ofVirtual().start(() -> {
                threadLocal.set(j);
                System.out.println(threadLocal.get() + " " + Thread.currentThread());
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println(threadLocal.get() + " " + Thread.currentThread());

            });
            Thread.sleep(100);
        }

        Thread.sleep(5000);

注:最下面的sleep操作,可以替换成对每个虚拟线程的join操作,我懒得join每个线程了,所以直接在主线程里sleep了5秒,以确保所有虚拟线程都可以完成。

运行结果如下:

0 VirtualThread[#27]/runnable@ForkJoinPool-1-worker-1
1 VirtualThread[#31]/runnable@ForkJoinPool-1-worker-1
2 VirtualThread[#32]/runnable@ForkJoinPool-1-worker-1
3 VirtualThread[#33]/runnable@ForkJoinPool-1-worker-2
4 VirtualThread[#34]/runnable@ForkJoinPool-1-worker-2
5 VirtualThread[#35]/runnable@ForkJoinPool-1-worker-2
6 VirtualThread[#36]/runnable@ForkJoinPool-1-worker-1
7 VirtualThread[#37]/runnable@ForkJoinPool-1-worker-2
8 VirtualThread[#38]/runnable@ForkJoinPool-1-worker-1
9 VirtualThread[#39]/runnable@ForkJoinPool-1-worker-2
0 VirtualThread[#27]/runnable@ForkJoinPool-1-worker-1
1 VirtualThread[#31]/runnable@ForkJoinPool-1-worker-1
2 VirtualThread[#32]/runnable@ForkJoinPool-1-worker-2
3 VirtualThread[#33]/runnable@ForkJoinPool-1-worker-1
4 VirtualThread[#34]/runnable@ForkJoinPool-1-worker-2
5 VirtualThread[#35]/runnable@ForkJoinPool-1-worker-2
6 VirtualThread[#36]/runnable@ForkJoinPool-1-worker-1
7 VirtualThread[#37]/runnable@ForkJoinPool-1-worker-2
8 VirtualThread[#38]/runnable@ForkJoinPool-1-worker-1
9 VirtualThread[#39]/runnable@ForkJoinPool-1-worker-2

可见,一共有10个并发虚拟线程,而实际的载体线程数量只有2个。然而,每个虚拟线程维护了一个自己的ThreadLocal变量副本。

总结:在虚拟线程下,ThreadLocal变量是和虚拟线程绑定的,而不是和载体线程绑定的。

虚拟线程和ThreadLocal都是JVM的产物,从侧面印证了,ThreadLocal和操作系统线程之间并无直接关系。

例5:ThreadLocal值不可继承

    private static final ThreadLocal<String> tl = new ThreadLocal<>();

    static void main() {
        new Thread(() -> {
            tl.set("abc");
            System.out.println(tl.get());
            new Thread(() -> {
                System.out.println(tl.get());
            }).start();
        }).start();
    }

可见,ThreadLocal对所有子线程可见,但其值不可继承。

例6:InheritableThreadLocal值可继承

    private static final InheritableThreadLocal<String> tl = new InheritableThreadLocal<>();

    static void main() {
        new Thread(() -> {
            tl.set("abc");
            System.out.println(tl.get());
            new Thread(() -> {
                System.out.println(tl.get());
                new Thread(() -> {
                    System.out.println(tl.get());
                }).start();
            }).start();
        }).start();
    }

运行结果如下:

abc
abc
abc

可见,InheritableThreadLocal的值是可继承的。

例7:InheritableThreadLocal值继承的一次性复制

    private static final InheritableThreadLocal<String> tl = new InheritableThreadLocal<>();

    static void main() {
        new Thread(() -> {
            tl.set("abc");
            System.out.println(Thread.currentThread() + tl.get());
            new Thread(() -> {
                System.out.println(Thread.currentThread() + tl.get());
                tl.set("def");
                System.out.println(Thread.currentThread() + tl.get());
            }).start();

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            System.out.println(Thread.currentThread() + tl.get());
        }).start();
    }

运行结果如下:

Thread[#26,Thread-0,5,main]abc
Thread[#29,Thread-1,5,main]abc
Thread[#29,Thread-1,5,main]def
Thread[#26,Thread-0,5,main]abc

可见,子线程从父线程继承InheritableThreadLocal值,是复制了一个副本,而不是共享的。

参考

  • https://docs.oracle.com/en/java/javase/25/core/thread-local-variables.html
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值