ThreadLocal详解
使用了很久的ThreadLocal,却一直停留在一知半解的地步,直到最近深入阅读源码才算真正理解了它,特此做一个全面的总结
什么是ThreadLocal?为什么要使用ThreadLocal?
背景:业务流程中总是需要用到一些有较长传参链路的变量(如订单id,店铺id),有的甚至需要贯穿整个流程(如用户id,traceId)。随着功能迭代,入参增多和调用链变长,每个方法都增加同样的公共入参,参数传递会变得非常麻烦且冗余 诉求:需要一个工具,用于存储当前请求的信息(如用户id,业务id,traceId),减少冗余的参数传递
JDK便提供了ThreadLocal类,ThreadLocal是一个线程级变量工具,可以保存当前线程的变量。使用threadLocal后的存储值依旧不会使用共享变量,不需要担心是否线程安全
如何使用(案例)
demo:在不考虑跨线程/线程池的情况下,可以将当前线程周期认为是本次请求流程,ThrealLocal暂时可以满足需求 注意:如果想要存储的值不是局部变量(如用户id,traceId),而是static变量,此时存在线程安全问题,不能使用threadLocal,线程共享与threadLocal有冲突
public class Test {
public static ThreadLocal<Long> userIdHolder = new ThreadLocal<>();
public void doSomeThingA(Long userId) {
userIdHolder.set(userId);
doSomeThingB();
userIdHolder.remove();
}
public void doSomeThingB() {
// ...
doSomeThingC();
}
public void doSomeThingC() {
Long userId = userIdHolder.get();
// ...
}
}
demo2:web应用中搭配HandlerInterceptor使用
public class AuthIntercept extends HandlerInterceptor {
@Override
public boolean preHandle(...) {
userIdHolder.set("xxx") // 值通常从header取
}
@Override
public void afterCompletion(...) {
userIdHolder.remove();
}
}
demo3:web应用再搭配Dubbo使用
public class DubboFilter implements Filter {
@Override
public Result invoke(...) throws RpcException {
if (rpcContext.isConsumerSide()) {
rpcContext.setAttachment("user_id", userIdHolder.get);
}
// invoke
}
}
public class DubboFilter implements Filter {
@Override
public Result invoke(...) throws RpcException {
if (rpcContext.isProviderSide()) {
userIdHolder.set(rpcContext.getAttachment("use_id));
}
try {
// invoke
} finally {
if (rpcContext.isProviderSide()) {
userIdHolder.remove();
}
}
}
}
MDC(Mapped Diagnostic Context): MDC是一种日志记录工具,可以在适合的时候保存需要的值到MDC,在需要记录日志时取出,内部使用ThreadLocal实现,部分源码如下
public class BasicMDCAdapter implements MDCAdapter {
private InheritableThreadLocal<Map<String, String>> inheritableThreadLocal = new InheritableThreadLocal<Map<String, String>>() {
protected Map<String, String> childValue(Map<String, String> parentValue) {
return parentValue == null ? null : new HashMap(parentValue);
}
};
}
传递数据库连接:数据库connection并不适合显式传递,MyBatis使用ThreadLocal传递当前线程的connection(SqlSession)。部分源码如下
public class SqlSessionManager implements SqlSessionFactory, SqlSession {
private final ThreadLocal<SqlSession> localSqlSession = new ThreadLocal();
}
传递SimpleDateFormat:有些项目代码中经常出现new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
,如果每个请求单独new再往下传递也不太合适,此时可以使用ThreadLocal传递
public static ThreadLocal<SimpleDateFormat> simpleDateFormatHolder = new ThreadLocal<>();
原理
每个线程的数据保存在 Thread 类的 ThreadLocal.threadLocalMap 中,将公共的ThreadLocal变量作为key访问
public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null; // 属于当前线程的ThreadLocalMap
}
public class ThreadLocal {
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数组使用斐波那契(Fibonacci)散列法,使计算结果分散的更加均匀,哈希碰撞后向后一位寻址,直到找到nullEntry或nullKey可回收位置进行存储。存储后进行探测式清理logn次,其中每1次都从i开始清理nullKey脏entry,直到遇到nullEntry,若非脏key而是有值的entry,移动到下个nullEntry

private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1); // 得到较均匀的槽下标i
Entry e = table[i];
if (e != null && e.get() == key) // 有值且就是要找的key
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) { // 继续往后直到遇到空entry
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null) // Entry失效,key已被回收,从此处开始清理无用的value,直到遇到空Entry
expungeStaleEntry(i);
else
i = nextIndex(i, len); // 有值但不是要找的key
e = tab[i];
}
return null;
}
/**
* 当前为脏entry,清理直到下个nullEntry
*/
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
tab[staleSlot].value = null; // 清理当前无用value
tab[staleSlot] = null;
size--;
Entry e;
int i;
// 往后清理脏value并rehash
for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else { // rehash到第一个为null的entry
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i; // 返回脏entry之后第一个nullEntry的下标
}
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) { // 找到entry替换原值
e.value = value;
return;
}
if (k == null) { // 替换脏entry
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value); // 当前为nullEntry,放置新entry
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold) // 没清理调脏entry,且size超2/3则rehash
rehash();
}
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
// 找到最前的脏entry
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
// 往后遍历
for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value; // 替换新值
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// 以最前脏entry/i作为清理起点
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// 没找到key,创建新entry
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// 找到其他脏entry,清理
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
/**
* 试探性寻找脏entry,检查logn次,可以检查出所有脏entry
*/
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) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
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); // 清掉当前脏entry,并清理直到下个nullEntry
return;
}
}
}
如何跨线程使用
业务中可能需要起新线程处理异步逻辑,而子线程无法获取父线程的ThreadLocal值。因此需要使用inheritableThreadLocal(ThreadLocal的子类)
public class Test {
public static ThreadLocal<Long> userIdHolder = new InheritableThreadLocal<>();
public void doSomeThingA(Long userId) throws InterruptedException {
userIdHolder.set(userId); // 父线程
new Thread(() -> {
Long userId = userIdHolder.get(); // 子线程获取
}).start();
}
}
inheritableThreadLocal实现跨线程传递的原理:在父线程创建子线程的时候,会将父线程的 InheritableThreadLocal 复制一份到子线程中
public class Thread implements Runnable {
private void init(boolean inheritThreadLocals) {
Thread parent = currentThread();
if (inheritThreadLocals && parent.inheritableThreadLocals != null) {
this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}
}
}
如何在线程池中使用
线程池中 InheritableThreadLocal 会失效,因为只有开始几个核心线程算作新建的线程,此时InheritableThreadLocal可以继承自父线程,但后面提交任务到线程池都是在复用线程,不会调用new Thread(),因此InheritableThreadLocal无法传递 解决方案:使用 TaskDecorator 给任务做切面
@Configuration
public class ThreadPoolConfig {
@Bean("threadPoolA")
public ThreadPoolTaskExecutor threadPoolA() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 其他配置...
executor.setTaskDecorator(new TestDecorator()); // 设置装饰器
executor.initialize();
return executor;
}
}
public class TestDecorator implements TaskDecorator {
public static final ThreadLocal<Long> userIdHolder = new InheritableThreadLocal<>();
@Override
public Runnable decorate(Runnable runnable) {
try {
Long userId = userIdHolder.get(); // 获取父线程信息
return () -> {
try {
userIdHolder.set(userId); // 子线程设置threadlocal
runnable.run();
} finally {
userIdHolder.remove(); // 子线程执行结束后清理threadlocal,防止下一次复用线程读取到这次的脏数据
}
};
} catch (IllegalStateException e) {
return runnable;
}
}
}
更优雅的实现:将threadLocal变量统一维护在AutoCloseable类中,使用try-with-resources写法替代try-finally,用完自行remove
public class ThreadLocalUtil implements AutoCloseable {
public static final ThreadLocal<Long> userIdHolder = new InheritableThreadLocal<>();
public ThreadLocalUtil(Long userId) {
userIdHolder.set(userId);
}
@Override
public void close() {
userIdHolder.remove();
}
}
public class TestDecorator implements TaskDecorator {
public static final ThreadLocal<Long> userIdHolder = new InheritableThreadLocal<>();
@Override
public Runnable decorate(Runnable runnable) {
try {
Long userId = ThreadLocalUtil.userIdHolder.get();
return () -> {
try (ThreadLocalUtil threadLocalUtil = new ThreadLocalUtil(userId)) {
runnable.run();
}
};
} catch (IllegalStateException e) {
return runnable;
}
}
}
ThreadLocal内存泄漏
java内存泄漏的定义
java和c++对内存泄漏的定义不同。java中可达的无用对象视为内存泄漏,而c++中不可达的无用对象属于内存泄漏

threadLocal内存泄漏的条件(定义)
- 线程必须是循环使用的。因为threadLocalMap是线程属性,若是线程消亡,threadLocal就会回收,此时不可能内存泄漏
- 开发者意识到不再需要threadLocal的value(或key+value),希望回收其内存,但错误得把threadLocalRef置null,此时value未被回收(被ThreadLocalMap持有)
需要说明一点,当开发者没有意识到需要清理threadLocal时,此时产生的内存泄漏严格来说不是threadLocal导致的。举个例子,当静态变量 public static Integer TEST = 1
无用后忘记回收产生的内存泄漏,不能算是Integer导致的,这与数据类型无关
为什么threadlocal会有内存泄漏
将threadLocalRef手动置null后,没有了外部强引用,虽然threadlocalMap的弱引用key可以被回收,但剩下的value依旧被ThreadLocalMap持有,算作内存泄漏。直到当前线程下次使用其他ThreadLocalKey的get()、set()、remove()方法,才有可能检查到nullKey并清理对应value
如何避免
- 如果不再需要value,调用remove()
- 如果不再需要key+value,先调用remove清掉当前线程的value,再将key置为null。其他线程的value此时处于内存泄漏(因为被其他线程的threadLocalMap引用),但其他线程调用不同的threadLocalKey时会检查到nullKey并清掉自己的value
是线程安全的吗?能否解决线程安全问题?
ThreadLocal与线程安全无关,也无法解决线程安全问题
- 若保存的是线程内部变量,则不存在线程安全问题,谈线程安全无意义
- 若保存的是共享变量(static),线程不安全。因为ThreadLocalMap保存的是变量引用,其他线程依旧可以修改
ThreadLocalMap的key为什么使用弱引用
4种引用类型
- 强引用(StrongReference):如果一个对象具有强引用,即使内存不足,jvm也只会抛出OutOfMemory。对于不使用的大对象,要通过object = null或跳出生命周期帮助回收
- 软引用(SoftReference):如果一个对象只有软引用,只有内存空间不足才会回收这些对象的内存
- 弱引用(WeakReference):gc如果发现对象只被弱引用时,无论内存是否足够,都会将其回收,ref置为null
- 虚引用(PhantomReference):如果一个对象仅有虚引用,在任何时候都可能被gc回收
ThreadLocal使用强引用和弱引用的区别
- 如果使用强引用,在外部强引用失效后,ThreadLocalMap中key的强引用还存在,但当前线程该entry内存未能释放且已经无法正常访问
- 反之使用弱引用,在外部强引用失效后,弱引用key会被回收。至于value,暂时处于内存泄漏,但当前线程通过操作其他threadLocal值的get(),set(),remove()方法时,会自行检查nullKey并回收其value
ThreadLocalMap的value为什么没用弱引用
既然value有可能出现内存泄漏,为什么不直接将value设为弱引用。因为在ThreadLocal.set()之后,value的原引用往往已经过了生命周期,只剩下ThreadLocalMap中的引用,如果使用弱引用后value回收,使用get()时会返回null
转载自:https://juejin.cn/post/7364768889577422888