likes
comments
collection
share

ThreadLocal详解

作者站长头像
站长
· 阅读数 56

使用了很久的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;
    }
}
ThreadLocal详解

深入ThreadLocalMap中,其Entry数组使用斐波那契(Fibonacci)散列法,使计算结果分散的更加均匀,哈希碰撞后向后一位寻址,直到找到nullEntry或nullKey可回收位置进行存储。存储后进行探测式清理logn次,其中每1次都从i开始清理nullKey脏entry,直到遇到nullEntry,若非脏key而是有值的entry,移动到下个nullEntry

ThreadLocal详解
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详解

threadLocal内存泄漏的条件(定义)

  1. 线程必须是循环使用的。因为threadLocalMap是线程属性,若是线程消亡,threadLocal就会回收,此时不可能内存泄漏
  2. 开发者意识到不再需要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

如何避免

  1. 如果不再需要value,调用remove()
  2. 如果不再需要key+value,先调用remove清掉当前线程的value,再将key置为null。其他线程的value此时处于内存泄漏(因为被其他线程的threadLocalMap引用),但其他线程调用不同的threadLocalKey时会检查到nullKey并清掉自己的value

是线程安全的吗?能否解决线程安全问题?

ThreadLocal与线程安全无关,也无法解决线程安全问题

  1. 若保存的是线程内部变量,则不存在线程安全问题,谈线程安全无意义
  2. 若保存的是共享变量(static),线程不安全。因为ThreadLocalMap保存的是变量引用,其他线程依旧可以修改

ThreadLocalMap的key为什么使用弱引用

4种引用类型

  1. 强引用(StrongReference):如果一个对象具有强引用,即使内存不足,jvm也只会抛出OutOfMemory。对于不使用的大对象,要通过object = null或跳出生命周期帮助回收
  2. 软引用(SoftReference):如果一个对象只有软引用,只有内存空间不足才会回收这些对象的内存
  3. 弱引用(WeakReference):gc如果发现对象只被弱引用时,无论内存是否足够,都会将其回收,ref置为null
  4. 虚引用(PhantomReference):如果一个对象仅有虚引用,在任何时候都可能被gc回收

ThreadLocal使用强引用和弱引用的区别

  1. 如果使用强引用,在外部强引用失效后,ThreadLocalMap中key的强引用还存在,但当前线程该entry内存未能释放且已经无法正常访问
  2. 反之使用弱引用,在外部强引用失效后,弱引用key会被回收。至于value,暂时处于内存泄漏,但当前线程通过操作其他threadLocal值的get(),set(),remove()方法时,会自行检查nullKey并回收其value

ThreadLocal详解

ThreadLocalMap的value为什么没用弱引用

既然value有可能出现内存泄漏,为什么不直接将value设为弱引用。因为在ThreadLocal.set()之后,value的原引用往往已经过了生命周期,只剩下ThreadLocalMap中的引用,如果使用弱引用后value回收,使用get()时会返回null

转载自:https://juejin.cn/post/7364768889577422888
评论
请登录