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冲突的时候,有四种方法:
- 开放寻址法:当关键字key的哈希地址p=H(key)出现冲突时,以p为基础,产生另一个哈希地址p1,如果p1仍然冲突,再以p为基础,产生另一个哈希地址p2,…,直到找出一个不冲突的哈希地址pi ,将相应元素存入其中。
- 再hash:准备多种hash算法,一个冲突就换一个,直到换到合适的为止。hash算法都用完了还冲突,那就退化到其他方法,或者扩容才行了。
- 拉链法:在冲突的位置上建立一个链表,存储冲突元素。
- 建立公共溢出区:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。
开放寻址法与再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上。
- ThreadLocal本身不存在任何强引用,GC时被回收掉;
- 装载ThreadLocal对应的数据的Thread,一直存活,比如我们常用的线程池的技术;
- 没有或者较少调用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值,复制到正在执行的线程中,执行结束后移除掉。
这里面有两个问题:
- 手动执行以上步骤太麻烦,也很难让所有人都注意到这一点,显示增加以上的代码逻辑。
- 如何获取到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深拷贝,这样就没有线程安全问题了;
- 也可以使用一些线程安全的类,或者使用同步措施;