ThreadLocal 原理与实战

学习更多干货类容(JAVA、前端、Kafka、redis等等)请关注我的公众号
互联网技术专栏

ThreadLocal我会将其解释为线程上下文变量,当我们想要在方法间传递参数,又不想很挫的将每个方法都参数列表都加上这个参数时,可以使用它来帮助我们隐式传递参数

原理

每个线程Thread都有一个ThreadLocalMap,ThreadLocal是这个Map的工具类。当我们通过ThreadLocal存放数据时,这个Map会添加一条记录。这条记录的key存放的是这个ThreadLocal的引用,value存的是缓存的数据(多个ThreadLocal存储不同的数据,这样这个Map就会有很多记录)

在这里插入图片描述

 public class ThreadLocal<T> {
        // ThreadLocal#set
        public void set(T value) {
            Thread t = Thread.currentThread();
            // 获取当前线程的ThreadLocalMap
            ThreadLocalMap map = getMap(t);
            if (map != null)
                // 调用ThreadLocalMap#set,如果ThreadLocalMap不会空,则设置值,key为当前的ThreadLocal对象
                map.set(this, value);
            else
                // ThreadLocalMap为空,则初始化
                createMap(t, value);
        }

         // ThreadLocal#get
        public T get() {
            Thread t = Thread.currentThread();
            // 获取当前线程的ThreadLocalMap
            ThreadLocalMap map = getMap(t);
            if (map != null) {
                // 调用ThreadLocalMap#getEntry,this即当前的ThreadLocal对象,返回vlue
                ThreadLocalMap.Entry e = map.getEntry(this);
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    T result = (T)e.value;
                    return result;
                }
            }
            // 如果获取不到值,会调用初始化方法
            return setInitialValue();
        }



        // ThreadLocalMap 每个Thread对象中都包含了一个ThreadLocalMap 
         static class ThreadLocalMap {

            // key是弱引用(指向当前ThreadLocal对象)如果这个ThreadLocal对象除了entry中的这个弱引用之外,没有其他强引用的话(e.g threadLocal == null),
            // 这个ThreadLocal对象仍然可以回收,避免了无法回收。回收之后这个entry中的key为null,后续通过调用get/remove方法清除entry
            // 但是基本上我们threadLocal都是静态变量修饰的,不会出现没有其他引用的情况,所以很鸡肋
            static class Entry extends WeakReference<ThreadLocal<?>> {
                Object value;

                Entry(ThreadLocal<?> k, Object v) {
                    super(k);
                    value = v;
                }
            }

            // 数组
            private Entry[] table;

            // ThreadLocalMap#set
            private void set(ThreadLocal<?> key, Object value) {

                Entry[] tab = table;
                int len = tab.length;
                // 计算hash值取模,既数组中的位置    & (table.length - 1) 相当于 %table.length 
                int i = key.threadLocalHashCode & (len-1);

                for (Entry e = tab[i];
                    e != null; // for循环,当数组这个位置没有entry值时,跳出循环
                    e = tab[i = nextIndex(i, len)]) { // 如果有值,说明可能出现两种情况(1、与entry中的key相同,直接覆盖 2、不相同,说明hash冲突了,则继续遍历下一个位置)
                    ThreadLocal<?> k = e.get();
                    // entry中的key相同,直接覆盖
                    if (k == key) {
                        e.value = value;
                        return;
                    }

                    if (k == null) {
                        replaceStaleEntry(key, value, i);
                        return;
                    }
                }
                // 当数组这个位置没有entry值时,把key,value封装成entry放入数组中
                tab[i] = new Entry(key, value);
                // 数组size+1
                int sz = ++size;
                if (!cleanSomeSlots(i, sz) && sz >= threshold)
                    rehash();
            }

            // ThreadLocalMap#getEntry
            private Entry getEntry(ThreadLocal<?> key) {
                // 计算hash值取模,既数组中的位置
                int i = key.threadLocalHashCode & (table.length - 1);
                Entry e = table[i];
                // 如果这个键值对的key不等于空(没有被回收),则返回这个键值对
                if (e != null && e.get() == key)
                    return e;
                else
                    // key等于空(key被回收了)则会对entry进行回收
                    return getEntryAfterMiss(key, i, e);
            }
    }
}

坑点

  • 内存泄漏 使用线程池时,线程不会销毁,threadLocal中的值仍然存在
  • 数据污染 使用线程池时,这个线程被重复利用。上一次的值没有清,导致这一次会取到上一次的结果
  • 不支持父子线程之间使用

避免问题的方法

  • 使用完调用remove方法,且尽量在try-finally块中回收
  • 使用支持父子线程的ThreadLocal(InheritableThreadLocal、阿里ttl)

使用场景

  • 用户信息
  • 链路id
  • 业务方法上下文共用的一些参数

实战

下面我记录下我使用threadLocal用过的场景和碰到过的坑

接口优化

我在公司项目中用threadLocal做过一个优化,在优化后置打印订单这个接口时发现,整个接口上下文有多次重复根据id去数据库查询订单的代码,于是我使用threadLocal缓存订单数据进行复用,跨方法进行传递这些对象会比较方便

我是这么设计的,首先我们来定义这个ThreadLocal对象具体存放的值,理论上这个值就是我们需要缓存的订单,但是为了避免出现ThreadLocal串数据的情况(双保险,为了避免没有remove的情况),我定义了一个map,key是订单的id。后续从threadlocal中拿订单时,是根据id来捞,如果串数据了,那么查到的数据则为空(空的话需要从数据库查一次),这样就能尽量避免使用threadLocal产生的风险。

// 伪代码
private static final ThreadLocal<TradePostPrintContext> TRADE_POST_PRINT_CONTEXT = new ThreadLocal<>();

class TradePostPrintContext implements Serializable {

    /**
    * 订单
    */
    public Map<Long, Trade> sid2TradeMap;

}

main() {
    try {
        method1();
        ...
        ...
        method2();
    } finally {
        TRADE_POST_PRINT_CONTEXT.remove();
    }
}

method1() {
    Map<Long, Trade> sid2TradeMap = new HashMap();
    sid2TradeMap.put(1, new Trade())
    TRADE_POST_PRINT_CONTEXT.set(new TradePostPrintContext(sid2TradeMap));
}

method2() {
    TradePostPrintContext context = TRADE_POST_PRINT_CONTEXT.get();
     Map<Long, Trade> sid2TradeMap = context.sid2TradeMap;
    Trade = sid2TradeMap.get(1) != null ? sid2TradeMap.get(1) : queryDb();
}
mybatisPlus的pageHelper踩坑

首先碰到的坑是这样的,首先代码执行了一个查询数据库的方法,但是这个方法是不分页的,方法里并没有进行分页(pageHelper)的配置。执行到这个方法的时却无缘无故调用了分页的相关sql。然后我开始观察PageHelper的源码发现,分页配置是放在ThreadLocal中的。于是我猜测是threadLocal使用完没有remove导致的。后面我看到当查询执行完毕,会调用销毁ThreadLocal的方法,照理说不会出现问题。

但是会出现一种场景,就是当我们他通过pageHelper设置完page后,没有调用查询方法,方法直接结束了,这样ThreadLocal就不会进入销毁的逻辑。后面这个线程被重复利用。因为上一次分页没有清,导致这一次原本没有分页的查询会进行分页

protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();

PageHelper.setPage(1,10);
if (param != null){
    list = userMapper.selectIf(param)
} else {
    // param为空,没有进行查询,分页信息不会清除
    list = new ArrayList<User>();
}

学习更多干货类容(JAVA、前端、Kafka、redis等等)请关注我的公众号
互联网技术专栏

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

澄风

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值