ThreadLocal本地变量中你所不知道的那些事

说起本地线程变量,大家首先会想到的是JDK默认提供的ThreadLocal,用来存储在整个调用链中都需要访问的数据,并且是线程安全的,本地变量为线程上下文环境传递提供便捷。

首先大概了解下ThreadLocal 是如何存储值的吧!
Threadlocal 是一个线程内部的存储类,可以在指定线程内存储数据,数据存储以后,只有指定线程可以得到存储数据
大致意思就是ThreadLocal提供了线程内存储变量的能力,这些变量不同之处在于每一个线程读取的变量是对应的互相独立的。通过get和set方法就可以得到当前线程对应的值。
做个不恰当的比喻,从表面上看ThreadLocal相当于维护了一个map,key就是当前的线程,value就是需要存储的对象。
这里的这个比喻是不恰当的,实际上是ThreadLocal的静态内部类ThreadLocalMap threadLocals为每个Thread都维护了一个数组table,ThreadLocal确定了一个数组下标,而这个下标就是value存储的对应位置。

public T get() {
     Thread t = Thread.currentThread();  // @1
     ThreadLocalMap map = getMap(t);  // @2
     if (map != null) {               // @3
         ThreadLocalMap.Entry e = map.getEntry(this);
         if (e != null) {
             @SuppressWarnings("unchecked")
             T result = (T)e.value;
             return result;
        }
    }
    return setInitialValue();  // @4
}


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

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

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

代码@1:获取当前线程。

代码@2:获取线程的threadLocals属性,在上图中已展示其存储结构。

代码@3:如果线程对象的threadLocals属性不为空,则从该Map结构中,用threadLocal对象为键去查找值,如果能找到,则返回其value值,否则执行代码@4。

代码@4:如果线程对象的threadLocals属性为空,或未从threadLocals中找到对应的键值对,则调用该方法执行初始化。

set方法、remove其实现的逻辑基本一样,就是对线程对象的threadLocals属性进行操作(Map结构)。

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

相信大家能很容易看出这些代码,现在我们来做个例子:

public class ThreadLocalService {

   private static ThreadLocal<Integer> requestIdThreadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        Integer reqId = new Integer(5);
        ThreadLocalService a = new ThreadLocalService();
        a.setRequestId(reqId);
    }

    public void setRequestId(Integer requestId) {
        requestIdThreadLocal.set(requestId);
        doBussiness();
    }

    public void doBussiness() {
        System.out.println("首先打印requestId:" + requestIdThreadLocal.get());
        new Thread(() -> {
            System.out.println("子线程启动");
            System.out.println("在子线程中访问requestId:" + requestIdThreadLocal.get());
        }).start();

    }
}

运行结果你会发现,ThreadLocal 居然不能在主线程和子线程中传递变量。
为了解决该问题,JDK引入了另外一个线程本地变量实现类 InheritableThreadLocal

InheritableThreadLocal只是在ThreadLocal的get、set、remove流程中,重写了getMap、createMap方法,整体流程与ThreadLocal保持一致,所以接下来我们来看一下InheritableThreadLocal是如何重写上述这两个方法的。

ThreadLocalMap getMap(Thread t) {
    return t.inheritableThreadLocals;
}

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

从代码得知,
ThreadLocal操作的是Thread对象的threadLocals属性,
InheritableThreadLocal操作的是Thread对象的inheritableThreadLocals属性。

紧接着就来探究InheritableThreadLocal是如何继承自父对象的线程本地变量的,请先看下此段代码

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++;
                    }
                }
            }
        }

这段代码类似于Map的复制,只不过其在Hash冲突时,不是使用链表结构,而是直接在数组中找下一个为null的槽位。

看到这里我们再来举个InheritableThreadLocal的例子:

public class InheritableThreadLocalService {
    /**
     * 模拟tomcat线程池
     */
    private static ExecutorService tomcatExecutors = Executors.newFixedThreadPool(10);
    /**
     * 业务线程池,默认Control中异步任务执行线程池
     */
    private static ExecutorService businessExecutors = Executors.newFixedThreadPool(5);
    /**
     * 线程上下文环境,模拟在Control这一层,设置环境变量,然后在这里提交一个异步任务,模拟在子线程中,是否可以访问到刚设置的环境变量值。
     */
    private static InheritableThreadLocal<Integer> requestIdThreadLocal = new InheritableThreadLocal<>();

    public static void main(String[] args) {

        // 模式10个请求,每个请求执行ControlThread的逻辑,其具体实现就是,先输出父线程的名称,
        // 然后设置本地环境变量,并将父线程名称传入到子线程中,在子线程中尝试获取在父线程中的设置的环境变量
        for (int i = 1; i <= 10; i++) {
            tomcatExecutors.submit(new ControlThread(i));
        }

        //简单粗暴的关闭线程池
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        businessExecutors.shutdown();
        tomcatExecutors.shutdown();
    }


    /**
     * 模拟Control任务
     */
    static class ControlThread implements Runnable {
        private int i;

        public ControlThread(int i) {
            this.i = i;
        }

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + ":" + i);
            requestIdThreadLocal.set(i);
            //使用线程池异步处理任务
            businessExecutors.submit(new BusinessTask(Thread.currentThread().getName()));
        }
    }

    /**
     * 业务任务,主要是模拟在Control控制层,提交任务到线程池执行
     */
    static class BusinessTask implements Runnable {
        private String parentThreadName;

        public BusinessTask(String parentThreadName) {
            this.parentThreadName = parentThreadName;
        }

        @Override
        public void run() {
//            System.out.println(Thread.currentThread().getName() + ":****" + requestIdThreadLocal.get());
            //如果与上面的能一一对应上来,则说明正确,否则失败
            System.err.println("parentThreadName:" + parentThreadName + ":" + requestIdThreadLocal.get());
        }
    }

}

请注意这个线程:pool-2-thread-3:****8
请注意这个线程:pool-2-thread-5:****5
请注意这个线程:pool-2-thread-4:****4
请注意这个线程:pool-2-thread-2:****7
请注意这个线程:pool-2-thread-1:****6
请注意这个线程:pool-2-thread-2:****7
请注意这个线程:pool-2-thread-4:****4
请注意这个线程:pool-2-thread-5:****5
请注意这个线程:pool-2-thread-3:****8
请注意这个线程:pool-2-thread-1:****6

从执行结果可以看出,当 InheritableThreadLocal 在单线程时没有问题,但是在多线程状态下,因为线程池的复用机制,能减少线程的频繁创建与销毁,线程池中的线程拷贝的数据将来自于第一个提交任务的外部线程,即后面的外部线程向线程池中提交任务时,子线程访问的本地变量都来源于第一个外部线程,因此本地变量值也会是第一个外部线程的值,造成线程本地变量不一致

那我们要如何解决在多线程下数据一一对应呢,此时 阿里巴巴开源的专门解决InheritableThreadLocal的局限性的 TransmittableThreadLocal 闪亮登场


    private static InheritableThreadLocal<Map<TransmittableThreadLocal<?>, ?>> holder = new InheritableThreadLocal<Map<TransmittableThreadLocal<?>, ?>>() {
        protected Map<TransmittableThreadLocal<?>, ?> initialValue() {
            return new WeakHashMap();
        }

        protected Map<TransmittableThreadLocal<?>, ?> childValue(Map<TransmittableThreadLocal<?>, ?> parentValue) {
            return new WeakHashMap(parentValue);
        }
    };


    public final void set(T value) {
        super.set(value);       // @1
        if (null == value) {     // @2
            this.removeValue();
        } else {
            this.addValue();   // @3
        }

    }

 private void removeValue() {
        ((Map)holder.get()).remove(this);
    }

   private void addValue() {
        if (!((Map)holder.get()).containsKey(this)) {
            ((Map)holder.get()).put(this, (Object)null);
        }

    }

代码@1:首先调用父类的set方法,将value存入线程本地遍历,即Thread对象的inheritableThreadLocals中。

代码@2:如果value为空,则调用removeValue()否则调用addValue。

代码@3:当前线程在调用threadLocal方法的set方法(即向线程本地遍历存储数据时),如果需要设置的值不为null,则调用addValue方法,将当前ThreadLocal存储到TransmittableThreadLocal的全局静态变量holder。
使用了线程本地变量,内部存放的结构为Map, ?>,即该对象缓存了线程执行过程中所有的TransmittableThreadLocal对象,并且其关联的值不为空。但这样做有什么用呢?
这里就要去了解

       @Nonnull
        public static Object replay(@Nonnull Object captured) {
            Map<TransmittableThreadLocal<?>, Object> capturedMap = (Map)captured;
            Map<TransmittableThreadLocal<?>, Object> backup = new HashMap();
            Iterator iterator = ((Map)TransmittableThreadLocal.holder.get()).entrySet().iterator();

            while(iterator.hasNext()) {
                Entry<TransmittableThreadLocal<?>, ?> next = (Entry)iterator.next();
                TransmittableThreadLocal<?> threadLocal = (TransmittableThreadLocal)next.getKey();
                backup.put(threadLocal, threadLocal.get());
                if (!capturedMap.containsKey(threadLocal)) {
                    iterator.remove();
                    threadLocal.superRemove();
                }
            }

            setTtlValuesTo(capturedMap);
            TransmittableThreadLocal.doExecuteCallback(true);
            return backup;
        }

        @Nonnull
        public static Object clear() {
            return replay(Collections.emptyMap());
        }

        public static void restore(@Nonnull Object backup) {
            Map<TransmittableThreadLocal<?>, Object> backupMap = (Map)backup;
            TransmittableThreadLocal.doExecuteCallback(false);
            Iterator iterator = ((Map)TransmittableThreadLocal.holder.get()).entrySet().iterator();

            while(iterator.hasNext()) {
                Entry<TransmittableThreadLocal<?>, ?> next = (Entry)iterator.next();
                TransmittableThreadLocal<?> threadLocal = (TransmittableThreadLocal)next.getKey();
                if (!backupMap.containsKey(threadLocal)) {
                    iterator.remove();
                    threadLocal.superRemove();
                }
            }

            setTtlValuesTo(backupMap);
        }

replay():"重放"父线程的本地环境变量,即使用从父线程中捕获过来的上下文环境,在子线程中重新执行一遍,并返回原先存在与子线程中的上下文环境变量。

restore():恢复线程池中当前执行任务的线程的上下文环境,即代码@1,会直接继承父线程中的上下文环境,但会将原先存在该线程的线程上下文环境进行备份,在任务执行完后通过执行restore方法进行恢复。

capturedMap 子线程从父线程捕获的线程本地遍历。

backup 线程池中处理本次任务的线程中原先存在的本地线程变量。

这个例子只需要将上面的代码替换成 TransmittableThreadLocal 即可,它的实现原理是:
从InheritableThreadLocal不支持线程池的根本原因是InheritableThreadLocal是在父线程创建子线程时复制的,由于线程池的复用机制,减少开销,子线程只会复制一次,要支持线程池中能访问提交任务线程的本地变量,其实只需要在父线程向线程池提交任务时复制父线程的上下环境,那在子线程中就能够如愿访问到父线程中的本地变量,实现本地环境变量在线程池调用中的透传,这也就是TransmittableThreadLocal最本质的实现原理。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值