ThreadLocal

本文详细解析ThreadLocal的内部结构,说明它如何解决线程安全问题,讨论InheritableThreadLocal的特性以及ThreadLocal可能导致内存泄露的条件,还提到在线程池中使用TransmittableThreadLocal的解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

ThreadLocal内部结构

在这里插入图片描述

在这里插入图片描述

ThreadLocal与Thread的关系
在这里插入图片描述

在Thread类里面维护了一个字段ThreadLocalMap,但是对于这个字段的操作方法,由ThreadLocal进行声明。

两个线程使用同一个threadLocal,为什么没有线程安全问题?

public class ThreadLocalDemo {

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

    public static void print(String str) {
        System.out.println(str + " :" + threadLocal.get());
        threadLocal.remove();
    }
    
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            threadLocal.set("local_A");
            print("A");
            System.out.println("after remove : " + threadLocal.get());

        },"A").start();

        Thread.sleep(1000);

        new Thread(() -> {
            threadLocal.set("local_B");
            print("B");
            System.out.println("after remove : " + threadLocal.get());

        },"B").start();
    }
}

针对以上代码,可能误以为存在线程安全问题,ThreadLocal使用了什么神奇的方式解决了该问题,但其实根本就不存在线程安全问题,看threadLocal.set();和threadLocal.get();源码可知

set方法源码

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            map.set(this, value);
        } else {
            createMap(t, value);
        }
    }
    
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

set方法,其实是获取当前线程的t.threadLocals字段,该字段就是真正用来存储数据的,而不是ThreadLocal threadLocal = new ThreadLocal<>();存储数据的。自己new的ThreadLocal,仅仅用来当做key存到线程的threadLocals。
因此这里虽然两个线程都在使用自己new的threadLocal,可是这个对象仅仅用来当做key存放到各自线程的threadLocals字段中。真正的容器是各自线程的threadLocals字段。所以此处是每个线程都有一个存放数据的容器threadLocals字段,两个容器都是用了同一个对象当做key。

在这里插入图片描述

ThreadLocal的remove方法,会将Entry的key和value都置空。如果不手动调用remove方法。Entry中的key是弱引用。在下次gc时,会回收弱引用的对象。但是value是强引用。因此可能存在entry中key是null,value有值的情况。如果value没有其他外部引用的话,value将会随着线程生命周期结束,而结束。

ThreadLocal使用场景,比如数据库事务,用户session

InheritableThreadLocal 可继承的ThreadLocal

在这里插入图片描述

InheritableThreadLocal是ThreadLocal的子类,该类没有扩充新字段
只是重写了getMap和createMap方法。

    public Thread() {
        init(null, null, "Thread-" + nextThreadNum(), 0);
    }
    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize) {
        init(g, target, name, stackSize, null, true);
    }

在这里插入图片描述

创建线程的时候,会判断主线程的inheritableThreadLocals是否为null,如果不是null,就把主线程中.inheritableThreadLocals的内容复制到子线程。用来传递到子线程
但如果在线程池中,由于线程是复用的,因此无法进行传递

线程池中不同线程值传递的解决方案:阿里云开源的TransmittableThreadLocal。

ThreadLocal可能导致内存泄露的条件

1、没有手动删除数据
2、线程一直运行,没有结束,比如线程池的线程
3、没有再次调用set/get/remove等方法,
3的解释因为调用这些方法的时候,会判断key为null的Entry,如果key为空,那么value应该就需要被清理了。因此会把对应的Entry清空。不仅仅当前位置的,会依次遍历下个位置,只要key为null的Entry就会被清空。
删除无效数据的方法expungeStaleEntry被调用的时机:
1、set的时候如果map满了,扩容的时候会调用。
2、get的时候,如果发现当前位置Entry中的key为null,会调用。
3、调用remove方法的时候,删除完当前位置的Entry之后,也会调用

在这里插入图片描述