深入剖析ThreadLocal使用场景、实现原理、设计思想

打印 上一主题 下一主题

主题 855|帖子 855|积分 2565

前言

ThreadLocal可以用来存储线程的本地数据,做到线程数据的隔离
ThreadLocal的使用不当可能会导致内存泄漏,排查内存泄漏的问题,不仅需要熟悉JVM、利用好各种分析工具还耗费人工
如果能明白其原理并正确使用,就不会导致各种意外发生
本文将从使用场景、实现原理、内存泄漏、设计思想等层面分析ThreadLocal,并顺带聊聊InheritableThreadLocal
ThreadLocal使用场景

什么是上下文?
比如线程处理一个请求,请求会经过MVC流程,由于流程很长,会经历很多方法,这些方法就可以叫上下文
ThreadLocal作用在上下文中存储常用的数据、存储会话信息、存储线程本地变量等
比如使用拦截器在请求处理前,通过请求中的token获取登录用户信息,将用户信息存储在ThreadLocal中,方便后续处理请求时从ThreadLocal中直接获取用户信息
如果线程会重复利用,为了避免数据错乱,使用完(拦截器处理后)应该删除该数据
ThreadLocal 常用的方法有:set()、get()、remove()分别对应存储、获取和删除
可以将ThreadLocal放在工具类中方便使用
  1. public class ContextUtils {
  2.     public static final ThreadLocal<UserInfo> USER_INFO_THREAD_LOCAL = new ThreadLocal();
  3. }
复制代码
拦截器伪代码
  1. //执行前 存储
  2. public boolean postHandle(HttpServletRequest request)  {
  3.     //解析token获取用户信息
  4.         String token = request.getHeader("token");
  5.         UserInfo userInfo = parseToken(token);   
  6.         //存入
  7.         ContextUtils.USER_INFO_THREAD_LOCAL.set(userInfo);
  8.    
  9.     return true;
  10. }
  11. //执行后 删除
  12. public void postHandle(HttpServletRequest request)  {
  13.     ContextUtils.USER_INFO_THREAD_LOCAL.remove();
  14. }
复制代码
使用时
  1. //提交订单
  2. public void orderSubmit(){
  3.     //获取用户信息
  4.     UserInfo userInfo = ContextUtils.USER_INFO_THREAD_LOCAL.get();
  5.     //下单
  6.     submit(userInfo);
  7.     //删除购物车
  8.     removeCard(userInfo);
  9. }
复制代码
为了更好的使用ThreadLocal,我们应该了解其实现原理,避免使用不当造成意外发生
ThreadLocalMap

Thread 线程中有两个字段存储ThreadLocal的内部类ThreadLocalMap
  1. public class Thread implements Runnable {   
  2.    
  3.     ThreadLocal.ThreadLocalMap threadLocals = null;
  4.    
  5.     ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
  6. }
复制代码
threadLocals用于实现ThreadLocal
inheritableThreadLocals 用于实现InheritableThreadLocal (可继承的ThreadLocal 后文再聊)

ThreadLocalMap 的实现是哈希表,其内部类Entry是哈希表的节点,由Entry数组实现哈希表 ThreadLocalMap
  1. public class ThreadLocal<T> {
  2.     //,,,
  3.         static class ThreadLocalMap {
  4.         //...
  5.         static class Entry extends WeakReference<ThreadLocal<?>> {
  6.             Object value;
  7.             Entry(ThreadLocal<?> k, Object v) {
  8.                 super(k);
  9.                 value = v;
  10.             }
  11.         }
  12.         }
  13. }
复制代码
ThreadLocalMap.set

通过哈希获取下标,当发生哈希冲突时,遍历哈希表(不再使用链地址法)直到位置上没有节点再进行构建
遍历期间如果有节点,则根据节点取出key进行比较,如果是则是覆盖;如果节点没有key说明该节点的ThreadLocal被回收(已过期),为了防止内存泄漏会清理节点
最后会检查其他位置有没有已过期的节点进行清理,并检查扩容
  1. public void set(T value) {
  2.     //获取当前线程
  3.     Thread t = Thread.currentThread();
  4.     //获取当前线程的ThreadLocalMap
  5.     ThreadLocalMap map = getMap(t);
  6.    
  7.     if (map != null) {
  8.         //添加数据
  9.         map.set(this, value);
  10.     } else {
  11.         //没有就初始化
  12.         createMap(t, value);
  13.     }
  14. }
复制代码
获取哈希值时,使用哈希值自增的原子类获取,步长则是每次自增的数量(也许是经过研究、测试的,尽量减少哈希冲突)
  1.         void createMap(Thread t, T firstValue) {
  2.         t.threadLocals = new ThreadLocalMap(this, firstValue);
  3.     }
复制代码
nextIndex是获取下一个下标,超出上限时回到0
  1.         ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
  2.         //初始化数组 16
  3.         table = new Entry[INITIAL_CAPACITY];
  4.         //获取下标
  5.         int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
  6.         //构建节点
  7.         table[i] = new Entry(firstKey, firstValue);
  8.         //设置大小
  9.         size = 1;
  10.         //设置负载因子
  11.         setThreshold(INITIAL_CAPACITY);
  12.    }
复制代码
get

在获取数据时

获取当前线程的ThreadLocalMap,如果为空则初始化,否则获取节点
  1. private void set(ThreadLocal<?> key, Object value) {
  2.     //获取哈希表
  3.     Entry[] tab = table;
  4.     int len = tab.length;
  5.     //获取下标
  6.     int i = key.threadLocalHashCode & (len-1);
  7.     //遍历 直到下标上没有节点
  8.     for (Entry e = tab[i];
  9.          e != null;
  10.          e = tab[i = nextIndex(i, len)]) {
  11.         //获取key
  12.         ThreadLocal<?> k = e.get();
  13.                 //key如果存在则覆盖
  14.         if (k == key) {
  15.             e.value = value;
  16.             return;
  17.         }
  18.                 //如果key不存在 说明该ThreadLocal以及不再使用(GC回收),需要清理防止内存泄漏
  19.         if (k == null) {
  20.             replaceStaleEntry(key, value, i);
  21.             return;
  22.         }
  23.     }
  24.     //构建节点
  25.     tab[i] = new Entry(key, value);
  26.     //计数
  27.     int sz = ++size;
  28.     //清理其他过期的槽,如果满足条件进行扩容
  29.     if (!cleanSomeSlots(i, sz) && sz >= threshold)
  30.         rehash();
  31. }
复制代码
在获取节点时,先根据哈希值获取到下标,再查看节点,比较key;如果匹配不上则说明key过期可能发生内存泄漏要去清理哈希表
  1.         //获取哈希值
  2.     private final int threadLocalHashCode = nextHashCode();
  3.     //哈希值自增器
  4.     private static AtomicInteger nextHashCode =
  5.         new AtomicInteger();
  6.     //增长步长
  7.     private static final int HASH_INCREMENT = 0x61c88647;
  8.     //获取哈希值
  9.     private static int nextHashCode() {
  10.         return nextHashCode.getAndAdd(HASH_INCREMENT);
  11.     }
复制代码
内存泄漏

在设置、获取数据的过程中,都会去判断key是否过期,如果过期就清理
实际上ThreadLocal使用不当是会造成内存泄漏的
设计者为了避免使用不当导致的内存泄漏,在常用方法中尽量清理这些过期的ThreadLocal
前文说过节点继承弱引用,在构造中设置key为弱引用(也就是ThreadLocal)
当ThreadLocal在任何地方都不被使用时,下次GC会将节点的key设置为空
如果value也不再使用,但是由于节点Entry(null,value)存在,就无法回收value,导致出现内存泄漏

因此使用完数据后,尽量使用remove进行删除
并且设计者在set、get、remove等常用方法中都会检查key为空的节点并删除,避免内存泄漏
设计思想

为什么要把entry中的key,也就是ThreadLocal设置成弱引用?
我们先想象一个场景:线程在我们的服务中经常重复利用,而在某些场景下ThreadLocal并不长期使用
如果节点entry 的key、value都是强引用,一但不再使用ThreadLocal,那么这个ThreadLocal还作为强引用存储在节点中,那么就无法回收,相当于发生内存泄漏
把ThreadLocal设置为弱引用后,这种场景下如果value也不再使用依旧会发生内存泄漏,因此在set、get、remove方法中都会区检查删除key为空的节点,避免内存泄漏
既然value可能无法回收,为什么不把value也设置成弱引用?
由于value存储的是线程隔离的数据,如果将value设置成弱引用,当外层也不使用value对应的对象时,它就没有强引用了,再下次gc被回收,导致数据丢失
InheritableThreadLocal

InheritableThreadLocal 继承 ThreadLocal 用于父子线程间的线程变量传递
  1.                 private static int nextIndex(int i, int len) {
  2.             return ((i + 1 < len) ? i + 1 : 0);
  3.         }
复制代码
前文说过线程中另一个ThreadLocalMap就是用于InheritableThreadLocal 的
在创建线程时,如果父线程中inheritableThreadLocals 不为空 则传递
  1.         public T get() {
  2.         //获取当前线程
  3.         Thread t = Thread.currentThread();
  4.         //获取线程的ThreadLocalMap
  5.         ThreadLocalMap map = getMap(t);
  6.         if (map != null) {
  7.             //获取节点
  8.             ThreadLocalMap.Entry e = map.getEntry(this);
  9.             if (e != null) {
  10.                 @SuppressWarnings("unchecked")
  11.                 T result = (T)e.value;
  12.                 return result;
  13.             }
  14.         }
  15.         //初始化(懒加载)
  16.         return setInitialValue();
  17.     }
复制代码
总结

ThreadLocal 用于隔离线程间的数据,可以存储数据作用在上下文中,由于线程可能重复利用,使用后需要删除,避免出现数据混乱
Thread线程中存储ThreadLocalMap,ThreadLocalMap是一个使用开放定址法解决哈希冲突的哈希表,其中节点存储Key是ThreadLocal,Value存储的是线程要存储数据
节点继承弱引用,并设置ThreadLocal为弱引用,这就导致当ThreadLocal不再使用时,下次GC会将其回收,此时Key为空,如果Value也不再使用,但是节点未删除就会导致value被使用,从而导致内存泄漏
在ThreadLocal的set、get、remove等常用方法中,遍历数组的同时还回去将过期的节点(key为空)进行删除,避免内存泄漏
如果将ThreadLocal设置成强引用,当ThreadLocal不再使用时会发生内存泄漏;将ThreadLocal设置成弱引用时,虽然也可能发生内存泄漏,但可以在常用方法中检查并清理这些数据;如果将value设置成弱引用,当外层不使用value时会发生数据丢失
InheritableThreadLocal继承ThreadLocal ,用于父子线程间的ThreadLocal数据传递
最后(不要白嫖,一键三连求求拉~)

本篇文章被收入专栏 由点到线,由线到面,深入浅出构建Java并发编程知识体系,感兴趣的同学可以持续关注喔
本篇文章笔记以及案例被收入 gitee-StudyJavagithub-StudyJava 感兴趣的同学可以stat下持续关注喔~
案例地址:
Gitee-JavaConcurrentProgramming/src/main/java/G_ThreadLocal
Github-JavaConcurrentProgramming/src/main/java/G_ThreadLocal
有什么问题可以在评论区交流,如果觉得菜菜写的不错,可以点赞、关注、收藏支持一下~
关注菜菜,分享更多干货,公众号:菜菜的后端私房菜
本文由博客一文多发平台 OpenWrite 发布!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

雁过留声

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表