每日一博 - ThreadLocal VS InheritableThreadLocal VS TransmittableThreadLocal

Posted 小小工匠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了每日一博 - ThreadLocal VS InheritableThreadLocal VS TransmittableThreadLocal相关的知识,希望对你有一定的参考价值。


ThreadLocal

位于java.lang包中的ThreadLocal 。

多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。

ThreadLocal是除了加锁这种同步方式之外的一种保证一种规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题。

ThreadLocal是JDK包提供的,它提供线程本地变量,如果创建一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题


核心API

  • public T get() 从线程上下文环境中获取设置的值
  • public void set(T value) 将值存储到线程上下文环境中,供后续使用
  • public void remove() 清除线程本地上下文环境

ThreadLocal类

【数据存储位置】

当线程调用 threadLocal 对象的 set(Object value) 方法时,数据并不是存储在 ThreadLocal 对象中,而是存储在 Thread 对象中,这也是 ThreadLocal 的由来,具体存储在线程对象的threadLocals 属性中,其类型为 ThreadLocal.ThreadLocalMap

【ThreadLocal.ThreadLocalMap】
Map 结构,即键值对,键为 threadLocal 对象,值为需要存储到线程上下文的值(threadLocal#set)方法的参数


源码分析

set

   public void set(T value) {
   		// 获取当前线程 
        Thread t = Thread.currentThread();
         // 获取线程的 threadLocals 属性
        ThreadLocalMap map = getMap(t);
        // 如果不为空,设置k  v 
        if (map != null)
            map.set(this, value);
        else
        	// 初始化线程对象的 threadLocals,然后将 threadLocal:value 键值对存入线程对象的threadLocals 属性中。
            createMap(t, value);
    }

get

public T get() {
		// 获取当前线程 
        Thread t = Thread.currentThread();
        // 获取线程的 threadLocals 属性
        ThreadLocalMap map = getMap(t);
		// 如果线程对象的 threadLocals 属性不为空,则从该 Map 结构中,用 threadLocal 对象为键去查找值,如果能找到,则返回其 value 值,否则执行代码 setInitialValue()
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        // 果线程对象的 threadLocals 属性为空,或未从 threadLocals 中找到对应的键值对,则调用该方法执行初始化
        return setInitialValue();
    }
 private T setInitialValue() {
 		// 调用 initialValue() 获取默认初始化值,该方法默认返回 null,子类可以重写,实现线程本地变量的初始化。
        T value = initialValue();
        // 获取当前线程。
        Thread t = Thread.currentThread();
        // 获取该线程对象的 threadLocals 属性。
        ThreadLocalMap map = getMap(t);
        // 如果不为空,则将 threadLocal:value 存入线程对象的 threadLocals 属性中。
        if (map != null)
            map.set(this, value);
        else
        	// 否则初始化线程对象的 threadLocals,然后将 threadLocal:value 键值对存入线程对象的threadLocals 属性中。
            createMap(t, value);
        return value;
    }
/**
*
* 初始化线程对象的 threadLocals,然后将 threadLocal:value 键值对存入线程对象的threadLocals 属性中。
*/
  void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }


remove

     public void remove() {
      // 获取该线程对象的 threadLocals 属性。
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
         // 移除
             m.remove(this);
     }

缺陷

ThreadLocal 无法在父子线程之间传递, 看源码我们也知道了,都是Thread.currentThread.

那我们来证明下吧

public class ThreadLocalTest {


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

    public static void main(String[] args) {

        ThreadLocalTest threadLocalTest = new ThreadLocalTest();

        threadLocal.set("artisan Test");

        doSomething();

    }

    private static void doSomething() {
        System.out.println("threadLocal中的对象:" + threadLocal.get());

        new Thread(()->{
            System.out.println("开启子线程");
            System.out.println("子线程中获取threadLocal:" + threadLocal.get());
        }).start();

    }
}
    


InheritableThreadLocal

由于 ThreadLocal 在父子线程交互中子线程无法访问到存储在父线程中的值,无法满足某些场景的需求,比如链路跟踪

为了解决上述问题,JDK 引入了 InheritableThreadLocal,即子线程可以访问父线程中的线程本地变量,准确的说是子线程可以访问在创建子线程时父线程当时的本地线程变量,其实现原理是在创建子线程将父线程当前存在的本地线程变量拷贝到子线程的本地线程变量中。

源码解析

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
	.....
}

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

  /**
     * Get the map associated with a ThreadLocal.
     *
     * @param t the current thread
     */
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

和 ThreadLocal比一比

可以知道 ThreadLocal 操作的是 Thread 对象的 threadLocals 属性,而 InheritableThreadLocal 操作的是 Thread 对象的 inheritableThreadLocals 属性


   /**
     * Create the map associated with a ThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the table.
     */
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }

createMap 被执行的条件是调用 InheritableThreadLocal#get、set 时如果线程的inheritableThreadLocals 属性为空时才会被调用

咦 ,看到这里没啥用啊

InheritableThreadLocal 是如何继承自父对象的线程本地变量的呢?

那就得看 Thread#init 方法

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name;

		//  获取当前线程对象,即待创建的线程的父线程。
        Thread parent = currentThread();
          
         ........
         ........
         ........
        
        // 如果父线程的 inheritableThreadLocals 不为空并且 inheritThreadLocals 为 true(该值默认为true),则使用父线程的 inherit 本地变量的值来创建子线程的 inheritableThreadLocals 结构,即将父线程中的本地变量复制到子线程中
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;

        /* Set thread ID */
        tid = nextThreadID();
    }
 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++;
                    }
                }
            }
        }

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

子线程默认拷贝父线程的方式是浅拷贝,如果需要使用深拷贝,需要使用自定义ThreadLocal,继承 InheritableThreadLocal 并重写 childValue 方法


那来验证下吧

public class InheritableThreadLocalTest {


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

    public static void main(String[] args) {

        InheritableThreadLocalTest threadLocalTest = new InheritableThreadLocalTest();

        threadLocal.set("artisan InheritableThreadLocal");

        doSomething();

    }

    private static void doSomething() {
        System.out.println("threadLocal中的对象:" + threadLocal.get());

        new Thread(()->{
            System.out.println("开启子线程");
            System.out.println("子线程中获取threadLocal:" + threadLocal.get());
        }).start();

    }
}
    


符合预期,在子线程中如愿访问到了在主线程中设置的本地环境变量。


局限性

InheritableThreadLocal 支持子线程访问在父线程的核心思想是在创建线程的时候将父线程中的本地变量值复制到子线程,即复制的时机为创建子线程时。

线程池能够复用线程,减少线程的频繁创建与销毁,如果使用 InheritableThreadLocal,那么线程池中的线程拷贝的数据来自于第一个提交任务的外部线程,即后面的外部线程向线程池中提交任务时,子线程访问的本地变量都来源于第一个外部线程,造成线程本地变量混乱

看个代码

package com.artisan.threadlocal;

import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;
 
public class Service {

    /**

     * 模拟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) {

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

            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("parentThreadName:" + parentThreadName + ":" + requestIdThreadLocal.get());

        }

    }
}
    


子线程中出现出现了线程本地变量混乱的现象


TransmittableThreadLocal

TransmittableThreadLocal 是什么

TransmittableThreadLocal 是阿里巴巴开源的专门解决 InheritableThreadLocal 的局限性,实现线程本地变量在线程池的执行过程中,能正常的访问父线程设置的线程变量。


实现原理

从InheritableThreadLocal 不支持线程池的根本原因是 InheritableThreadLocal 是在父线程创建子线程时复制的,由于线程池的复用机制,“子线程”只会复制一次。要支持线程池中能访问提交任务线程的本地变量,其实只需要在父线程在向线程池提交任务时复制父线程的上下环境,那在子线程中就能够如愿访问到父线程中的本地遍历,实现本地环境变量在线程调用之中的透传,实现链路跟踪,这也就是 TransmittableThreadLocal 最本质的实现原理。

TransmittableThreadLocal 继承自 InheritableThreadLocal,接下来将从 set 方法为入口,开始探究TransmittableThreadLocal 实现原理

    @Override
    public final void set(T value) {
    	// 首先调用父类的 set 方法,将 value 存入线程本地遍历,即 Thread 对象的inheritableThreadLocals 中
        super.set(value);
        // may set null to remove value   如果 value 为空,则调用 removeValue() 否则调用 addValue。
        if (null == value) removeValue();
        else addValue();
    }

  private void addValue() {
  	// 当前线程在调用 threadLocal 方法的 set 方法(即向线程本地遍历存储数据时),如果需要设置的值不为 null,则调用 addValue 方法,将当前 ThreadLocal 存储到 TransmittableThreadLocal 的全局静态变量 holder。
        if (!holder.get().containsKey(this)) { 
            holder.get().put(this, null); // WeakHashMap supports null value.
        }
    }
   private void removeValue() {
        holder.get().remove(this);
    }

看看holder

   private static InheritableThreadLocal<Map<TransmittableThreadLocal<?>, ?>> holder =
            new InheritableThreadLocal<Map<TransmittableThreadLocal<?>, ?>>() {
                @Override
                protected Map<TransmittableThreadLocal<?>, ?> initialValue() {
                    return new WeakHashMap<以上是关于每日一博 - ThreadLocal VS InheritableThreadLocal VS TransmittableThreadLocal的主要内容,如果未能解决你的问题,请参考以下文章

每日一博 - instanceof vs isInstance vs isAssignableFrom

每日一博 - 延时任务的多种实现方式解读

每日一博 - 3D架构图 cloudcraft

每日一博 - Devops全流程

每日一博 - Devops全流程

Java Review - 线程池中使用ThreadLocal不当导致的内存泄漏案例&源码分析