共计 14035 个字符,预计需要花费 36 分钟才能阅读完成。
- 为什么要用 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();}
}
- 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 的对象是类共用的,可能呈现在办法区。
- 为什么用 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 对象。
如下图所示:
- 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 没被设计成弱援用的起因。
- 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 将会长期存在上来,会导致内存泄露。 - 如何解决内存泄露问题?
后面说过的 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 及时回收,无需触发额定的清理机制,所以它能解决内存泄露问题。
- 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 为空为止。
- 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 倍的大小扩容。
- 父子线程如何共享数据?
后面介绍的 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 设置的值,拷贝一份到子线程中。
- 线程池中如何共享数据?
在实在的业务场景中,个别很少用独自的线程,绝大多数,都是用的线程池。
那么,在线程池中如何共享 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 了。
如果你仔细观察这个例子,你可能会发现,代码中除了应用 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 数据。
- ThreadLocal 有哪些用处?
上面列举几个常见的场景:
1. 在 spring 事务中,保障一个线程下,一个事务的多个操作拿到的是一个 Connection。
2. 在 hiberate 中治理 session。
3. 在 JDK8 之前,为了解决 SimpleDateFormat 的线程平安问题。
4. 获取以后登录用户上下文。
5. 长期保留权限数据。
6. 应用 MDC 保留日志信息。