ThreadLocal 详解

x33g5p2x  于2021-12-18 转载在 其他  
字(3.0k)|赞(0)|评价(0)|浏览(434)

1. ThreadLocal 是用来干什么的

通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量该如何解决呢? JDK 中提供的 ThreadLocal 类正是为了解决这样的问题。 ThreadLocal 类主要解决的就是让每个线程绑定自己的值,可以将 ThreadLocal 类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。

如果你创建了一个 ThreadLocal 变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是 ThreadLocal 变量名的由来。他们可以使用 get()set() 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。

2. ThreadLocal 示例

代码如下:

public class TestThreadLocal {
    private static final ThreadLocal<String> STRING_THREAD_LOCAL = new ThreadLocal<String>(){
        // 用来初始化值,默认为 null
        @Override
        protected String initialValue() {
            return "我是初始值";
        }
    };

    public static void main(String[] args) throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + " begin");
        System.out.println("STRING_THREAD_LOCAL = " + STRING_THREAD_LOCAL.get());
        System.out.println("main change");
        STRING_THREAD_LOCAL.set("我是 main");
        System.out.println("STRING_THREAD_LOCAL = " + STRING_THREAD_LOCAL.get());
        System.out.println(Thread.currentThread().getName() + " end");

        TimeUnit.SECONDS.sleep(1);
        System.out.println("-----------------------------------------------------------");

        Thread t1 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " begin");
            System.out.println("STRING_THREAD_LOCAL = " + STRING_THREAD_LOCAL.get());
            System.out.println("t1 change");
            STRING_THREAD_LOCAL.set("我是 t1");
            System.out.println("STRING_THREAD_LOCAL = " + STRING_THREAD_LOCAL.get());
            System.out.println(Thread.currentThread().getName() + " end");
        }, "t1");
        t1.start();

        TimeUnit.SECONDS.sleep(1);
        System.out.println("-----------------------------------------------------------");

        Thread t2 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " begin");
            System.out.println("STRING_THREAD_LOCAL = " + STRING_THREAD_LOCAL.get());
            System.out.println("t2 change");
            STRING_THREAD_LOCAL.set("我是 t2");
            System.out.println("STRING_THREAD_LOCAL = " + STRING_THREAD_LOCAL.get());
            System.out.println(Thread.currentThread().getName() + " end");
        }, "t2");
        t2.start();
    }
}

控制台输出如下:

main begin
STRING_THREAD_LOCAL = 我是初始值
main change
STRING_THREAD_LOCAL = 我是 main
main end
-----------------------------------------------------------
t1 begin
STRING_THREAD_LOCAL = 我是初始值
t1 change
STRING_THREAD_LOCAL = 我是 t1
t1 end
-----------------------------------------------------------
t2 begin
STRING_THREAD_LOCAL = 我是初始值
t2 change
STRING_THREAD_LOCAL = 我是 t2
t2 end

3. ThreadLocal 实现原理

每一个线程都有一个自己的 ThreadLocalMap 实例,其中每个键值对的键是一个 ThreadLocal 对象,值为每个线程专属的值,我们可以结合下图来理解。

4. ThreadLocal 内存泄漏

内存泄漏和内存溢出是不同的:

  • 内存泄漏:一块内存一直被占用着,无法另作他用,就好像丢了一样,被称作内存泄漏。
  • 内存溢出:内存用完了,递归时很有可能出现内存溢出问题。

内存泄漏的多的话会加大内存溢出的概率。

上图中的 tl 是一个强引用。
为什么 ThreadLocalMap 中的 key 指向 ThreadLocal 对象时使用弱引用?

在线程结束后,tl 引用会销毁,假如 key 使用强引用的话会导致在垃圾回收时无法回收 ThreadLocal 对象,会产生内存泄漏(此时导致内存泄漏的是 ThreadLocal 对象及其对应的键值对)。

就算使用弱引用,还有可能会导致内存泄漏(此时导致内存泄漏的是 ThreadLocalMap 中的 value),ThreadLocal 被回收后,key 会变为 null,会导致整个 value 再也无法被访问到,value 是强引用,会导致它指向的对象无法被回收,从而导致内存泄漏。

为了避免上述两种内存泄漏的发生,对于那些不再使用的 ThreadLocal 我们一定要调用 remove() 方法移除。

相关文章