(并发编程)ThreadLocal

ThreadLocal叫做线程本地变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本,实现线程隔离

当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本
既然每个Thread有自己的实例副本,且其它Thread不可访问,那就不存在多线程间共享的问题,所以不会出现线程不安全的情况

引言#

ThreadLocal在java生态中应用很广泛,本文基于jdk1.8,现在我们就从源码的角度来进行剖析;我先把ThreadLocal常见的问题先抛出来,大家可以先思考下:

  • ThreadLocal的实例副本是什么?

  • ThreadLocalMap的key为什么是ThreadLocal对象,为什么继承WeakReference?如果在get()的时候发生gc,key是否为null

  • ThreadLocal在什么情况下会出现内存泄漏问题,如何避免内存泄漏?

  • ThreadLocalMap的数据结构和HashMap有什么区别

  • ThreadLocalMap如何来解决Hash冲突?

  • ThreadLocalMap 扩容机制

  • ThreadLocal set() 和 get() 源码分析

  • ThreadLocalMap中过期key的清理机制?探测式清理和启发式清理流程

  • InheritableThreadLocal的作用

  • ThreadLocal在系统中的使用场景有哪些

ThreadLocal为什么是线程安全#

先看下最基本的ThreadLocal使用例子:

public class ThreadLocalTest {

    public static void main(String[] args) {
        ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 1);

        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                longLocal.set(i);
                System.out.println(Thread.currentThread().getName() + " : " + longLocal.get());
            }
        }, "thread-1").start();

        for (int i = 0; i < 5; i++) {
            longLocal.set(i);
            System.out.println(Thread.currentThread().getName() + " : " + longLocal.get());
        }
    }
}

打印结果:

main : 0
thread-1 : 0
thread-1 : 1
thread-1 : 2
thread-1 : 3
thread-1 : 4
main : 1
main : 2
main : 3
main : 4

可以看到,各个线程的threadLocal值是相互独立的,本线程的累加操作不会影响到其他线程的值,真正达到了线程内部隔离的效果

ThreadLocal的数据结构#

Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,也就是说每个线程有一个自己的ThreadLocalMap;这就是上面的第一个问题,ThreadLocalMap就是线程的实例副本

	/* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

inheritableThreadLocals后面讲,主要用于线程之前数据的传递

ThreadLocalMap有自己的独立实现,可以简单地将它的key视作ThreadLocalvalue为代码中放入的值(实际上key并不是ThreadLocal本身,而是它的一个弱引用

每个线程在往ThreadLocal里放值的时候,都会往自己的ThreadLocalMap里存,读也是以ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);  // current-thread获取副本
        if (map != null)
            map.set(this, value);  // this 当前对象当作key,ThreadLocal对象,弱引用
        else
            createMap(t, value);  // 创建新的map
    }

我们来看看存的代码

  • 当前线程中获取到ThreadLocalMap,然后将当前ThreadLocal的引用当作key,set的值当初value进行存储
  • 如果不存在,则调用createMap创建一个新的map
void createMap(Thread t, T firstValue) {
	t.threadLocals = new ThreadLocalMap(this, firstValue);
}

内存布局 如下图:

这里写图片描述

  • Thread Ref 和 ThreadLocal Ref 存放在栈中,具体的Thread和ThreadLocal对象都存在堆
  • ThreadLocalMap的key指向ThreadLocal对象,value就是具体的值

为什么用ThreadLocal对象当作key的好处#

1、自动释放, 当 Thread 对象销毁后,ThreadLocalMap 对象也随之销毁,JVM 及时回收,避免了内存泄漏。如果按我们的想法:定义一个静态的map,将当前 thread(或 thread 的 ID) 作为key,需要保存的对象作为 value,put 到 map 中;如果任务完成之后,当前线程销毁了,这个静态 map 中该线程的信息不会自动回收,如果我们不手动去释放,这个 map 会随着时间的积累越来越大,最后出现内存泄漏。而一旦需要进行手动释放,那很有可能就会有漏网之鱼,这就像埋一个定时炸弹,定期爆发,而又不好排查!

2、性能提升,各线程访问的 ThreadLocalMap 是各自不同的 ThreadLocalMap,所以不需要同步,速度会快很多;而如果把所有线程要用的对象都放到一个静态 map 中的话,多线程并发访问需要进行同步

GC 之后key是否为null?#

回应开头的那个问题, ThreadLocalkey是弱引用,那么在ThreadLocal.get()的时候,发生GC之后,key是否是null

为了搞清楚这个问题,我们需要搞清楚Java四种引用类型

  • 强引用:我们常常new出来的对象就是强引用类型,只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足的时候
  • 软引用:使用SoftReference修饰的对象被称为软引用,软引用指向的对象在内存要溢出的时候被回收
  • 弱引用:使用WeakReference修饰的对象被称为弱引用,只要发生垃圾回收,若这个对象只被弱引用指向,那么就会被回收
  • 虚引用:虚引用是最弱的引用,在 Java 中使用 PhantomReference 进行定义。虚引用中唯一的作用就是用队列接收对象即将死亡的通知

示例代码:

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> test("a", false),"thread-1");
    t1.start(); t1.join();
    Thread t2 = new Thread(() -> test("b", true),"thread-2");
    t2.start(); t2.join();

}

private static void test(String s,boolean isGC)  {
    try {
        new ThreadLocal<>().set(s);   //没有建立任何强引用, 所以GC的时候能被回收,如果 ThreadLocal t = new ThreadLocal<>() 建立了强引用,则无法回收
        if (isGC) {
            System.gc();
        }
        Thread t = Thread.currentThread();
        Class<? extends Thread> clz = t.getClass();
        Field field = clz.getDeclaredField("threadLocals");  // 通过反射获取threadLocalMap
        field.setAccessible(true);
        Object ThreadLocalMap = field.get(t);
        Class<?> tlmClass = ThreadLocalMap.getClass();
        Field tableField = tlmClass.getDeclaredField("table");
        tableField.setAccessible(true);
        Object[] arr = (Object[]) tableField.get(ThreadLocalMap);
        for (Object o : arr) {
            if (o != null) {
                Class<?> entryClass = o.getClass();
                Field valueField = entryClass.getDeclaredField("value");
                Field referenceField = entryClass.getSuperclass().getSuperclass().getDeclaredField("referent");
                valueField.setAccessible(true);
                referenceField.setAccessible(true);
                System.out.printf("弱引用key:%s,值:%s%n", referenceField.get(o), valueField.get(o));
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

打印结果:

弱引用key:java.lang.ThreadLocal@2591b063,值:a
弱引用key:java.lang.ThreadLocal@1f96fc87,值:java.lang.ref.SoftReference@7d7a5ede
弱引用key:null,值:b

从打印结果分析,当线程2触发gc的时候,虽然线程2的ThreadLocal对象被清理,但 ThreadLocalMap的强引用还存在,只是key为空;这个时候如果线程没中断,则会出现内存泄漏的问题;对应之前的问题:如果在get()的时候发生gc,key是否为null

触发gc的时候 ThreadLocal对象被回收,但ThreadLocalMap 的强引用没有中断,导致value永远存在,如果线程不结束,就会出现内存泄漏的情况;所以alibaba规范中使用ThreadLocal必须在finally中调用 .remove()方法;

注:可以这么讲,但凡没有强引用的ThreadLocal对象,都是待回收的垃圾,如果要使用ThreadLocal,就必须要实例化该对象获取get()方法,实例化就建立了强引用,基于这点GC不会回收有效的TheadLocal对象(普通线程从入栈到出栈后,就会断掉所有的引用,线程池核心线程除外)

为什么继承WeakReference#

这里我们衍生几个问题:

  • ThreadLocal为什么使用弱引用
  • 为什么value不使用弱引用
  1. 我们先来说说为什么要使用弱引用

在以往使用完对象后等着GC清理(方法出栈后失去引用,等待GC回收),但是对于ThreadLocal来说,即使我们使用完成后,因为该线程副本还存在该对象的引用,属于对象可达(必须当前线程执行完成),否则永远不会被回收,这是就会出现内存浪费的情况,当程序中对象过多,而这些本地线程中的强引用对象无法释放,gc无法回收,则严重会出现程序崩溃的情况;往往这种问题是最难排查的,特别是在线程池场景,核心线程不会销毁的场景下,线程永远处于可达状态,消耗的内存会越积越多;所以基于以下场景,将ThreadLocal对象设计成弱引用最合适不过;

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}
  1. 为什么value不使用弱引用

不设置为弱引用,是因为不清楚这个value除了map的引用还是否还存在其他引用,如果不存在其他引用,当GC的时候就会直接将这个value干掉了,而此时我们的ThreadLocal还处于使用期间,就会造成value为null的错误,所以必须将其设置为强引用

ThreadLocal在什么情况下会出现内存泄漏问题,如何避免内存泄漏?#

避免方式:

  • current-thread 当前线程结束释放引用
  • 手动调用 threadLocal.remove()方法清理
  • ThreadLocal在扩容和寻址的时候都进行了清理操作,虽然还是会存在内存泄漏,但影响的内存不大

ThreadLocalMap的数据结构和HashMap有什么区别#

当我们查看源码时可以看到以下代码:

hashmap hash处理:

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

threadlocalMap hash处理:

private static int nextHashCode() {
	return nextHashCode.getAndAdd(HASH_INCREMENT);  
}

private static final int HASH_INCREMENT = 0x61c88647;  // 黄金分割数

本质上都是hash散列,HASH_INCREMENT:这个值很特殊,它是斐波那契数 也叫 黄金分割数hash增量为 这个数字,带来的好处就是 hash分布非常均匀

借大佬示例一用:

public static void main(String[] args) {
    final int HASH_INCREMENT = 0x61c88647;

    int hash = 0;
    for (int i = 0; i < 16; i++) {
        hash = i * HASH_INCREMENT + HASH_INCREMENT;
        int bucket = hash & 15;
        System.out.println(i + " 在桶中的位置:"+ bucket);
    }
}

打印结果:

0 在桶中的位置:7
1 在桶中的位置:14
2 在桶中的位置:5
3 在桶中的位置:12
4 在桶中的位置:3
5 在桶中的位置:10
6 在桶中的位置:1
7 在桶中的位置:8
8 在桶中的位置:15
9 在桶中的位置:6
10 在桶中的位置:13
11 在桶中的位置:4
12 在桶中的位置:11
13 在桶中的位置:2
14 在桶中的位置:9
15 在桶中的位置:0

在处理hash冲突时ThreadLocalMap 主要采用开放定址法,大家可以思考下为什么ThreadLocalMap不和HashMap一样用链表的方式,而采用线性存储的方式; HashMap Jdk1.8 主要采用 链表转红黑树的方式来处理,HashMap后续分析

ThreadLocalMap如何来解决Hash冲突#

虽然ThreadLocalMap中使用了黄金分隔数来作为hash计算因子,大大减少了Hash冲突的概率,但是仍然会存在冲突。

HashMap中解决冲突的方法是在数组上构造一个链表结构,冲突的数据挂载到链表上,如果链表长度超过一定数量则会转化成红黑树

private void set(ThreadLocal<?> key, Object value) {

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

    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)]) {  // i++
        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();
}

在ThreadLocalMap set源码中可以看出,当我们通过 int i = key.threadLocalHashCode & (len-1) 计算出 hash 值,如果出现冲突,顺序查看表中下一单元,直到找出一个空单元或查遍全表。

正因为采取的线性探测法解决冲突,所以在查找的时候,必须比较 key 值是否相等,否则顺序寻找下一个单元

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

nextIndex就是递增索引下标

ThreadLocalMap 扩容机制#

if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();

private void rehash() {
    expungeStaleEntries();

    // Use lower threshold for doubling to avoid hysteresis
    if (size >= threshold - threshold / 4)
        resize();
}

扩容后的tab的大小为oldLen * 2,然后遍历老的散列表,重新计算hash位置,然后放到新的tab数组中,如果出现hash冲突则往后寻找最近的entrynull的槽位,遍历完成之后,oldTab中所有的entry数据都已经放入到新的tab中了。重新计算tab下次扩容的阈值

如下:

private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    int newLen = oldLen * 2;  // 扩容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;
}

ThreadLocal set() 和 get() 源码分析#

set() 源码分析#

可以参考大佬博客:https://blog.csdn.net/l18848956739/article/details/106122096

private void set(ThreadLocal<?> key, Object value) {

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);  // hash运算

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

        if (k == key) {  // 如果 key.hashCode 相等,则替换value值
            e.value = value;
            return;
        }

        if (k == null) {  //如果 key 为空,说明当前桶位置的Entry是过期数据,因为 Entry e = tab[i];e != null;符合条件就表示Entry肯定是有值的,这里k为null,就代表被gc回收
            replaceStaleEntry(key, value, i);  //替换过期数据的逻辑
            return;
        }
    }

    tab[i] = new Entry(key, value);  // 如果数组为空,则表示可以直接存放
    int sz = ++size;
    //启发式清理,清理散列数组中Entry的key过期的数据
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();  //扩容
}

replaceStaleEntry() 方法详解

private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;

    // Back up to check for prior stale entry in current run.
    // We clean out whole runs at a time to avoid continual
    // incremental rehashing due to garbage collector freeing
    // up refs in bunches (i.e., whenever the collector runs).
    int slotToExpunge = staleSlot;  //slotToExpunge表示开始探测式清理过期数据的开始下标,默认从当前的staleSlot开始
    for (int i = prevIndex(staleSlot, len);  // 先往前查找,直到tab[i]为null会执行完循环
         (e = tab[i]) != null;
         i = prevIndex(i, len))
        if (e.get() == null)  // 如果没有找到被回收的对象,则将 slotToExpunge 更新为i
            slotToExpunge = i;

    // Find either the key or trailing null slot of run, whichever
    // occurs first
    for (int i = nextIndex(staleSlot, len);  //然后向后查询
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();

        // If we find key, then we need to swap it
        // with the stale entry to maintain hash table order.
        // The newly stale slot, or any other stale slot
        // encountered above it, can then be sent to expungeStaleEntry
        // to remove or rehash all of the other entries in run.
        if (k == key) {   // 如果存在相同的ThreadLocal引用
            e.value = value; // 则将新的value 重新赋值

            tab[i] = tab[staleSlot];  // 替换新数据并且交换当前staleSlot位置
            tab[staleSlot] = e;

            // Start expunge at preceding stale entry if it exists
            if (slotToExpunge == staleSlot)  // 代表prevIndex和nextIndex 都没有找到null值并且符合 k== key的时候,走交换数据逻辑,并从当前索引往后进行过期清理
                slotToExpunge = i;
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);  // 启发式过期清理,从当前索引i开始往后清理
            return;
        }

        // 如果k != key则会接着往下走,k == null说明当前遍历的Entry是一个过期数据,slotToExpunge == staleSlot说明,
        // 一开始的向前查找数据并未找到过期的Entry。如果条件成立,则更新slotToExpunge 为当前位置,这个前提是前驱节点扫描时未发现过期数据
        if (k == null && slotToExpunge == staleSlot)  
            slotToExpunge = i;
    }

    // If key not found, put new entry in stale slot
    tab[staleSlot].value = null;   // set方法 if (k == null) 才走replaceStaleEntry方法,所以将value置空
    tab[staleSlot] = new Entry(key, value);

    // If there are any other stale entries in run, expunge them 
    if (slotToExpunge != staleSlot) // 如果slotToExpunge 不为staleSlot,说明存在清理项,进行过期清理,补充上面的if(k == key) 的else逻辑
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

get() 源码分析#

private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)   //如果从Entry 中找到相同的对象,则直接返回
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}


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

    while (e != null) {
        ThreadLocal<?> k = e.get();
        if (k == key)
            return e;
        if (k == null)
            expungeStaleEntry(i);  // 探测式清理
        else
            i = nextIndex(i, len);  // 遍历下个索引,因为采用地址法存储,则直接取下个索引
        e = tab[i];
    }
    return null;
}

get()方法相对简单,先判断hash位置是否存在key,如果存在并且和当前请求的ThreadLocal相同,则直接返回value,否则就往后遍历查找

ThreadLocalMap中过期key的清理机制?探测式清理和启发式清理流程#

上面多次提及到ThreadLocalMap过期可以的两种清理方式:探测式清理(expungeStaleEntry())启发式清理(cleanSomeSlots())探测式清理是以当前Entry 往后清理,遇到值为null则结束清理,属于线性探测清理。而启发式清理被作者定义为:Heuristically scan some cells looking for stale entries.

探测式清理:探测式清理,是以当前遇到的 GC 元素开始,向后不断的清理。直到遇到 null 为止,才停止 rehash 计算

private int expungeStaleEntry(int staleSlot) {  //staleSlot 开始清理的索引下标
    Entry[] tab = table;
    int len = tab.length;

    // expunge entry at staleSlot
    tab[staleSlot].value = null;   //先把改索引的值置空;弱引用gc后Map可能为 null,value
    tab[staleSlot] = null;  // help gc
    size--;  //长度减少

    // Rehash until we encounter null
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len);   // 开始遍历当前索引之后的所有key为空的数组
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        if (k == null) {   // k == null 会回收该条数据
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            int h = k.threadLocalHashCode & (len - 1);  //h 重新hash运算的位置,因为前面回收了一些过期的数据
            if (h != i) {  // 如果重新计算的值不等于i,就说明存在回收,则重新更换位置,而且之前发生 Hash冲突 的Entry元素的位置应该更接近真实hash出来的位置
                tab[i] = null;
                while (tab[h] != null)  //一直往后找,找到为null的位置为止,因为中间会有些数据已经被回收了,会空留出内存,可以看看大佬的图文分析
                    h = nextIndex(h, len);
                tab[h] = e;  //重新赋值
            }
        }
    }
    return i;
}

启发式清理:

就是通过while循环的方式在探测式清理后在进行清除,nextIndex累计的次数为 m = n>>>=1 的次数,如果遍历过程中,连续 m 次没有发现过期的Entry,就可以认为数组中已经没有过期Entry了

private boolean cleanSomeSlots(int i, int n) {  //探测式清理后返回的数字下标,这里至少保证了Hash冲突的下标至探测式清理后返回的下标这个区间无过期的Entry, n 数组总长度
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    do {
        i = nextIndex(i, len);
        Entry e = tab[i];
        if (e != null && e.get() == null) {  // 如果发下过期的Entry则在执行探测性清理
            n = len;  //重置n
            removed = true;
            i = expungeStaleEntry(i);   //探测性清理
        }
    } while ( (n >>>= 1) != 0);  // 循环条件: m = logn/log2(n为数组长度)
    return removed;
}

这个 m 的计算是 n >>>= 1 ,你也可以理解成是数组长度的2的几次幂。
例如:数组长度是16,那么24=16,也就是连续4次没有过期Entry,即 m = logn/log2(n为数组长度)

InheritableThreadLocal和TransmittableThreadLocal的作用#

我们使用ThreadLocal的时候,在异步场景下是无法给子线程共享父线程中创建的线程副本数据的,为了解决这个问题JDK提供了InheritableThreadLocal

public class InheritableThreadLocalDemo {
    public static void main(String[] args) {
        ThreadLocal<String> threadLocal = new ThreadLocal<>();
        ThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
        threadLocal.set("父类数据:threadLocal");
        inheritableThreadLocal.set("父类数据:inheritableThreadLocal");
 
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("子线程获取父类threadLocal数据:" + threadLocal.get());
                System.out.println("子线程获取父类inheritableThreadLocal数据:" + inheritableThreadLocal.get());
            }
        }).start();
    }
}

打印结果:

子线程获取父类threadLocal数据:null
子线程获取父类inheritableThreadLocal数据:父类数据:inheritableThreadLocal

实现原理是子线程是通过在父线程中通过调用new Thread()方法来创建子线程,Thread#init方法在Thread的构造方法中被调用。在init方法中拷贝父线程数据到子线程中:

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {

    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =
        ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}

TransmittableThreadLocal使用#

InheritableThreadLocal仍然有缺陷,一般我们做异步化处理都是使用的线程池,而InheritableThreadLocal是在new Thread中的init()方法给赋值的,而线程池是线程复用的逻辑,所以这里会存在问题

如何使用TransmittableThreadLocal:

引用依赖

<properties>
    <ttl.version>2.11.4</ttl.version>
</properties>

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>transmittable-thread-local</artifactId>
    <version>${ttl.version}</version>
</dependency>
public class TransmittableThreadLocalTest {

    private static ThreadLocal<String> ttl = new TransmittableThreadLocal<>();

    /** 保证只有1个线程,以便观察这个线程被多个Runnable复用时,能否成功完成ThreadLocal的传递 **/
    private static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
            1,1,0, TimeUnit.SECONDS,new ArrayBlockingQueue<>(10)
    );

    public static void main(String[] args) throws InterruptedException {
        ttl.set("a");

        for (int i = 0; i < 5; i++) {
            if (i == 1) {
                ttl.set("b");
            }
            TtlRunnable runnable = TtlRunnable.get(() -> {
                System.out.println(Thread.currentThread().getName() + " : " + ttl.get());
            });

            threadPoolExecutor.execute(runnable);
            TimeUnit.MILLISECONDS.sleep(500);
        }
    }
}

打印结果:

pool-1-thread-1 : a
pool-1-thread-1 : a
pool-1-thread-1 : b
pool-1-thread-1 : b
pool-1-thread-1 : b

后面在深入源码分析

ThreadLocal在系统中的使用场景有哪些#

  • Web请求的用户身份态:Session
  • 请求的链路跟踪:traceId
  • SimpleDateFormat转换时间问题:因为SimpleDateFormat不是线程安全的,用ThreadLocal能提升转换效率
  • 系统链路日志等
posted @   糯米๓  阅读(90)  评论(0编辑  收藏  举报
编辑推荐:
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· Obsidian + DeepSeek:免费 AI 助力你的知识管理,让你的笔记飞起来!
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
more_horiz
keyboard_arrow_up dark_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示