> 文档中心 > ThreadLocal 原理与实战

ThreadLocal 原理与实战

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

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

原理

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

ThreadLocal 原理与实战

 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等等)请关注我的公众号
互联网技术专栏