java中,main线程ThreadLocal的值怎么传递到线程池内?

java中,main线程ThreadLocal的值怎么传递到线程池内?

main线程中定义了一个ThreadLocal变量ThreadLocal<String> USER_THREAD_LOCAL = new ThreadLocal<>(),并且调用USER_THREAD_LOCAL.set("zhangsan"),此时通过线程池调用,线程池内部调用USER_THREAD_LOCAL.get()得到什么?

使用TransmittableThreadLocal一招搞定,不过ThreadLocal还有其他的坑,也需要注意一下。

前一段时间,有同事使用ThreadLocal踩坑了,正好引起了我的兴趣。

所以近期,我抽空把ThreadLocal的源码再研究了一下,越看越有意思,发现里面的东西还真不少。

我把精华浓缩了一下,汇集成了下面11个问题,看看你能顶住第几个?

最近无意间获得一份BAT大厂大佬写的刷题笔记,一下子打通了我的任督二脉,越来越觉得算法没有想象中那么难了。

[BAT大佬写的刷题笔记,让我offer拿到手软](这位BAT大佬写的Leetcode刷题笔记,让我offer拿到手软)

1. 为什么要用ThreadLocal?

并发编程是一项非常重要的技术,它让我们的程序变得更加高效。

但在并发的场景中,如果有多个线程同时修改公共变量,可能会出现线程安全问题,即该变量最终结果可能出现异常。

为了解决线程安全问题,JDK出现了很多技术手段,比如:使用synchronized或Lock,给访问公共资源的代码上锁,保证了代码的原子性。

但在高并发的场景中,如果多个线程同时竞争一把锁,这时会存在大量的锁等待,可能会浪费很多时间,让系统的响应时间一下子变慢。

因此,JDK还提供了另外一种用空间换时间的新思路:ThreadLocal。

它的核心思想是:共享变量在每个线程都有一个副本,每个线程操作的都是自己的副本,对另外的线程没有影响。

例如:

@Service
public class ThreadLocalService {
    private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

    public void add() {
        threadLocal.set(1);
        doSamething();
        Integer integer = threadLocal.get();
    }
}

2. ThreadLocal的原理是什么?

为了搞清楚ThreadLocal的底层实现原理,我们不得不扒一下源码。

ThreadLocal的内部有一个静态的内部类叫:ThreadLocalMap

public class ThreadLocal<T> {
     ...
     public T get() {
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取当前线程的成员变量ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //根据threadLocal对象从map中获取Entry对象
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                //获取保存的数据
                T result = (T)e.value;
                return result;
            }
        }
        //初始化数据
        return setInitialValue();
    }
    
    private T setInitialValue() {
        //获取要初始化的数据
        T value = initialValue();
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取当前线程的成员变量ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        //如果map不为空
        if (map != null)
            //将初始值设置到map中,key是this,即threadLocal对象,value是初始值
            map.set(this, value);
        else
           //如果map为空,则需要创建新的map对象
            createMap(t, value);
        return value;
    }
    
    public void set(T value) {
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取当前线程的成员变量ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        //如果map不为空
        if (map != null)
            //将值设置到map中,key是this,即threadLocal对象,value是传入的value值
            map.set(this, value);
        else
           //如果map为空,则需要创建新的map对象
            createMap(t, value);
    }
    
     static class ThreadLocalMap {
        ...
     }
     ...
}

ThreadLocal的get方法、set方法和setInitialValue方法,其实最终操作的都是ThreadLocalMap类中的数据。

其中ThreadLocalMap类的内部如下:

static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
   }
   ...
   private Entry[] table;
   ...
}

ThreadLocalMap里面包含一个静态的内部类Entry,该类继承于WeakReference类,说明Entry是一个弱引用。

ThreadLocalMap内部还包含了一个Entry数组,其中:Entry = ThreadLocal + value。

而ThreadLocalMap被定义成了Thread类的成员变量。

public class Thread implements Runnable {
    ...
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

下面用一张图从宏观上,认识一下ThreadLocal的整体结构:

从上图中看出,在每个Thread类中,都有一个ThreadLocalMap的成员变量,该变量包含了一个Entry数组,该数组真正保存了ThreadLocal类set的数据。

Entry是由threadLocal和value组成,其中threadLocal对象是弱引用,在GC的时候,会被自动回收。而value就是ThreadLocal类set的数据。

下面用一张图总结一下引用关系:

上图中除了Entry的key对ThreadLocal对象是弱引用,其他的引用都是强引用。

需要特别说明的是,上图中ThreadLocal对象我画到了堆上,其实在实际的业务场景中不一定在堆上。因为如果ThreadLocal被定义成了static的,ThreadLocal的对象是类共用的,可能出现在方法区。

3. 为什么用ThreadLocal做key?

不知道你有没有思考过这样一个问题:ThreadLocalMap为什么要用ThreadLocal做key,而不是用Thread做key?

如果在你的应用中,一个线程中只使用了一个ThreadLocal对象,那么使用Thread做key也未尝不可。

@Service
public class ThreadLocalService {
    private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
}    

但实际情况中,你的应用,一个线程中很有可能不只使用了一个ThreadLocal对象。这时使用Thread做key不就出有问题?

@Service
public class ThreadLocalService {
    private static final ThreadLocal<Integer> threadLocal1 = new ThreadLocal<>();
    private static final ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>();
    private static final ThreadLocal<Integer> threadLocal3 = new ThreadLocal<>();
}    

假如使用Thread做key时,你的代码中定义了3个ThreadLocal对象,那么,通过Thread对象,它怎么知道要获取哪个ThreadLocal对象呢?

如下图所示:

 

因此,不能使用Thread做key,而应该改成用ThreadLocal对象做key,这样才能通过具体ThreadLocal对象的get方法,轻松获取到你想要的ThreadLocal对象。

如下图所示:

 

4. Entry的key为什么设计成弱引用?

前面说过,Entry的key,传入的是ThreadLocal对象,使用了WeakReference对象,即被设计成了弱引用。

那么,为什么要这样设计呢?

假如key对ThreadLocal对象的弱引用,改为强引用。

我们都知道ThreadLocal变量对ThreadLocal对象是有强引用存在的。

即使ThreadLocal变量生命周期完了,设置成null了,但由于key对ThreadLocal还是强引用。

此时,如果执行该代码的线程使用了线程池,一直长期存在,不会被销毁。

就会存在这样的强引用链:Thread变量 -> Thread对象 -> ThreadLocalMap -> Entry -> key -> ThreadLocal对象。

那么,ThreadLocal对象和ThreadLocalMap都将不会被GC回收,于是产生了内存泄露问题。

为了解决这个问题,JDK的开发者们把Entry的key设计成了弱引用。

弱引用的对象,在GC做垃圾清理的时候,就会被自动回收了。

如果key是弱引用,当ThreadLocal变量指向null之后,在GC做垃圾清理的时候,key会被自动回收,其值也被设置成null。

如下图所示:

接下来,最关键的地方来了。

由于当前的ThreadLocal变量已经被指向null了,但如果直接调用它的get、set或remove方法,很显然会出现空指针异常。因为它的生命已经结束了,再调用它的方法也没啥意义。

此时,如果系统中还定义了另外一个ThreadLocal变量b,调用了它的get、set或remove,三个方法中的任何一个方法,都会自动触发清理机制,将key为null的value值清空。

如果key和value都是null,那么Entry对象会被GC回收。如果所有的Entry对象都被回收了,ThreadLocalMap也会被回收了。

这样就能最大程度的解决内存泄露问题。

需要特别注意的地方是:

  1. key为null的条件是,ThreadLocal变量指向null,并且key是弱引用。如果ThreadLocal变量没有断开对ThreadLocal的强引用,即ThreadLocal变量没有指向null,GC就贸然的把弱引用的key回收了,不就会影响正常用户的使用?
  2. 如果当前ThreadLocal变量指向null了,并且key也为null了,但如果没有其他ThreadLocal变量触发get、set或remove方法,也会造成内存泄露。

下面看看弱引用的例子:

public static void main(String[] args) {
    WeakReference<Object> weakReference0 = new WeakReference<>(new Object());
    System.out.println(weakReference0.get());
    System.gc();
    System.out.println(weakReference0.get());
}

打印结果:

java.lang.Object@1ef7fe8e
null

传入WeakReference构造方法的是直接new处理的对象,没有其他引用,在调用gc方法后,弱引用对象会被自动回收。

但如果出现下面这种情况:

public static void main(String[] args) {
    Object object = new Object();
    WeakReference<Object> weakReference1 = new WeakReference<>(object);
    System.out.println(weakReference1.get());
    System.gc();
    System.out.println(weakReference1.get());
}

执行结果:

java.lang.Object@1ef7fe8e
java.lang.Object@1ef7fe8e

先定义了一个强引用object对象,在WeakReference构造方法中将object对象的引用作为参数传入。这时,调用gc后,弱引用对象不会被自动回收。

我们的Entry对象中的key不就是第二种情况吗?在Entry构造方法中传入的是ThreadLocal对象的引用。

如果将object强引用设置为null:

public static void main(String[] args) {
    Object object = new Object();
    WeakReference<Object> weakReference1 = new WeakReference<>(object);
    System.out.println(weakReference1.get());
    System.gc();
    System.out.println(weakReference1.get());

    object=null;
    System.gc();
    System.out.println(weakReference1.get());
}

执行结果:

java.lang.Object@6f496d9f
java.lang.Object@6f496d9f
null

第二次gc之后,弱引用能够被正常回收。

由此可见,如果强引用和弱引用同时关联一个对象,那么这个对象是不会被GC回收。也就是说这种情况下Entry的key,一直都不会为null,除非强引用主动断开关联。

此外,你可能还会问这样一个问题:Entry的value为什么不设计成弱引用?

答:Entry的value假如只是被Entry引用,有可能没被业务系统中的其他地方引用。如果将value改成了弱引用,被GC贸然回收了(数据突然没了),可能会导致业务系统出现异常。

而相比之下,Entry的key,管理的地方就非常明确了。

这就是Entry的key被设计成弱引用,而value没被设计成弱引用的原因。

 

最近就业形式比较困难,为了感谢各位小伙伴对苏三一直以来的支持,我特地创建了一些工作内推群, 看看能不能帮助到大家。

你可以在群里发布招聘信息,也可以内推工作,也可以在群里投递简历找工作,也可以在群里交流面试或者工作的话题。

加微信:su_san_java,备注:知乎内推,即可加入该群。我不敢100%能够保证你找到合适的工作,但至少有一个跟同城市的同行交流的机会。

5. ThreadLocal真的会导致内存泄露?

通过上面的Entry对象中的key设置成弱引用,并且使用get、set或remove方法清理key为null的value值,就能彻底解决内存泄露问题?

答案是否定的。

如下图所示:

假如ThreadLocalMap中存在很多key为null的Entry,但后面的程序,一直都没有调用过有效的ThreadLocal的get、set或remove方法。

那么,Entry的value值一直都没被清空。

所以会存在这样一条强引用链:Thread变量 -> Thread对象 -> ThreadLocalMap -> Entry -> value -> Object。

其结果就是:Entry和ThreadLocalMap将会长期存在下去,会导致内存泄露。

6. 如何解决内存泄露问题?

前面说过的ThreadLocal还是会导致内存泄露的问题,我们有没有解决办法呢?

答:有办法,调用ThreadLocal对象的remove方法。

不是在一开始就调用remove方法,而是在使用完ThreadLocal对象之后。列如:

先创建一个CurrentUser类,其中包含了ThreadLocal的逻辑。

public class CurrentUser {
    private static final ThreadLocal<UserInfo> THREA_LOCAL = new ThreadLocal();
    
    public static void set(UserInfo userInfo) {
        THREA_LOCAL.set(userInfo);
    }
    
    public static UserInfo get() {
       THREA_LOCAL.get();
    }
    
    public static void remove() {
       THREA_LOCAL.remove();
    }
}

然后在业务代码中调用相关方法:

public void doSamething(UserDto userDto) {
   UserInfo userInfo = convert(userDto);
   
   try{
     CurrentUser.set(userInfo);
     ...
     
     //业务代码
     UserInfo userInfo = CurrentUser.get();
     ...
   } finally {
      CurrentUser.remove();
   }
}

需要我们特别注意的地方是:一定要在finally代码块中,调用remove方法清理没用的数据。如果业务代码出现异常,也能及时清理没用的数据。

remove方法中会把Entry中的key和value都设置成null,这样就能被GC及时回收,无需触发额外的清理机制,所以它能解决内存泄露问题。

7. ThreadLocal是如何定位数据的?

前面说过ThreadLocalMap对象底层是用Entry数组保存数据的。

那么问题来了,ThreadLocal是如何定位Entry数组数据的?

在ThreadLocal的get、set、remove方法中都有这样一行代码:

int i = key.threadLocalHashCode & (len-1);

通过key的hashCode值,与数组的长度减1。其中key就是ThreadLocal对象,与数组的长度减1,相当于除以数组的长度减1,然后取模。

这是一种hash算法。

接下来给大家举个例子:假设len=16,key.threadLocalHashCode=31,

于是: int i = 31 & 15 = 15

相当于:int i = 31 % 16 = 15

计算的结果是一样的,但是使用与运算效率跟高一些。

为什么与运算效率更高?

答:因为ThreadLocal的初始大小是16,每次都是按2倍扩容,数组的大小其实一直都是2的n次方。这种数据有个规律就是高位是0,低位都是1。在做与运算时,可以不用考虑高位,因为与运算的结果必定是0。只需考虑低位的与运算,所以效率更高。

如果使用hash算法定位具体位置的话,就可能会出现hash冲突的情况,即两个不同的hashCode取模后的值相同。

ThreadLocal是如何解决hash冲突的呢?

我们看看getEntry是怎么做的:

private Entry getEntry(ThreadLocal<?> key) {
    //通过hash算法获取下标值
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    //如果下标位置上的key正好是我们所需要寻找的key
    if (e != null && e.get() == key)
        //说明找到数据了,直接返回
        return e;
    else
        //说明出现hash冲突了,继续往后找
        return getEntryAfterMiss(key, i, e);
}

再看看getEntryAfterMiss方法:

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    //判断Entry对象如果不为空,则一直循环
    while (e != null) {
        ThreadLocal<?> k = e.get();
        //如果当前Entry的key正好是我们所需要寻找的key
        if (k == key)
            //说明这次真的找到数据了
            return e;
        if (k == null)
            //如果key为空,则清理脏数据
            expungeStaleEntry(i);
        else
            //如果还是没找到数据,则继续往后找
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

关键看看nextIndex方法:

private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

当通过hash算法计算出的下标小于数组大小,则将下标值加1。否则,即下标大于等于数组大小,下标变成0了。下标变成0之后,则循环一次,下标又变成1。。。

寻找的大致过程如下图所示:

如果找到最后一个,还是没有找到,则再从头开始找。

不知道你有没有发现,它构成了一个:环形。

ThreadLocal从数组中找数据的过程大致是这样的:

  1. 通过key的hashCode取余计算出一个下标。
  2. 通过下标,在数组中定位具体Entry,如果key正好是我们所需要的key,说明找到了,则直接返回数据。
  3. 如果第2步没有找到我们想要的数据,则从数组的下标位置,继续往后面找。
  4. 如果第3步中找key的正好是我们所需要的key,说明找到了,则直接返回数据。
  5. 如果还是没有找到数据,再继续往后面找。如果找到最后一个位置,还是没有找到数据,则再从头,即下标为0的位置,继续从前往后找数据。
  6. 直到找到第一个Entry为空为止。

8. ThreadLocal是如何扩容的?

从上面得知,ThreadLocal的初始大小是16。那么问题来了,ThreadLocal是如何扩容的?

在set方法中会调用rehash方法:

private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);

    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) {
            e.value = value;
            return;
        }

        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

注意一下,其中有个判断条件是:sz(之前的size+1)如果大于或等于threshold的话,则调用rehash方法。

threshold默认是0,在创建ThreadLocalMap时,调用它的构造方法:

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

调用setThreshold方法给threshold设置一个值,而这个值INITIAL_CAPACITY是默认的大小16。

private void setThreshold(int len) {
    threshold = len * 2 / 3;
}

也就是第一次设置的threshold = 16 * 2 / 3, 取整后的值是:10。

换句话说当sz大于等于10时,就可以考虑扩容了。

rehash代码如下:

private void rehash() {
    //先尝试回收一次key为null的值,腾出一些空间
    expungeStaleEntries();

    if (size >= threshold - threshold / 4)
        resize();
}

在真正扩容之前,先尝试回收一次key为null的值,腾出一些空间。

如果回收之后的size大于等于threshold的3/4时,才需要真正的扩容。

计算公式如下:

16 * 2 * 4 / 3 * 4 - 16 * 2 / 3 * 4 = 8

也就是说添加数据后,新的size大于等于老size的1/2时,才需要扩容。

private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    //按2倍的大小扩容
    int newLen = oldLen * 2;
    Entry[] newTab = new Entry[newLen];
    int count = 0;

    for (int j = 0; j < oldLen; ++j) {
        Entry e = oldTab[j];
        if (e != null) {
            ThreadLocal<?> k = e.get();
            if (k == null) {
                e.value = null; // Help the GC
            } else {
                int h = k.threadLocalHashCode & (newLen - 1);
                while (newTab[h] != null)
                    h = nextIndex(h, newLen);
                newTab[h] = e;
                count++;
            }
        }
    }

    setThreshold(newLen);
    size = count;
    table = newTab;
}

resize中每次都是按2倍的大小扩容。

扩容的过程如下图所示:

扩容的关键步骤如下:

  1. 老size + 1 = 新size
  2. 如果新size大于等于老size的2/3时,需要考虑扩容。
  3. 扩容前先尝试回收一次key为null的值,腾出一些空间。
  4. 如果回收之后发现size还是大于等于老size的1/2时,才需要真正的扩容。
  5. 每次都是按2倍的大小扩容。

9. 父子线程如何共享数据?

前面介绍的ThreadLocal都是在一个线程中保存和获取数据的。

但在实际工作中,有可能是在父子线程中共享数据的。即在父线程中往ThreadLocal设置了值,在子线程中能够获取到。

例如:

public class ThreadLocalTest {

    public static void main(String[] args) {
        ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
        threadLocal.set(6);
        System.out.println("父线程获取数据:" + threadLocal.get());

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

执行结果:

父线程获取数据:6
子线程获取数据:null

你会发现,在这种情况下使用ThreadLocal是行不通的。main方法是在主线程中执行的,相当于父线程。在main方法中开启了另外一个线程,相当于子线程。

显然通过ThreadLocal,无法在父子线程中共享数据。

那么,该怎么办呢?

答:使用InheritableThreadLocal,它是JDK自带的类,继承了ThreadLocal类。

修改代码之后:

public class ThreadLocalTest {

    public static void main(String[] args) {
        InheritableThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>();
        threadLocal.set(6);
        System.out.println("父线程获取数据:" + threadLocal.get());

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

执行结果:

父线程获取数据:6
子线程获取数据:6

果然,在换成InheritableThreadLocal之后,在子线程中能够正常获取父线程中设置的值。

其实,在Thread类中除了成员变量threadLocals之外,还有另一个成员变量:inheritableThreadLocals。

Thread类的部分代码如下:

ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

最关键的一点是,在它的init方法中会将父线程中往ThreadLocal设置的值,拷贝一份到子线程中。

感兴趣的小伙伴,可以找我私聊。或者看看我后面的文章,后面还会有专栏。

10. 线程池中如何共享数据?

在真实的业务场景中,一般很少用单独的线程,绝大多数,都是用的线程池。

那么,在线程池中如何共享ThreadLocal对象生成的数据呢?

因为涉及到不同的线程,如果直接使用ThreadLocal,显然是不合适的。

我们应该使用InheritableThreadLocal,具体代码如下:

private static void fun1() {
    InheritableThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>();
    threadLocal.set(6);
    System.out.println("父线程获取数据:" + threadLocal.get());

    ExecutorService executorService = Executors.newSingleThreadExecutor();

    threadLocal.set(6);
    executorService.submit(() -> {
        System.out.println("第一次从线程池中获取数据:" + threadLocal.get());
    });

    threadLocal.set(7);
    executorService.submit(() -> {
        System.out.println("第二次从线程池中获取数据:" + threadLocal.get());
    });
}

执行结果:

父线程获取数据:6
第一次从线程池中获取数据:6
第二次从线程池中获取数据:6

由于这个例子中使用了单例线程池,固定线程数是1。

第一次submit任务的时候,该线程池会自动创建一个线程。因为使用了InheritableThreadLocal,所以创建线程时,会调用它的init方法,将父线程中的inheritableThreadLocals数据复制到子线程中。所以我们看到,在主线程中将数据设置成6,第一次从线程池中获取了正确的数据6。

之后,在主线程中又将数据改成7,但在第二次从线程池中获取数据却依然是6。

因为第二次submit任务的时候,线程池中已经有一个线程了,就直接拿过来复用,不会再重新创建线程了。所以不会再调用线程的init方法,所以第二次其实没有获取到最新的数据7,还是获取的老数据6。

那么,这该怎么办呢?

答:使用TransmittableThreadLocal,它并非JDK自带的类,而是阿里巴巴开源jar包中的类。

可以通过如下pom文件引入该jar包:

<dependency>
   <groupId>com.alibaba</groupId>
   <artifactId>transmittable-thread-local</artifactId>
   <version>2.11.0</version>
   <scope>compile</scope>
</dependency>

代码调整如下:

private static void fun2() throws Exception {
    TransmittableThreadLocal<Integer> threadLocal = new TransmittableThreadLocal<>();
    threadLocal.set(6);
    System.out.println("父线程获取数据:" + threadLocal.get());

    ExecutorService ttlExecutorService = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));

    threadLocal.set(6);
    ttlExecutorService.submit(() -> {
        System.out.println("第一次从线程池中获取数据:" + threadLocal.get());
    });

    threadLocal.set(7);
    ttlExecutorService.submit(() -> {
        System.out.println("第二次从线程池中获取数据:" + threadLocal.get());
    });

}

执行结果:

父线程获取数据:6
第一次从线程池中获取数据:6
第二次从线程池中获取数据:7

我们看到,使用了TransmittableThreadLocal之后,第二次从线程中也能正确获取最新的数据7了。

nice。

如果你仔细观察这个例子,你可能会发现,代码中除了使用TransmittableThreadLocal类之外,还使用了TtlExecutors.getTtlExecutorService方法,去创建ExecutorService对象。

这是非常重要的地方,如果没有这一步,TransmittableThreadLocal在线程池中共享数据将不会起作用。

创建ExecutorService对象,底层的submit方法会TtlRunnable或TtlCallable对象。

以TtlRunnable类为例,它实现了Runnable接口,同时还实现了它的run方法:

public void run() {
    Map<TransmittableThreadLocal<?>, Object> copied = (Map)this.copiedRef.get();
    if (copied != null && (!this.releaseTtlValueReferenceAfterRun || this.copiedRef.compareAndSet(copied, (Object)null))) {
        Map backup = TransmittableThreadLocal.backupAndSetToCopied(copied);

        try {
            this.runnable.run();
        } finally {
            TransmittableThreadLocal.restoreBackup(backup);
        }
    } else {
        throw new IllegalStateException("TTL value reference is released after run!");
    }
}

这段代码的主要逻辑如下:

  1. 把当时的ThreadLocal做个备份,然后将父类的ThreadLocal拷贝过来。
  2. 执行真正的run方法,可以获取到父类最新的ThreadLocal数据。
  3. 从备份的数据中,恢复当时的ThreadLocal数据。

11. ThreadLocal有哪些用途?

最后,一起聊聊ThreadLocal有哪些用途?

老实说,使用ThreadLocal的场景挺多的。

下面列举几个常见的场景:

  1. 在spring事务中,保证一个线程下,一个事务的多个操作拿到的是一个Connection。
  2. 在hiberate中管理session。
  3. 在JDK8之前,为了解决SimpleDateFormat的线程安全问题。
  4. 获取当前登录用户上下文。
  5. 临时保存权限数据。
  6. 使用MDC保存日志信息。

等等,还有很多业务场景,这里就不一一列举了。

由于篇幅有限,今天的内容先分享到这里。希望你看了这篇文章,会有所收获。

接下来留几个问题给大家思考一下:

  1. ThreadLocal变量为什么建议要定义成static的?
  2. Entry数组为什么要通过hash算法计算下标,即直线寻址法,而不直接使用下标值?
  3. 强引用和弱引用有什么区别?
  4. Entry数组大小,为什么是2的N次方?
  5. 使用InheritableThreadLocal时,如果父线程中重新set值,在子线程中能够正确的获取修改后的新值吗?

敬请期待我的下一篇文章,谢谢。

最近无意间获得一份阿里大佬写的刷题笔记,一下子打通了我的任督二脉,进大厂原来没那么难。

链接:pan.baidu.com/s/1UECE5y 密码:bhbe

 

得到 null

理解 ThreadLocal 最好的方式是把它想像成这是一个线程安全的 Map<Thread, XX>, 操作 put、get、remove 的时候, key 是自身的 Thread(它实际的实现是更近一步, 把 XX 放到了 Thread 里面), 当代码分了很多层(拦截器, 过滤器, 各种反射, 以及各种 web service repository 等)的时候, 只要线程是同一个就可以用来共享值, 它作用于所有的 Thread, 用 static final 修饰才符合直觉, 将 ThreadLocal 当成方法里的局部变量或者类里面的属性变量(单例就无所谓了)会很不伦不类. 你完全可以用类似下面的方式来达到 ThreadLocal 同样的效果(不适用于虚拟线程, 当然, 如果能获取到虚拟线程的标识, 将其做为 Map 的 key 道理是一样的)

public class ShareValue<T> {

    private final Map<Thread, T> shareMap = new ConcurrentHashMap<>();

    public void put(T value) {
        shareMap.put(Thread.currentThread(), value);
    }
    public T get() {
        return shareMap.get(Thread.currentThread());
    }
    public void remove() {
        shareMap.remove(Thread.currentThread());
    }
}


// 在其他用的地方
private static final ShareValue<String> SHARE_STRING = new ShareValue<>();
private static final ShareValue<XXX> SHARE_XXX = new ShareValue<>();

 


但是在构建线程池的时候, 可以用 ThreadFactory 将父线程的数据传递到子线程去

private static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();

public static void main(String[] args) {
    try {
        THREAD_LOCAL.set("张三");

        System.out.println("主: " + Thread.currentThread().getName() + " : " + THREAD_LOCAL.get());
        System.out.println("-------------");

        {
            String tn = "子 1";
            List<Future<?>> futureList = new ArrayList<>();
            // 线程池正常也应该用 static final 来修饰
            ExecutorService es = Executors.newFixedThreadPool(2);
            for (int i = 0; i < 5; i++) {
                futureList.add(es.submit(() -> {
                    System.out.println(tn + " : " + Thread.currentThread().getName() + " : " + THREAD_LOCAL.get());
                }));
            }
            for (Future<?> future : futureList) {
                try {
                    future.get();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }

        System.out.println("-------------");

        {
            String tn = "子 2";
            List<Future<?>> futureList = new ArrayList<>();
            // 跟上面的 子 1 相比, 就多了这个 factory
            ThreadFactory threadFactory = r -> {
                String value = THREAD_LOCAL.get(); // 现在的作用域还在主线程
                // value = ""; // 如果这一句解开, value 是可变变量, 编译就不会通过了
                return new Thread(() -> {
                    // 这里的 get() 跟上面的 <子 1> 部分是一样的, 都是空
                    System.out.println(tn + " : " + Thread.currentThread().getName() + " : " + value + " : " + THREAD_LOCAL.get());
                    try {
                        THREAD_LOCAL.set(value); // 重点就在这里, 当前的作用域是新线程, 所以上面的 get() 也是空的, 所以需要从父线程的不可变变量再设置进当前线程
                        r.run();
                    } finally {
                        THREAD_LOCAL.remove(); // 要养成清理的好习惯
                    }
                });
            };
            ExecutorService es = Executors.newFixedThreadPool(2, threadFactory); // 跟上面 子 1 相比, 只多了这里

            for (int i = 0; i < 5; i++) {
                futureList.add(es.submit(() -> {
                    System.out.println(tn + " : " + Thread.currentThread().getName() + " : " + THREAD_LOCAL.get());
                }));
            }
            for (Future<?> future : futureList) {
                try {
                    future.get();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
        System.out.println("-------------");
        System.out.println("主: " + Thread.currentThread().getName() + " : " + THREAD_LOCAL.get());
    } finally {
        THREAD_LOCAL.remove(); // 要养成清理的好习惯
    }
}

自定义的线程池或者 spring 的 @Async 注解都可以通过这种方式, 将父线程中的日志或请求上下文带去子线程, 注意别忘了清理 https://moelholm.com/blog/2017/07/24/spring-43-using-a-taskdecorator-to-copy-mdc-data-to-async-threads

 

从 jdk-20 https://openjdk.org/projects/jdk/20/ 开始出了 scope values , 20 开始孵化, 21 预览, 用法上更简单, 直接指定作用域生效, 虚拟机会做好清理

 

java21了~还用啥TL

使用ScopedValue+StructuredTaskScope 就可以实现了。

 

看了答主在其他回答下的问题,我大概率知道题主为啥说InheritableThreadLocal 在线程池中拿到的不一致了。

InheritableThreadLocal<Integer> local = new InheritableThreadLocal<>();
local.set(1);
System.out.println("main local is:" + local.get());
ExecutorService executor = Executors.newSingleThreadExecutor(r -> new Thread(
        Thread.currentThread().getThreadGroup(), r, "pool-thread", 0, true));
Runnable poolAction = () -> System.out.println("pool local is:" + local.get());
Future<?> future = executor.submit(poolAction);
future.get();
local.set(2);
future = executor.submit(poolAction);
future.get();
System.out.println("main local is:" + local.get());
executor.shutdown();

output:

main local is:1
pool local is:1
pool local is:1
main local is:2

请看如上代码,在main线程更改local的值之后,pool并没有随之更新。

解决方案也真的非常简单,将<Integer>改成<AtomicInteger>或者<AtomicReference<T>>即可解决问题。

 

AtomicReference我给个示例吧

InheritableThreadLocal<AtomicReference<String>> local = new InheritableThreadLocal<>();
local.set(new AtomicReference<>("首次"));
local.get().set("二次");

只列出有改动的地方,其他地方省略。

 

  1. 你得先了解Threadlocal是什么; Threadlocal是当前线程的上下文
  2. 你得了解线程池是什么; 线程池中的线程有的是新创建的, 有的是过去创建还在存活的, 还可能涉及reactor, 在这些不同的情况下; 池内线程如何获取另外一个线程的上下文需要考虑的都不一样

然后

  1. 建议看InheritableThreadLocal
  2. 不建议看阿里的ttlThreadlocal, 阿里的ttlThreadlocal可能会给新人产生误解, 因为这玩意不单单是一个Threadlocal, 你一不留神用错了, 会带来很多问题;

 

参考了阿里的TTLThreadlocal和netflix的context, 重构的一个简化的ttlThreadlocal

/**
 * ttl threadLocal
 *
 * @author kael
 * @since 2020年03月20日 10:03:13
 */
public class JaThreadLocal<T> {

    /**
     * key : thread local hashcode, value : thread local value
     */
    static final ThreadLocal<Map<Integer, Object>> delegate = new ThreadLocal<>();
    /**
     * 异步线程传递 ThreadLocal 信息时允许添加业务钩子
     */
    private static final Map<Integer, JaFunctionP<Object>> bizHandlerMap = new ConcurrentHashMap<>();

    private final int hashCode;

    public JaThreadLocal() {
        this.hashCode = hashCode();
        init();
    }

    public JaThreadLocal(JaFunctionP<Object> bizHandler) {
        this();
        bizHandlerMap.put(hashCode, bizHandler);
    }

    public void set(T t) {
        Map<Integer, Object> map = init();
        map.put(hashCode, t);
    }

    @SuppressWarnings("unchecked")
    public T get() {
        Map<Integer, Object> map = delegate.get();
        return null == map ? null : (T) map.get(hashCode);
    }

    public void remove() {
        Map<Integer, Object> map = delegate.get();
        if (null != map) {
            map.remove(hashCode);
        }
    }

    private Map<Integer, Object> init() {
        Map<Integer, Object> map = delegate.get();
        if (null == map) {
            map = new HashMap<>();
            delegate.set(map);
        }
        return map;
    }

    /**
     * can transmit value from the thread of submitting task to the thread of executing task.
     */
    static class Transmit {

        static Map<Integer, Object> copy() {
            Map<Integer, Object> parent = delegate.get();
            return JaCollectionUtil.isEmpty(parent) ? new HashMap<>(0) : new HashMap<>(parent);
        }

        /**
         * 备份当前线程的ThreadLocal, 并传递父线程的ThreadLocal
         */
        static Map<Integer, Object> backup(Map<Integer, Object> parentCopy) {
            bizHandlerMap.forEach((hashCode, jaFunctionD) -> jaFunctionD.apply(parentCopy.get(hashCode)));
            Map<Integer, Object> backup = delegate.get();
            delegate.set(parentCopy);
            return backup;
        }

        /**
         * 恢复当前线程ThreadLocal, 并清理父线程的copy
         */
        static void restore(Map<Integer, Object> backup, Map<Integer, Object> parentCopy) {
            bizHandlerMap.forEach((hashCode, jaFunctionD) -> jaFunctionD.apply(null == backup ? null : backup.get(hashCode)));
            delegate.set(backup);
            if (null != parentCopy) {
                parentCopy.clear();
            }
        }
    }
}

/**
 * ttl runnable
 *
 * @author kael
 * @since 2020年03月20日 09:58:34
 */
public class JaRunnable implements Runnable {
    private final Map<Integer, Object> parentCopy;
    private final Runnable runnable;

    public static Runnable get(Runnable runnable) {
        return null == runnable ? null : runnable instanceof JaRunnable ? runnable : new JaRunnable(runnable);
    }

    private JaRunnable(Runnable runnable) {
        this.parentCopy = JaThreadLocal.Transmit.copy();
        this.runnable = runnable;
    }

    @Override
    public void run() {
        Map<Integer, Object> backup = JaThreadLocal.Transmit.backup(parentCopy);
        try {
            runnable.run();
        } finally {
            JaThreadLocal.Transmit.restore(backup, parentCopy);
        }
    }
}

/**
 * ttl callable
 *
 * @author kael
 * @since 2020年03月20日 09:58:56
 */
public final class JaCallable<V> implements Callable<V> {
    private final Map<Integer, Object> parentCopy;
    private final Callable<V> callable;

    public static <V> Callable<V> get(Callable<V> callable) {
        return null == callable ? null : callable instanceof JaCallable ? callable : new JaCallable<>(callable);
    }

    private JaCallable(Callable<V> callable) {
        this.parentCopy = JaThreadLocal.Transmit.copy();
        this.callable = callable;
    }

    @Override
    public V call() throws Exception {
        Map<Integer, Object> backup = JaThreadLocal.Transmit.backup(parentCopy);
        try {
            return callable.call();
        } finally {
            JaThreadLocal.Transmit.restore(backup, parentCopy);
        }
    }
}

 

今天来聊一聊阿里的 TTL 也就是TransmittableThreadLocal

对于实现父子线程的传参使用的一般就是InheritableThreadLocal,对于 InheritableThreadLocal 是如何实现的父子传参可以参考之前发表的这篇文章。

InheritableThreadLocal 是如何实现的父子线程局部变量的传递[1]

有的同学就会问了,既然有了InheritableThreadLocal能够实现父子线程的传参,那么阿里为什么还要在开源一个自己的TransmittableThreadLocal出来呢?

下面就说一下TransmittableThreadLocal解决了什么问题?

image-20241008220812701
版本:TransmittableTreadLocal v2.14.5
代码示例中都没有做remove操作,实际使用中不要忘记哦。本文代码示例加入remove方法不影响测试结果。
代码示例中都没有做remove操作,实际使用中不要忘记哦。本文代码示例加入remove方法不影响测试结果。
代码示例中都没有做remove操作,实际使用中不要忘记哦。本文代码示例加入remove方法不影响测试结果。

一、TransmittableThreadLocal解决了什么问题?

先思考一个问题,在业务开发中,如果想异步执行这个任务可以使用哪些方式?

  1. 使用@Async注解
  2. new Thread()
  3. 线程池
  4. MQ
  5. 其它

上述的几种方式中,暂时只探讨线程的方式,MQ等其他方式暂不在本文的探讨范围内。

不管是使用@Async注解,还是使用线程或者线程池,底层原理都是通过另一个子线程执行的。

对于@Async注解原理不了解的点击链接跳转进行查阅。
一文搞懂@Async注解原理[2]

既然是子线程,那么在涉及到父子线程之间变量传参的时候你们是通过什么方式实现的呢?

父子线程之间进行变量的传递可以通过InheritableThreadLocal实现。

InheritableThreadLocal实现父子线程传参的原理可以参考这篇。
InheritableThreadLocal 是如何实现的父子线程局部变量的传递[3]
本文可以说是对InheritableThreadLocal的一个补充。

当我们在使用new Thread()时,直接通过设置一个ThreadLocal即可实现变量的传递。

需要注意的是,此处传值需要使用InheritableThreadLocal,因为ThreadLocal无法实现在子线程中获取到父线程的值。

由于工作中大部分场景都是使用的线程池,所以我们上面的方式还可以生效吗?

线程池中线程的数量是可以指定的,并且线程是由线程池创建好,池化之后反复使用的。所以此时的父子线程关系中的变量传递就没有了意义,我们需要的是任务提交到线程池时的ThreadLocal变量值传递到任务执行时的线程

InheritableThreadLocal原理这篇文章的末尾,我们提到了线程池的传参方式,本质上也是通过InheritableThreadLocal进行的变量传递。

而阿里的TransmittableThreadLocal类是继承加强的InheritableThreadLocal

TransmittableThreadLocal可以解决线程池中复用线程时,将值传递给实际执行业务的线程,解决异步执行时的上下文传递问题。

除此之外,还有几个典型场景例子:

1、分布式跟踪系统或者全链路压测(链路打标)。

2、日志收集系统上下文。

3、Session 级 Cache。

4、应用容器或者上层框架跨应用代码给下层 SDK 传递信息。

二、TransmittableThreadLocal 怎么用?

上面我们知道了TransmittableThreadLocal可以用来做什么,解决的是线程池中池化线程复用线程时的值传递问题

下面我们就一起来看下怎么使用?

2.1、ThreadLocal

所有代码示例都在 springboot 中演示。

ThreadLocal 在父子线程间是如法传参的,使用方式如下:

@RestController
@RequestMapping("/test2")
public class Test2Controller {
​
    ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();
​
    @RequestMapping("/set")
    public Object set(){
        stringThreadLocal.set("主线程给的值:stringThreadLocal");
        Thread thread = new Thread(() -> {
            System.out.println("读取父线程stringThreadLocal的值:" + stringThreadLocal.get());
        });
        thread.start();
        return "";
    }
}

启动之后访问 /test2/set,显示如下:

image-20240928153605894

通过上面的输出可以看出来,并没有读取到父线程的值

所以为了实现父子传参,需要把 ThreadLocal 修改为 InheritableThreadLocal 。

2.2、InheritableThreadLocal

代码修改完成之后如下:

@RestController
@RequestMapping("/test2")
public class Test2Controller {
​
    ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();
    ThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
​
    @RequestMapping("/set")
    public Object set(){
        stringThreadLocal.set("主线程给的值:stringThreadLocal");
        inheritableThreadLocal.set("主线程给的值:inheritableThreadLocal");
        Thread thread = new Thread(() -> {
            System.out.println("读取父线程stringThreadLocal的值:" + stringThreadLocal.get());
            System.out.println("读取父线程inheritableThreadLocal的值:" + inheritableThreadLocal.get());
        });
        thread.start();
        return "";
    }
}

同样的执行一下看输出:

image-20240928154047262

在上面的演示例子中,都是直接用的new Thread(),下面我们改为线程池的方式试试。

修改完成之后的代码如下所示:

@RestController
@RequestMapping("/test2")
public class Test2Controller {
​
    ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();
    ThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
    ThreadLocal<String> transmittableThreadLocal = new TransmittableThreadLocal<>();
    ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 5, 60,
            TimeUnit.SECONDS, new LinkedBlockingQueue<>());
​
​
    @RequestMapping("/set")
    public Object set(){
        for (int i = 0; i < 10; i++) {
            String val = "主线程给的值:inheritableThreadLocal:"+i;
            System.out.println("主线程set;"+val);
            inheritableThreadLocal.set(val);
            executor.execute(()->{
                System.out.println("线程池:读取父线程 inheritableThreadLocal 的值:" + inheritableThreadLocal.get());
            });
        }
        return "";
    }
}

同样的看下输出:

image-20240928162533921

通过输出我们可以得出结论,当使用线程池时,因为线程都是复用的,在子线程中获取父线程的值,可能获取出来的是上一个线程 的值,所以这里会有线程安全问题。

线程池中的线程并不一定每次都是新创建的,所以对于InheritableThreadLocal是无法实现父子传参的。

如果感觉输出不够明显可以输出子线程的线程名称

下面我们看下怎么使用 TransmittableThreadLocal解决线程池中父子变量传递问题。

2.3、TransmittableThreadLocal

继续对上面代码进行改造,改造完成之后如下所示:

修改部分:TransmittableThreadLocal 的第一种使用方式,TtlRunnable.get() 封装。
@RestController
@RequestMapping("/test2")
public class Test2Controller {
​
    ThreadLocal<String> transmittableThreadLocal = new TransmittableThreadLocal<>();
    ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 5, 60,
            TimeUnit.SECONDS, new LinkedBlockingQueue<>());
​
    @RequestMapping("/set")
    public Object set(){
        for (int i = 0; i < 10; i++) {
            String val = "主线程给的值:TransmittableThreadLocal:"+i;
            System.out.println("主线程set3;"+val);
            transmittableThreadLocal.set(val);
            executor.execute(TtlRunnable.get(()->{
                System.out.println("线程池线程:"+Thread.currentThread().getName()+
                        "读取父线程 TransmittableThreadLocal 的值:"
                        + transmittableThreadLocal.get());
            }));
        }
        return "";
    }
}

执行结果如下所示:

image-20240928163259332

通过日志输出可以看到,子线程的输出已经把父线程中设置的值全部输出了,并没有像 InheritableThreadLocal 那样一直使用那几个值。

可以得出结论,**TransmittableThreadLocal可以解决线程池中复用线程时,将值传递给实际执行业务的线程,解决异步执行时的上下文传递问题。**

那么这样就没问题了吗,看起来使用真的很简单,仅仅需要将 Runnable 封装下即可,下面我们将ThreadLocal中存储的 String 类型的值改为 Map在试试。

三、TransmittableThreadLocal 中的深拷贝

我们将 ThreadLocal 中存储的值改为 Map,修改完代码如下:

@RestController
@RequestMapping("/test2")
public class Test2Controller {
​
    ThreadLocal<Map<String,Object>> transmittableThreadLocal = new TransmittableThreadLocal<>();
    ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 5, 60,
            TimeUnit.SECONDS, new LinkedBlockingQueue<>());
​
    @RequestMapping("/set")
    public Object set(){
        Map<String, Object> map = new HashMap<>();
        map.put("mainThread","主线程给的值:main");
        System.out.println("主线程赋值:"+ map);
        transmittableThreadLocal.set(map);
        executor.execute(TtlRunnable.get(()->{
            System.out.println("线程池线程:"+Thread.currentThread().getName()+
                    "读取父线程 TransmittableThreadLocal 的值:"
                    + transmittableThreadLocal.get());
        }));
        return "";
    }
}

调用接口执行结果如下:

image-20240928194955130

可以看到没啥问题,下面我们简单改一下代码。

1、在主线程提交子线程的任务之后再次修改 ThreadLocal 的值。

2、在子线程中修改 ThreadLocal 的值。

修改完成的代码如下所示:

@RestController
@RequestMapping("/test2")
public class Test2Controller {
​
    ThreadLocal<Map<String, Object>> transmittableThreadLocal = new TransmittableThreadLocal<>();
    ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 5, 60,
            TimeUnit.SECONDS, new LinkedBlockingQueue<>());
​
    @RequestMapping("/set")
    public Object set()  {
        Map<String, Object> map = transmittableThreadLocal.get();
        if (null == map) {map = new HashMap<>();}
        map.put("mainThread", "主线程给的值:main");
        System.out.println("主线程赋值:" + map);
        transmittableThreadLocal.set(map);
        executor.execute(TtlRunnable.get(() -> {
            System.out.println("子线程输出:" + Thread.currentThread().getName() + "读取父线程 TransmittableThreadLocal 的值:" + transmittableThreadLocal.get());
            Map<String, Object> childMap = transmittableThreadLocal.get();
            if (null == childMap){childMap = new HashMap<>();}
            childMap.put("childThread","子线程添加值");
        }));
        Map<String, Object> stringObjectMap = transmittableThreadLocal.get();
        if (null == stringObjectMap) {
            stringObjectMap = new HashMap<>();
        }
        stringObjectMap.put("mainThread-2", "主线程第二次赋值");
        transmittableThreadLocal.set(stringObjectMap);
        try{
            Thread.sleep(1000);
        }catch (InterruptedException e){e.printStackTrace();}
        System.out.println("主线程第二次输出ThreadLocal:"+transmittableThreadLocal.get());
        return "";
    }
}

调用接口输出如下:

image-20240928200124821

通过日志输出可以得出结论,当 ThreadLocal 存储的是对象时,父子线程共享同一个对象。

也就是说父子线程之间的修改都是可见的,原因就是父子线程持有的 Map 都是同一个,在父线程第二次设置值的时候,因为修改的都是同一个 Map,所以子线程也可以读取到。

这一点需要特别的注意,如果有严格的业务逻辑,且共享同一个ThreadLocal,需要注意这个线程安全问题。

那么怎么解决呢,那就是深拷贝,对象的深拷贝,保证父子线程独立,在修改的时候就不会出现父子线程共享同一个对象的事情。

TransmittableThreadLocal 其中有一个 copy 方法,copy 方法就是复制父线程值的,在此处返回一个新的对象,而不是父线程的对象即可,代码修改如下:

为什么是 copy 方法,后文会有介绍。
​
@RestController
@RequestMapping("/test2")
public class Test2Controller {
​
    ThreadLocal<Map<String, Object>> transmittableThreadLocal = new TransmittableThreadLocal(){
        @Override
        public Object copy(Object parentValue) {
            return new HashMap<>((Map)parentValue);
        }
    };
    ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 5, 60,
            TimeUnit.SECONDS, new LinkedBlockingQueue<>());
    @RequestMapping("/set")
    public Object set()  {
        Map<String, Object> map = transmittableThreadLocal.get();
        if (null == map) {map = new HashMap<>();}
        map.put("mainThread", "主线程给的值:main");
        System.out.println("主线程赋值:" + map);
        transmittableThreadLocal.set(map);
        executor.execute(TtlRunnable.get(() -> {
            System.out.println("子线程输出:" + Thread.currentThread().getName() + "读取父线程 TransmittableThreadLocal 的值:" + transmittableThreadLocal.get());
            Map<String, Object> childMap = transmittableThreadLocal.get();
            if (null == childMap){childMap = new HashMap<>();}
            childMap.put("childThread","子线程添加值");
        }));
        Map<String, Object> stringObjectMap = transmittableThreadLocal.get();
        if (null == stringObjectMap) {
            stringObjectMap = new HashMap<>();
        }
        stringObjectMap.put("mainThread-2", "主线程第二次赋值");
        transmittableThreadLocal.set(stringObjectMap);
        try{
            Thread.sleep(1000);
        }catch (InterruptedException e){e.printStackTrace();}
        System.out.println("主线程第二次输出ThreadLocal:"+transmittableThreadLocal.get());
        return "";
    }
}

修改部分如下:

image-20240928201340611

调用接口,查看执行结果可以发现,父子线程的修改已经是独立的对象在修改,不再是共享的。

相信到了这,对于 TransmittableThreadLocal 如何使用应该会了吧,下面我们就一起来看下 TransmittableThreadLocal到底是如何做到的父子线程变量的传递的。

四、TransmittableThreadLocal 原理

TransmittableThreadLocal 简称 TTL

在开始之前先放一张官方的时序图,结合图看源码更容易懂哦!

image-20240928202751094

4.1、TransmittableThreadLocal 使用方式

  • 修饰 Runnable 和Callable

这种方式就是上面代码示例中的形式,通过 TtlRunnableTtlCallable 修改传入线程池的 Runnable 和 Callable

  • 修饰线程池
    修饰线程池可以使用TtlExecutors工具类实现,其中有如下方法可以使用。
image-20240928203444826
  • Java Agent
    Agent 的形式不会对代码入侵,具体的使用可以参考官网,这里就不再说了,官网链接我会放在文章末尾。
    需要注意的是,如果需要和其他 Agent (如Skywalking、Promethues)一起使用,需要把 TransmittableThreadLocal Java Agent 放在第一位。

4.2、源码分析

先简单的概括下:

1、修饰 Runnable ,将主线程的 TTL 值传入到 TtlRunnable 的构造方法中。

2、将子线程的 TTL 进行备份,主线程的值设置到子线程中。

3、子线程执行业务逻辑。

4、删除子线程新增的 TTL,将备份重新设置到子线程中。

4.2.1、TtlRunnable#run 方法做了什么

先从TtlRunnable#run方法入手。

image-20240928211113313

从整体流程来看,整个上下文的传递流程可以规范成快照、回放、恢复(CRR) 三个操作。

  • captured 是主线程(线程A)传递的 TTL的值。
  • backup 是子线程(线程B)中当前存在的 TTL 的值。
  • replay 操作会将主线程中(线程A)的 TTL 的值回放到当前子线程(线程B)中,并返回回放前的 TTL 值的备份也就是上面的 backup
  • runnable.run() 是待执行的方法。
  • restore 是恢复子线程(线程B)进入之时备份的 TTL 的值。因为子线程的 TTL 可能已经发生变化,所以该方法就是回滚到子线程执行 replay 方法之前的 TTL 值。

4.2.2、captured 快照是什么时候做的

同学们思考下,快照又是什么时候做的呢?

通过上面 run 方法可以看到,在该方法的第一行已经是获取快照的值了,所以生成快照肯定不在run方法内了。

提示一下,开头放的时序图还记得吗,可以看下4.1

还记得我们封装了线程吗,使用TtlRunnable.get()进行封装的,返回的是TtlRunnable

答案就在这个方法内部,来看下方法内部做了哪些事情。

   @Nullable
    @Contract(value = "null -> null; !null -> !null", pure = true)
    public static TtlRunnable get(@Nullable Runnable runnable) {
        return get(runnable, false, false);
    }
​
    @Nullable
    @Contract(value = "null, _, _ -> null; !null, _, _ -> !null", pure = true)
    public static TtlRunnable get(@Nullable Runnable runnable, boolean releaseTtlValueReferenceAfterRun, boolean idempotent) {
        if (runnable == null) return null;
​
        if (runnable instanceof TtlEnhanced) {
            // avoid redundant decoration, and ensure idempotency
            if (idempotent) return (TtlRunnable) runnable;
            else throw new IllegalStateException("Already TtlRunnable!");
        }
        return new TtlRunnable(runnable, releaseTtlValueReferenceAfterRun);
    }
​
   private TtlRunnable(@NonNull Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {
        this.capturedRef = new AtomicReference<>(capture());
        this.runnable = runnable;
        this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;
    }
​

可以看到在调用TtlRunnable.get() 方法的最后,调用了TtlRunnable的构造方法,在该方法内部,又调用了capture方法。

image-20240929224153430

capture 方法内部是真正做快照的地方。

image-20240929224351079

其中的transmittee.capture()调用的ttlTransmittee的。

image-20240929230642634
需要注意的是,threadLocal.copyValue()拷贝的是引用,所以如果是对象,就需要重写copy方法。
public T copy(T parentValue) {
return parentValue;
}

代码中的 holder 是一个InheritableThreadLocal,他的值类型是WeakHashMap

image-20240929231949462

key 是TransmittableThreadLocal,value 始终是 null始终没有使用

里面维护了所有使用到的 TransmittableThreadLocal,统一添加到 holder中。

到了这又有了一个疑问?holder 中的 值什么时候添加的?

陷入看源码的误区,一个一个的来,不要一个方法一直扩散,要有一条主线,对于我们这里,已经知道了什么时候进行的快照,如何快照的就可以了,对于 holder中的值在哪里添加的,这就是另一个问题了。

4.2.3、holder 中在哪赋值的

holder 中赋值的地方在 addThisToHolder方法中实现。

具体可以在transmittableThreadLocal.get()transmittableThreadLocal.set()中查看。

    @Override
    public final T get() {
        T value = super.get();
        if (disableIgnoreNullValueSemantics || value != null) addThisToHolder();
        return value;
    }   
@Override
    public final void set(T value) {
        if (!disableIgnoreNullValueSemantics && value == null) {
            // may set null to remove value
            remove();
        } else {
            super.set(value);
            addThisToHolder();
        }
    }
​
    private void addThisToHolder() {
        if (!holder.get().containsKey(this)) {
            holder.get().put((TransmittableThreadLocal<Object>) this, null); // WeakHashMap supports null value.
        }
    }

addThisToHolder 中将此 TransmittableThreadLocal实例添加到 holder 的 key 中。

通过此方法,可以将所有用到的 TransmittableThreadLocal 实例记录。

4.2.4、replay 备份与回放数据

replay方法只做了两件事。

1、将快照中(主线程传递)的数据设置到当前子线程中。

2、返回当前线程的 TTL 值(快照回放当前子线程之前的TTL)。

image-20240928220957597

在 transmittee.replay 方法中真正的执行了备份与回放操作。

image-20240928222620309

4.2.5、restore 恢复

我们看下 CRR 操作的最后一步 restore 恢复。

restore 的功能就是将当前线程的 TTL 恢复到方法执行前备份的值。

image-20241003185059929

restore 方法内部调用了transmittee.restore方法。

image-20241003185211557

思考一下:为什么要在任务执行结束之后执行 restore 操作呢?

首先就是为了保持线程的干净,线程池中的线程都是复用的。

当一个线程重复执行多个任务的时候,第一个任务修改了 TTL 的值,如果不进行 restore ,第二个任务开始时就会获取到第一个任务修改之后的值,而不是预期的初始的值。

五、TransmittableThreadLocal的初始化方法

对于TransmittableThreadLocal相关的初始化方法有三个,如图所示。

image-20241003203404973

5.1、**ThreadLocal#initialValue()**

ThreadLocal 没有值时取值的方法,该方法在ThreadLocal#get 触发。

image-20241003204104674
  • 需要注意的是ThreadLocal#initialValue()是懒加载的,也就是创建ThreadLocal实例的时候并不会触发ThreadLocal#initialValue()的调用。
  • 如果我们先进行了 ThreadLocal.set(T)操作,在进行取值操作,也不会触发ThreadLocal#initialValue(),因为已经有值了,即使是设置的NULL也不会触发该初始化操作。
  • 如果调用了remove 方法,在取值会触发初始化ThreadLocal#initialValue()操作。

5.2、**InheritableThreadLocal#childValue(T)**

childValue方法用于在创建新线程时,初始化子线程的InheritableThreadLocal值。

image-20241003210352475

5.3、**TransmittableThreadLocal#copy(T)**

TtlRunnable或者TtlCallable 创建的时候触发。

例如 TtlRunnable.get()快照时触发。

用于初始化在例如:TtlRunnable执行中的TransmittableThreadLocal值。

image-20241003205557926

六、总结

本文通过代码示例依次演示ThreadLocalInheritableThreadLocalTransmittableThreadLocal实现父子线程传参演化过程。

得出结论如下:

  • 使用ThreadLocal无法实现父子线程传参。
  • InheritableThreadLocal可以实现父子传参,但是线程池场景复用线程问题无法解决。
  • TransmittableThreadLocal可以解决线程池复用线程的问题。

需要注意的是TransmittableThreadLocal保存对象时有深拷贝需求的需要重写TransmittableThreadLocal#copy(T)方法。

最后也欢迎你在评论区讨论交流工作中使用TransmittableThreadLocal时遇到了哪些坑呢?

参考链接

github.com/alibaba/tra…[4]

github.com/alibaba/tra…[5]

github.com/alibaba/tra…[6]

github.com/alibaba/tra…[7]

zhuanlan.zhihu.com/p/11[8]

zhengw-tech.com/2021/08…[9]

juejin.cn/post/699855…[10]

一文搞懂@Async注解原理[11]

InheritableThreadLocal 是如何实现的父子线程局部变量的传递[12]

原文:juejin.cn/post/74232251

[1] mp.weixin.qq.com/s? mp.weixin.qq.com/s?

[2] mp.weixin.qq.com/s/6InD mp.weixin.qq.com/s/6InD

[3] mp.weixin.qq.com/s/UPG0 mp.weixin.qq.com/s/UPG0

[4] github.com/alibaba/tran github.com/alibaba/tran

[5] github.com/alibaba/tran github.com/alibaba/tran

[6] github.com/alibaba/tran github.com/alibaba/tran

[7] github.com/alibaba/tran github.com/alibaba/tran

[8] zhuanlan.zhihu.com/p/11 zhuanlan.zhihu.com/p/11

[9] zhengw-tech.com/2021/08 zhengw-tech.com/2021/08

[10] juejin.cn/post/69985520 juejin.cn/post/69985520

[11] mp.weixin.qq.com/s/6InD mp.weixin.qq.com/s/6InD

[12] mp.weixin.qq.com/s? mp.weixin.qq.com/s?

[13] link.juejin.cn/? mp.weixin.qq.com/s/6InD

 

 

 

posted @ 2025-01-24 10:08  CharyGao  阅读(120)  评论(0)    收藏  举报