ThreadLocal - java并发系列(二)

98 阅读20分钟

ThreadLocal

ThreadLocal,翻译过来,就是线程级别的本地变量,其实,也可以理解为线程级别的全局变量,重点是线程,在同一个线程内,可以在任意位置访问它,也就是线程内共享的。

一、应用场景

  • 每个线程需要一个独享的对象(通常是工具类,典型需要使用的类有SimpleDateFormat和Random)

  • 每个线程内需要保存全局变量(例如在拦截器中获取用户信息),可以让不同方法直接使用,避免参数传递的麻烦。

以场景1为例,举个例子,我们编写一个线程安全的时间格式化工具:

private static final ThreadLocal<Map<String, SimpleDateFormat>> THREAD_FORMAT_MAP = ThreadLocal.withInitial(HashMap::new);

private static SimpleDateFormat getThreadLocalSimpleDateFormat(String pattern) {
    return THREAD_FORMAT_MAP.get().computeIfAbsent(pattern, SimpleDateFormat::new);
}

public static String format(Date date, String pattern) {
    return getThreadLocalSimpleDateFormat(pattern).format(date);
}

public static Date parse(String dateStr, String pattern) throws ParseException {
    return getThreadLocalSimpleDateFormat(pattern).parse(dateStr);
}

这里使用的HashMap和SimpleDateFormat都是线程不安全的,但是,我们把带有SimpleDateFormat的HashMap放在ThreadLocal中,每个线程独享,自然也就不用纠结线程安全的问题了。

二、源码实现

2.1 概述

ThreadLocal既然是Thread的独享变量,那我们把这个变量放在Thread中就好了。而一个线程可以维护多个ThreadLocal,那就把这个变量类型设置为map,就可以灵活的放多个ThreadLocal变量了。TheadLocal也确实就是这么实现的:

public class Thread implements Runnable {
    //Thead中有这样一个map对象,用于存储与Thead相关的ThreadLocal,注意这个是包级私有的
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

public class ThreadLocal<T> {

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

    /**
     * get/set方法,就是获取Thread的map,然后以当前的ThreadLocal作为key,去get/set
     **/

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
    
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
}

2.2 ThreadLocalMap

既然Thread使用Map存储ThreadLocal,并且使用ThreadLocal作为key,那么,理论上ThreadLocal应该重写hashCode和equals方法才对。然而并没有,因为Thread使用的map并不是java.util.Map,而是ThreadLocal中自定义的ThreadLocalMap。

2.2.1 ThreadLocal.threadLocalHashCode

ThreadLocal在对象初始化的时候,就生成了一个不可改变的hashCode,利用一个全局的AtomicInteger,初始值是0,每次新建对象,在该AtomicInteger的基础上,自增0x61c88647,作为自己的hashCode。至于为啥是0x61c88647,我也不太懂,反正就是使用这个数字比较神奇,作为步长递增,可以减少碰撞。可以参考为什么使用0x61c88647

 /**
     * 对象创建直接给定一个hashCode
     */
    private final int threadLocalHashCode = nextHashCode();

    /**
     * 生成hashcode使用的AtomicInteger,初始值是0
     */
    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    /**
     * hashCode自增的步长
     * The difference between successively generated hash codes - turns
     * implicit sequential thread-local IDs into near-optimally spread
     * multiplicative hash values for power-of-two-sized tables.
     */
    private static final int HASH_INCREMENT = 0x61c88647;

    /**
     * 使用AtomicInteger原子化的生成hashCode
     */
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

2.2.2 Entry & WeakReference

ThreadLocal的键值对类(Entry)比较有趣,直接继承自WeakReference<ThreadLocal<?>>,在WeakReference的基础上,增加了一个Object类型的value对象。也就是说,ThreadLocal作为key,是一个弱引用,而它对应的值(value)是一个强引用。

/**
 * The entries in this hash map extend WeakReference, using
 * its main ref field as the key (which is always a
 * ThreadLocal object).  Note that null keys (i.e. entry.get()
 * == null) mean that the key is no longer referenced, so the
 * entry can be expunged from table.  Such entries are referred to
 * as "stale entries" in the code that follows.
 */
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

复习一下Java的引用类型:

  • 强引用:常规引用,只要存活,就不会被回收;
  • 软引用:软引用是用来描述一些有用但并不是必需的对象,在Java中用java.lang.ref.SoftReference类来表示。对于 只被 软引用关联着的对象,只有在内存不足的时候JVM才会回收该对象。因此,这一点可以很好地用来解决OOM的问题,并且这个特性很适合用来实现缓存:比如网页缓存、图片缓存等。
  • 弱引用:弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收只被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。
  • 虚引用:虚引用就是形同虚设 ,它并不能决定 对象的生命周期。任何时候这个只有虚引用的对象都有可能被回收。因此,虚引用主要用来跟踪对象的回收,清理被销毁对象的相关资源。

Entry的这个ThreadLocal类型的key是弱引用,也就是说,在没有其他强引用关联该key时,它就可以被回收,key被设置为null。但value是强引用,即使key回收了,value也不会回收,一直不回收,显然会造成内存泄漏。而该实现又没有使用ReferenceQueue记录回收的key,因此在ThreadLocalMap的getEntry/set/remove方法中,在一定条件下,会触发过期的Entry(也就是key为null的Entry)的清理工作。

2.2.3 开放寻址

HashMap在解决hash冲突的时候,有四种方法:

  1. 开放寻址法:当关键字key的哈希地址p=H(key)出现冲突时,以p为基础,产生另一个哈希地址p1,如果p1仍然冲突,再以p为基础,产生另一个哈希地址p2,…,直到找出一个不冲突的哈希地址pi ,将相应元素存入其中。
  2. 再hash:准备多种hash算法,一个冲突就换一个,直到换到合适的为止。hash算法都用完了还冲突,那就退化到其他方法,或者扩容才行了。
  3. 拉链法:在冲突的位置上建立一个链表,存储冲突元素。
  4. 建立公共溢出区:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。

开放寻址法与再hash类似,数据都存在一个数组内,冲突了在数组内重新找个位置放,只是重新找位置的过程不同,再hash本质上可以看做是开放寻址法的一种实现。另外这种方法必须保证数组的长度大于存储的键值对的数量。而方法3/4也是类似的。

开放寻址 vs 拉链法

  • 开放寻址法的数据结构更简单,更加节省空间,但出现冲突后处理更复杂,计算量更大,删除时,如果删除的元素是多个冲突元素中的一个,需要对后面的元素作处理,就更复杂了。因此适合装载因子较小,冲突较小的场景。
  • 拉链法则更浪费空间,但是冲突处理上则简单很多,与开放寻址法相比,也算是一种空间换时间吧。

我们平常使用的HashMap,采用的是拉链法(也做了一点优化),而ThreadLocalMap采用的是开放寻址法。也许是因为ThreadLocalMap的key只能是ThreadLocal,而他的hash算法,是经过优化的,作者认为冲突的可能性比较小,并且ThreadLocal在常规使用过程中,也不会建特别多。

PS:我们使用时,可以使用一个包含多个属性的对象,作为ThreadLocal,也能一定程度上,减少hash冲突。

2.2.4 ThreadLocalMap.getEntry

ThreadLocalMap采用开放寻址的方式解决hash冲突,并且在getEntry/set/remove方法中,还要处理过期的Entry,这导致它的这些方法有点复杂。先看下他的getEntry方法。

private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}

key.threadLocalHashCode & (table.length - 1);这段代码是在计算key在数组中的位置,直接使用hashCode与数组长度-1,做与的位运算。数组的长度在初始化与扩容时,会保证是2的n次方,这样做与位运算,就是取二进制的后几位,一定比数组的长度小。

接下来就是尝试在该位置获取value值,找到就返回,找不到有可能是真的没有,也可能是和别的key冲突了,需要进一步调用getEntryAfterMiss方法处理。

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)//key过期
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);//计算冲突后的位置,其实就是在原基础上+1,超过长度则返回0
        e = tab[i];
    }
    return null;
}
/**
 * 计算冲突后的位置,其实就是在原基础上+1,超过长度则返回0
 */
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

这里查找的过程比较简单,就是在该位置继续往后找,直到找到或者遇到null为止。遇到Entry.key为null时,说明它的key被回收了,需要清除过期的Entry,也就是调用了expungeStaleEntry方法。

不过,并不能直接删除了事,与正常的删除类似,需要处理后面的元素,因为后面可能存在与过期元素冲突的元素,简单删除,下次查找冲突的key就可能出问题。

/** 
 * 清除位置staleSlot上过期的key,但这就像开放寻址法的删除一样,还要把后面可能冲突了 的元素处理一下,直到遇到null为止,否则下次查找就可能会出错。
 */
private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // 删除过期的key
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // 处理后面的元素,重新hash一下,直到遇到null停止
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        if (k == null) {
            //遇到过期的key,顺便删除
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            //重新hash,放到正确的位置
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                tab[i] = null;
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    // 返回停止的那个null的位置
    return i;
}

2.2.5 ThreadLocalMap.set

set方法也是,在操作过程中,会尝试清除一些过期的Entry

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

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

    // 搜索key所在的位置
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();
        // key已经存在,修改value即可
        if (k == key) {
            e.value = value;
            return;
        }
        // Entry过期,替换该Entry的key和value。思考一下,这里可以直接简单的替换么?
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    // 找到位置,设置key,value
    tab[i] = new Entry(key, value);
    int sz = ++size;//这里用的++size,还挺有趣的
    // 顺便尝试清除一些过期的Entry
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        //如果么有清除成功,更新后的值大于了扩容的阈值,则尝试扩容
        rehash();
}

replaceStaleEntry,替换过期的Entry的key和value,然而

  • 不能简单的替换,因为本次设置的key可能在后面已经存在,除非确定确实不存在,否则不能替换。
  • 顺便清理该位置两边最近的null之间的过期的Entry。
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                               int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;

    // 需要处理的过期的key的最小位置,记住它的含义
    int slotToExpunge = staleSlot;
    // 向前找到第一个null,并记录slotToExpunge为staleSlot前面第一个过期的key的位置
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;

    // 向后遍历直到null
    for (int i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();

        // 向后找到了key,替换key的value,同时和staleSlot的元素交换位置
        if (k == key) {
            e.value = value;

            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;

            // 清理slotToExpunge之后过期的key,并返回
            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }

        // 遇到过期的key,记录需要处理过期的key的最开始的位置
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }

    // 没有找到key,可以替换了
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);

    // 如果在两个null之间,存在过期的key,则清理
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

上述代码,在新增元素完成之后,会执行这样一行代码:cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);,这里先嵌套调用了expungeStaleEntry方法,之前讲过,这个方法会清除指定位置的过期元素,并对之后的元素rehash,直到null为止,最后把null的位置返回,作为cleanSomeSlots的第一个参数。

接下来我们看下cleanSomeSlots方法。


/**
 * Heuristically scan some cells looking for stale entries.
 * This is invoked when either a new element is added, or
 * another stale one has been expunged. It performs a
 * logarithmic number of scans, as a balance between no
 * scanning (fast but retains garbage) and a number of scans
 * proportional to number of elements, that would find all
 * garbage but would cause some insertions to take O(n) time.
 * 翻译过来就是,探索性的尝试清除过期的entry,会扫描log2n次,
 * 以达到一种性能和垃圾回收上的平衡,扫描太多性能较差,扫描太少垃圾太多
 */
private boolean cleanSomeSlots(int i, int 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) {
            //发现过期元素,使用expungeStaleEntry方法清除。
            n = len;
            removed = true;
            i = expungeStaleEntry(i);
        }
    //循环log2n次
    } while ( (n >>>= 1) != 0);
    return removed;
}

2.2.6 ThreadLocalMap.rehash

让我们的视角回到set方法,最后有这样一段代码,之前如果没有走到replaceStaleEntry分支中,最后就会走到这里,调用一下cleanSomeSlots方法。如果没有清除任何元素,并且达到了扩容阈值,则尝试扩容。

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

这个阈值,是数组长度的2/3。

//数组初始长度为16
private static final int INITIAL_CAPACITY = 16;

/**
 * 阈值为数组长度的2/3
 */
private void setThreshold(int len) {
    threshold = len * 2 / 3;
}

rehash方法,会先清除所有的过期元素,再尝试扩容。

private void rehash() {
    // 清除过期元素,就是循环调用expungeStaleEntry方法
    expungeStaleEntries();

    // 再次判断阈值,此时只要大于阈值的3/4,就扩容,防止rehash方法被反复调用
    if (size >= threshold - threshold / 4)
        //这个方法也没啥好说的,就是新建一个容量加倍的数组,把现有元素重新hash,复制到新的数组中
        resize();
}

2.2.7 ThreadLocalMap.remove

remove方法其实没什么好说的,通过expungeStaleEntry方法实现:

private void remove(ThreadLocal<?> key) {
    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)]) {
        if (e.get() == key) {
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}

三、注意事项

3.1 内存泄露

经常听到一种说法,ThreadLocal可能会产生内存泄漏,其实实践场景中,不大会产生这种场景。

那,什么是内存泄漏呢?

一块内存,程序中获取不到,但又无法被JVM回收。

什么场景下,ThreadLocal会产生内存泄漏呢

ThreadLocal本身,是不存储任何数据的,数据其实都是存储在了Thread的map上。

  1. ThreadLocal本身不存在任何强引用,GC时被回收掉;
  2. 装载ThreadLocal对应的数据的Thread,一直存活,比如我们常用的线程池的技术;
  3. 没有或者较少调用ThreadLocal的get/set/remove方法:
    • 没有触发过期Entry回收;
    • 触发了也没有成功回收,毕竟除非触发了rehash方法,否则不会全部检查一遍的。

真的容易触发内存泄漏么 以上的三个条件,2和3都比较常见,只是1不常见,因为使用TheadLocal,我们通常都会设置为静态变量,以达到在任何位置都能方便的获取的目的,也不会轻易把这个静态变量,设置为null。这显得ThreadLocal的弱引用设计的有那么一点鸡肋。

那不需要注意内存泄漏的问题么 还是需要的,当前提到线程,一般都是使用线程池。如果我们在一次请求中设置了线程变量,而不去回收它,那么它的值就一直躺在线程池中那个线程上,即使这次请求结束。

所以,我们一般设置线程池的值的时候,还是要注意值的生命周期,如果仅当次请求生效,建议放在try/finally块中,在finally块中,使用remove方法回收值。

其实这样做的主要目的,也不是为了防止内存泄漏,而是防止ThreadLocal污染。假设我们有个ThreadLocal装载用户的登录信息,在每次请求开始时,初始化它,方便我们在整个请求的中,非常容易获取到用户信息。但是却没有及时回收它。那么该用户信息,就会一直躺在线程池中的线程上。假设之后的某次请求,恰好走的了不会设置用户信息的分支上,那该线程绑定的就是错误的用户信息,也就是所谓的ThreadLocal污染,这在业务上,是不可接受的。

==在使用ThreadLocal时,一定要确认它的生命周期,做到及时清理==

3.2 跨线程

ThreadLocal是线程变量,但我们在实践中,又难免使用到多线程。当我们使用到了TheadLocal,但又不得不利用多线程时。就会出现TheadLocal丢失的问题。其实丢失也很正常,毕竟它是线程变量,你到了另外一个线程中还能获取它,反而是有问题的。不过,问题还是要解决。

3.2.1 InheritableThreadLocal

InheritableThreadLocal是ThreadLocal的子类,当在一个线程内,创建一个子线程时(其实就是new Thread()),子线程可以继承父线程所有的InheritableThreadLocal的值。也就达到了,线程变量跨线程传递的目的。

Thead中,除了有个存放ThreadLocal的map外,还有个存放InheritableThreadLocal的map。在初始化时,会获取父线程的inheritableThreadLocals,浅拷贝到当前线程内。

/*
 * InheritableThreadLocal values pertaining to this thread. This map is
 * maintained by the InheritableThreadLocal class.
 */
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
/**
 * Thread在构造函数中,会调用init方法,这里省略了与InheritableThreadLocal无关的代码
 */
private void init(ThreadGroup g, Runnable target, String name,
                    long stackSize, AccessControlContext acc,
                    boolean inheritThreadLocals) {
    ...

    Thread parent = currentThread();
    ...
    //复制当前线程(也就是所谓的父线程)的inheritableThreadLocals
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =
            ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    ...
}

复制map时,默认使用浅拷贝,如果需要深拷贝甚至改变线程变量的值,可以考虑复写InheritableThreadLocal的childValue方法。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    /**
     * 有需要可以复写该方法,深拷贝甚至改写线程变量的值
     */
    protected T childValue(T parentValue) {
        return parentValue;
    }

    /**
     * 复写getMap/createMap方法,使用Thread的inheritableThreadLocals,存放线程变量,与ThreadLocal区分开
     */
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

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

==然鹅,InheritableThreadLocal有点鸡肋,它虽然解决了父线程向子线程传递线程变量的问题,但我们现在通常使用线程池技术。线程池中的线程,仅在第一次创建的时候,可以获取父线程的InheritableThreadLocal,其余场景,不会重新初始化,也就无法从父线程中,获取InheritableThreadLocal变量。==

3.2.2 TransmittableThreadLocal

transmittable-thread-local 为解决跨线程传递ThreadLocal的问题,阿里开源了transmittable-thread-local,用官方文档的说法,以下为使用场景:

ThreadLocal的需求场景即TransmittableThreadLocal的潜在需求场景,如果你的业务需要『在使用线程池等会池化复用线程的执行组件情况下传递ThreadLocal值』则是TransmittableThreadLocal目标场景。

使用前,需要引入transmittable-thread-local依赖

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>transmittable-thread-local</artifactId>
    <version>2.14.3</version>
</dependency>

使用例子可以直接看官网的user guide,非常详细。

原理浅析

在源码解析之前,我们粗略思考一下,如果解决ThreadLocal的跨线程问题。简单来想想,可以这样:

在运行之前,把ThreadLocal值,复制到正在执行的线程中,执行结束后移除掉。

这里面有两个问题:

  1. 手动执行以上步骤太麻烦,也很难让所有人都注意到这一点,显示增加以上的代码逻辑。
  2. 如何获取到ThreadLocal的那个map,要知道无论是threadLocals还是inheritableThreadLocals都是Thread包级私有的,无法直接获取到。

1.优雅使用

user guide提到的使用方法,就可以看出,transmittable-thread-local是如何解决这个问题的,简单来说,就是装饰者模式。

TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();

// =====================================================

// 在父线程中设置
context.set("value-set-in-parent");
Runnable task = new RunnableTask();
// 额外的处理,生成修饰了的对象ttlRunnable
Runnable ttlRunnable = TtlRunnable.get(task);
executorService.submit(ttlRunnable);
// =====================================================

// Task中可以读取,值是"value-set-in-parent"
String value = context.get();

TtlRunnable.get方法生成了一个新的装饰对象TtlRunnable,该方法很简单,这里不去分析了,直接看TtlRunnable类:

public final class TtlRunnable implements Runnable, TtlWrapper<Runnable>, TtlEnhanced, TtlAttachments {
    private final AtomicReference<Object> capturedRef;
    private final Runnable runnable;
    private final boolean releaseTtlValueReferenceAfterRun;

    private TtlRunnable(@NonNull Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {
        //构造的时候,捕捉了TransmittableThreadLocal线程变量的快照
        this.capturedRef = new AtomicReference<>(capture());
        this.runnable = runnable;
        this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;
    }

    /**
     * wrap method {@link Runnable#run()}.
     */
    @Override
    public void run() {
        final Object captured = capturedRef.get();
        if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) {
            throw new IllegalStateException("TTL value reference is released after run!");
        }
        //执行时,先把构造时捕捉的线程变量,回放(replay)到当前线程上
        final Object backup = replay(captured);
        try {
            runnable.run();
        } finally {
            //恢复之前的状态
            restore(backup);
        }
    }
}

这里的capture、replay、restore与我们之前粗略想的方案类似。不过其实也没那么简单,这里可以看下官方的解答。 能在详细讲解一下replay、restore的设计理念吗?没有思考明白,为什么还需要有恢复步骤

再优雅些

TtlRunnable.get方法,使用装饰着模式,比我们简单的手敲代码优雅一些,但是每次向线程池中提交任务,都调用该方法,也很麻烦,容易遗忘。还是装饰者模式的思路,可以直接装饰线程池:

// 创建线程池
ExecutorService executorService = ...
// 额外的处理,生成修饰了的对象executorService
executorService = TtlExecutors.getTtlExecutorService(executorService);

TtlExecutors.getTtlExecutorService方法生成了一个装饰对象ExecutorTtlWrapper,ExecutorTtlWrapper在执行execute时,先调用了TtlRunnable.get方法,再把装饰后的Runable对象,传递给executor执行。这样,只要向这个装饰后的线程池中,提交任务,每个任务就会被自动装饰为TtlRunnable对象,无需显示手动调用,非常优雅。

@Override
public void execute(@NonNull Runnable command) {
    executor.execute(TtlRunnable.get(command, false, idempotent));
}

再再优雅些

可以使用JavaAgentdeveloper-guide.md。也就是再启动时增加JavaAgent参数:

在Java的启动参数加上:-javaagent:path/to/transmittable-thread-local-2.x.y.jar。

这样就可以对业务代码无侵入的解决这个问题。

2.获取ThreadLocal的值

在Thread中,无论是threadLocals还是inheritableThreadLocals都是Thread包级私有的,无法直接获取到。因此,要想办法把一个线程的TransmittableThreadLocal想办法保存起来。TransmittableThreadLocal是InheritableThreadLocal的子类,可以复写它的set方法,

    @Override
    public final void set(T value) {
        if (!disableIgnoreNullValueSemantics && value == null) {
            //默认,如果是null,直接移除;
            remove();
        } else {
            //set后调用addThisToHolder把this记录下来
            super.set(value);
            addThisToHolder();
        }
    }

    //把this记录到holder中
    private void addThisToHolder() {
        if (!holder.get().containsKey(this)) {
            holder.get().put((TransmittableThreadLocal<Object>) this, null); // WeakHashMap supports null value.
        }
    }

    // Note about the holder:
    // 1. holder self is a InheritableThreadLocal(a *ThreadLocal*).
    // 2. The type of value in the holder is WeakHashMap<TransmittableThreadLocal<Object>, ?>.
    //    2.1 but the WeakHashMap is used as a *Set*:
    //        the value of WeakHashMap is *always* null, and never used.
    //    2.2 WeakHashMap support *null* value.
    private static final InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>> holder = new InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>>() {
            //重写initialValue,初始创建一个WeakHashMap
            @Override
            protected WeakHashMap<TransmittableThreadLocal<Object>, ?> initialValue() {
                return new WeakHashMap<>();
            }

            //重写child方法,创建新的WeakHashMap,emmm,那为啥要使用InheritableThreadLocal呢
            @Override
            protected WeakHashMap<TransmittableThreadLocal<Object>, ?> childValue(WeakHashMap<TransmittableThreadLocal<Object>, ?> parentValue) {
                return new WeakHashMap<>(parentValue);
            }
    };

TransmittableThreadLocal在set过程中,把当前的TransmittableThreadLocal,记录在了holder中。这个holder,也是一个ThreadLocal。也就是每个Thread都有一个holder来记录自己有哪些TransmittableThreadLocal,这就达到了追踪Thread中有哪些TransmittableThreadLocal的目的。

==值得注意的是,holder是一个WeakHashMap,如果不是weak的,那TransmittableThreadLocal就会一直被一个强引用关联,无法被回收。==

另外,其实不止是set方法,get方法也要执行addThisToHolder,因为ThreadLocal可以在get时,调用initialValue初始化:

@Override
public final T get() {
    T value = super.get();
    if (disableIgnoreNullValueSemantics || value != null) addThisToHolder();
    return value;
}

参考 小伙伴同学们写的 TTL实际业务使用场景 与 设计实现解析的文章(写得都很好! ) transmittable-thread-local源码梳理

3.2.3 注意

  • 无论使用哪种技术,如果把ThreadLocal跨线程传递了,就意味着,ThreadLocal跨线程共享,可能有线程安全问题,需要注意。
    • 确保只读,当然就没有大问题;
    • 可以复写某些方法,让ThreadLocal深拷贝,这样就没有线程安全问题了;
    • 也可以使用一些线程安全的类,或者使用同步措施;