ThreadLocal

本文详细介绍了Java中的ThreadLocal类,包括其工作原理、如何存储和获取数据、以及为何选择ThreadLocal作为key的原因。重点讲解了线程数据隔离和跨函数传递的应用场景,以及InheritableThreadLocal在父子线程共享数据中的角色。

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

一、概述


        ThreadLocal(线程局部变量)是Java中的一个类,它提供了一种可以将数据与线程绑定的机制。每个ThreadLocal对象可以绑定一个特定的值,这个值只能被对应的线程访问和修改,其他线程无法访问。这种机制可以保证各个线程中的数据是相互独立的,避免了多线程并发访问时的线程安全问题。

        ThreadLocal通常被用于多线程环境下需要共享数据,但又要保证数据安全的情况。比如,在web应用中每个请求都会有一个对应的线程,而这个请求可能需要访问某些共享数据,但又不能让其他请求的线程访问,这时候就可以使用ThreadLocal将数据与请求线程绑定起来,实现数据的隔离和安全。
 

image.png

二、实现原理


        每个线程对象内部都维护着一个ThreadLocalMap(类似于HashMap),ThreadLocalMap内部数据结构是一个由Entry对象组成的数组(默认长度为16)。所谓的Entry对象其实就是一个键值对,key为ThreadLocal(弱引用,方便被回收),value为存储的数据。

        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];//初始化Entry数组
            //根据threadLocal的Hash编码与数组长度-1做与运算算出下标
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            //将新创建的Entry对象存入数组对应下标位置
            table[i] = new Entry(firstKey, firstValue);
            //长度为1
            size = 1;
            //容量为16
            setThreshold(INITIAL_CAPACITY);
        }

        当我们在某一个线程内部使用ThreadLocal存储值时,其实就是在该线程对象内部的ThreadLocalMap中存入了一个以ThreadLocal为key,value为值的键值对。

640.png

 三、常用方法与源码阅读


3.1 set()

存储数据至当前线程的 ThreadLocalMap : public void set(T value)

    public void set(T value) {
        Thread t = Thread.currentThread();//获取当前线程的全部信息
        ThreadLocalMap map = getMap(t);//获取当前线程内部的ThreadLocalMap 
        if (map != null)
            map.set(this, value);//有这个map时以当前threadLocal为键,value为值存入map
        else
            createMap(t, value);//没有则使用creatMap()方法创建Map
    }

    void createMap(Thread t, T firstValue) {
        //以当前threadLocal为键,value为值初始化创建一个ThreadLocalMap
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

3.2 get()

从当前线程的 ThreadLocalMap 中获取数据: public T get()

    public T get() {
        Thread t = Thread.currentThread();//获取当前线程的全部信息
        ThreadLocalMap map = getMap(t);//获取当前线程内部的ThreadLocalMap
        if (map != null) {
            //若map不为空,根据threadLocal获取到对应的Entry对象
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                //若Entry对象不为空,获取到对应的value并返回
                T result = (T)e.value;
                return result;
            }
        }
        //若为空执行setInitialValue()方法
        return setInitialValue();
    }

    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            //不为空存入键值对
            map.set(this, value);
        else
            //为空创建新Map
            createMap(t, value);、
        //返回初始化值
        return value;
    }

3.3 remove()

从当前线程的 ThreadLocalMap 中删除数据: public void remove()

        一定要在 finally 代码块中,调用 remove() 方法清理没用的数据。如果业务代码出现异常,也能及时清理没用的数据。 remove() 方法中会把 Entry 中的 key 和 value 都设置成 null ,这样就能被GC及时回收,无需触发额外的清理机制,所以它能解决内存泄露问题。

     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());//获取当前线程内部的Map集合
         if (m != null)
            //有map则根据threadLocal删除该Entry对象
             m.remove(this);
     }

3.4 下标计算

根据threadLocalHashCode计算下标时如出现哈希冲突,则使用开放定址法寻找新的下标。

        private static int nextIndex(int i, int len) {
            //如果计算下标不是数组最后一位则返回该位置的下一位,是则从头开始寻找
            return ((i + 1 < len) ? i + 1 : 0);
        }

四、为什么用ThreadLocal做key?


        如果在应用中,一个线程中只使用了一个 ThreadLocal 对象,那么使用 Thread 做 key 也是可以的,代表每个 Thread 线程对应一个 value 。但是,在实际应用程序开发过程中,一个线程中很有可能不只使用了一个 ThreadLoca l 对象。这时使用 Thread 做 key 就会产生混淆。

640.png

五、父子线程共享数据


        在这种情况下使用 ThreadLocal 是行不通的。 main 方法是在主线程中执行的,相当于父线程。在 main 方法中开启了另外一个线程,相当于子线程。两个线程对象,各自拥有不同的 ThreadLocalMap 。应该使用 InheritableThreadLocal ,它是 JDK 自带的类,继承了 ThreadLocal 类。从他的源码可以发现共享数据的原理是创建子线程的“一刹那”将父线程的ThreadLocalMap复制给子线程。

static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
        return new ThreadLocalMap(parentMap);
    }

private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];

            for (int j = 0; j < len; j++) {
                Entry e = parentTable[j];
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                    if (key != null) {
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value);
                        int h = key.threadLocalHashCode & (len - 1);
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }

六、ThreadLocal应用场景


6.1 线程数据隔离

        ThreadLocal 的主要价值在于线程隔离,ThreadLocal 中的数据只属于当前线程,该数据对别的线程是不可见的,起到隔离作用。这样操作,可以在多线程环境下,可以防止当前线程的数据被其他线程修改。另外,由于各个线程之间的数据相互隔离,避免了同步加锁带来的性能损失,大大提升了并发性的性能。

6.2 跨函数传递

        数据通常用于同一个线程内,跨类、跨方法传递数据时,如果不用 ThreadLocal ,那么相互之间的数据传递势必要靠返回值和参数,这样无形之中增加了这些类或者方法之间的耦合度。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值