(并发编程)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
视作ThreadLocal
,value
为代码中放入的值(实际上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?#
回应开头的那个问题, ThreadLocal
的key
是弱引用,那么在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不使用弱引用
- 我们先来说说为什么要使用弱引用
在以往使用完对象后等着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;
}
}
- 为什么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
冲突则往后寻找最近的entry
为null
的槽位,遍历完成之后,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能提升转换效率- 系统链路日志等
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· Obsidian + DeepSeek:免费 AI 助力你的知识管理,让你的笔记飞起来!
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了