一篇文章教你彻底理解ThreadLocal
创始人
2024-06-01 19:45:01
0

文章目录

  • ThreadLocal是什么?
  • ThreadLocal如何使用?
    • 特别注意
  • ThreadLocal数据存储
  • ThreadLocal原理解析
    • Thread.threadLocals
      • 原理
    • Thread.inheritableThreadLocals
      • 原理
  • ThreadLocal内存泄漏
    • 内存泄漏原因
    • 对内存泄漏的补救
    • 用完就要删除(最终解决)
  • 总结


ThreadLocal是什么?

ThreadLocal是用于多线程环境下保证数据安全的一种辅助类(存储资源数据),它最底层是基于Entry类型的数组存储数据,中间会包一层ThreadLocalMap。其依赖顺序为:ThreadLocal->ThreadLocalMap->Entry[]

从使用层面来看,其实就是个存储数据的类,用ThreadLocal存储的对象,对于每个线程来说都有一份独立的数据。比如说:你往ThreadLocal存入一个对象A后,每个访问ThreadLocal的线程都各自取出对象A,线程相互间修改数据后互不干扰,保证了数据安全。


ThreadLocal如何使用?

实际使用上ThreadLocal一般都是作为类的静态常量去声明使用

private static final ThreadLocal contextHolder = new ThreadLocal<>();

ThreadLocal有3个可调用的方法:get()、set()、remove() 分别对应往ThreadLocal里存对象、取对象以及删除对象。
在这里插入图片描述

特别注意

如果在线程池中使用ThreadLocal,在操作完后一定要调用remove()方法清除当前ThreadLocal数据,否则会引起内存泄露。 具体引起泄露原因放在下面分析!

ThreadLocal数据存储

有些人对ThreadLocal有一些简单的误区,认为ThreadLocal底层是由Map构成。这个说法不准确。上面介绍过说ThreadLocal最底层是由Entry数组构成,这里的Entry跟HashMap里的Entry没有关系。

在这里插入图片描述

ThreadLocal底层存储数据直接存储在Entry数组中,大概的流程是这样:先对key(当前ThreadLocal对象)进行hash,计算出需要存储的Entry数组下标,判断对应数组下标是否有数据,如果没有则直接存储。如果有,则以当前数组下标起点往后遍历寻找数据为空的下标,找到了就存储,存储格式为:(key=当前ThreadLocal对象 、 value=要存储的值)。如果直到数组末尾还未找到为空的数组下标位置。那么就会停止。

  • ThreadLocal hash冲突后往数组后面遍历寻找数据为空的下标存储
  • HashMap hash冲突后,原数组下标位置生成一条链表(or 红黑树)往链表下面存储或遍历

先对key(当前ThreadLocal对象)进行hash,计算出被存储的数组下标位置。然后这里会有两种情况:

  • 判断当前数组下标内数据的key == 用于进行hash的当前ThreadLocal对象。则直接取出当前数组下标数据的value返回
  • 判断当前数组下标内数据的key != 用于进行hash的当前ThreadLocal对象。则往后遍历并判断找出数组下标内数据的key == 用于进行hash的当前ThreadLocal对象然后返回数组下标数据的value

ThreadLocal原理解析

上面的介绍说过ThreadLocal是多线程环境下保证数据安全的一种辅助类,它的实现原理其实很简单。先说结论
ThreadLocal底层依赖Thread类下名为:threadLocals的一个变量。ThreadLocal的get、set以及remove方法都是围绕这个threadLocals 变量进行的处理。也就是说我们在操作ThreadLocal时,往ThreadLocal里存的对象实际上都会存到当前Thread线程对象下的threadLocals字段里,由于每个线程对象都有一份threadLocals变量,从而实现了线程之间的数据隔离,一定意义上保证了数据安全。

简单贴一段基于ThreadLocal的get()方法的代码验证上面说法的源码,其余的set()、remove()方法可自行阅读

  public T get() {//获取当前线程Thread t = Thread.currentThread();//从当前线程中取出数据ThreadLocalMapThreadLocalMap map = getMap(t);//这里的getMap(t);逻辑贴在下面if (map != null) {//从ThreadLocalMap中获取期望数据ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}//如果当前线程没有ThreadLocalMap(存过ThreadLocal),则初始化return setInitialValue();}//获取当前线程的threadLocals字段并返回ThreadLocalMap getMap(Thread t) {return t.threadLocals;}

Thread.threadLocals

上面介绍说过ThreadLocal实现线程数据隔离就是依靠线程类的threadLocals属性。通过阅读Thread类的部分源码可知,threadLocals属性的修饰类型其实是ThreadLocal类的一个子类ThreadLocalMap

ThreadLocal.ThreadLocalMap threadLocals = null;

原理

这里说一个因果关系再次论证上面的结论: ThreadLocal底层是基于ThreadLocalMap类型去存储数据(最下层是:entry),而实际的数据会存储到当前Thread线程的threadLocals属性中。 当前Thread线程的threadLocals属性就是ThreadLocalMap类型 所以 当前线程在对ThreadLocal进行存取删操作,实际都是在操作当前线程本身的对象属性。这就实现了线程隔离,保证了数据安全。

Thread.inheritableThreadLocals

眼尖的人可能会发现在Thread.threadLocals属性下面还有个inheritableThreadLocals的属性,它的类型也是ThreadLocal.ThreadLocalMap。那它是用来干嘛的呢?

回答这个问题之前先思考一个问题,在一般场景下ThreadLocal帮助我们隔离了线程数据,保证了数据安全,这点没问题。那如果在线程池中或者当前线程的子线程中去使用ThreadLocal,由于ThreadLocal是跟每个线程绑定且数据隔离的,那线程池或者子线程中怎么能获取到外层的ThreadLocal对象呢?例如下面这种场景:

package com.example.study.threadLocal;import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class TestThreadLocal {private static final ThreadLocal contextHolder = new ThreadLocal<>();static {contextHolder.set("yxj");}public void test(){System.out.println("正常使用"+contextHolder.get());ExecutorService executorService = Executors.newFixedThreadPool(5);executorService.execute(()->{System.out.println("线程池中使用"+contextHolder.get());});Runnable runnable = ()->{System.out.println("子线程中使用"+contextHolder.get());};Thread thread = new Thread(runnable);thread.start();}public static void main(String[] args) {TestThreadLocal testThreadLocal = new TestThreadLocal();testThreadLocal.test();}}

上面这种代码场景应该不少见,在线程池&子线程中获取外层的数据进行操作。直接输出一下结果:
在这里插入图片描述
为了解决上面这种情况,Java提供了一个名为InheritableThreadLocal的类,它继承了ThreadLocal。它重写了ThreadLocal的getMap方法

    //重写前(ThreadLocal)ThreadLocalMap getMap(Thread t) {return t.threadLocals;}//重写后(InheritableThreadLocal)ThreadLocalMap getMap(Thread t) {return t.inheritableThreadLocals;}

由原来的获取当前线程的threadLocals属性变成了获取当前线程的inheritableThreadLocals属性。也就是说如果使用了InheritableThreadLocal类,那么当你操作ThreadLocal时,实际上是操作当前线程的inheritableThreadLocals属性。

到这里你可能有些模糊,当前线程的inheritableThreadLocals属性和threadLocals属性它们都是ThreadLocalMap类型。为什么inheritableThreadLocals能解决上面那种情况的问题呢?

别急,先看效果再说原理,先将代码中ThreadLocal的实现类由ThreadLocal改成InheritableThreadLocal,其余代码保持不变
在这里插入图片描述
然后执行看下效果:
在这里插入图片描述
结果发现使用了InheritableThreadLocal就能在线程池中获取外面线程里的存进ThreadLocal的数据。实现了线程数据传递效果。

原理

InheritableThreadLocal相比其父类ThreadLocal类。达到实现线程数据传递目的的区别在于引用的当前线程属性不同。

  • InheritableThreadLocal —> Thread.inheritableThreadLocals
  • ThreadLocal —> Thread.threadLocals

但是最底层的真相是Thread线程类对于上面这两个属性有不同的处理,在创建线程对象时,“主”线程会将自身的inheritableThreadLocals属性传递给即将被创建的线程对象。但是对于threadLocals属性却不会传递。因此使用InheritableThreadLocal类能实现线程数据传递,解决了上面的那种情况。下面看代码:

  private void init(ThreadGroup g, Runnable target, String name,long stackSize, AccessControlContext acc,boolean inheritThreadLocals) {if (name == null) {throw new NullPointerException("name cannot be null");}this.name = name;//这里的parent就是当前线程(主),被创建的线程叫子线程Thread parent = currentThread();SecurityManager security = System.getSecurityManager();if (g == null) {if (security != null) {g = security.getThreadGroup();}if (g == null) {g = parent.getThreadGroup();}}g.checkAccess();      if (security != null) {if (isCCLOverridden(getClass())) {security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);}}g.addUnstarted();this.group = g;this.daemon = parent.isDaemon();this.priority = parent.getPriority();if (security == null || isCCLOverridden(parent.getClass()))this.contextClassLoader = parent.getContextClassLoader();elsethis.contextClassLoader = parent.contextClassLoader;this.inheritedAccessControlContext =acc != null ? acc : AccessController.getContext();this.target = target;setPriority(priority);//主要逻辑点在这,进行了一次主、子线程之间的数据传递,inheritThreadLocals默认是trueif (inheritThreadLocals && parent.inheritableThreadLocals != null)this.inheritableThreadLocals =ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);this.stackSize = stackSize;tid = nextThreadID();}

主要的逻辑点就是这段代码:
在这里插入图片描述

现在就能回答上面一开始的问题了,Thread.inheritableThreadLocals属性是用来干嘛的?
答:用来线程之前数据传递的,传递ThreadLocal对象。

ThreadLocal内存泄漏

ThreadLocal会发生内存泄漏的场景一般都是在线程池中使用ThreadLocal不当(线程处理完任务后没有remove掉ThreadLocal数据)导致的。

内存泄漏原因

我们先整个把ThreadLocal和Thread这两个类之间的关系串一串,首先ThreadLocal底层的存储是由<当前ThreadLocal对象, value>形式存储的,那么说明当前ThreadLocal对象直接引用着当前线程内的存储在ThreadLocal里的对象

如果当前ThreadLocal所在的对象类在GC引用链中不可达了,也就是需要被GC释放掉了。那么当GC过后,当前ThreadLocal不存在了。按照一般对象来说,引用我自身的XX对象不存在了,那么我自身也在GC引用链中不可达,我也应该被GC掉。但是!以ThreadLocal被存储的value数据来说,引用它的不止有key为当前ThreadLocal的引用。还有当前线程对象的threadLocals或者inheritableThreadLocals属性,只要当前线程对象不死亡,那么ThreadLocal中被存储的value数据也不会回收。这样就导致了本来应该需要被回收的对象,却一直无法回收(如果是在线程池中使用,那么该数据将永远无法被回收,除非线程池关闭!)。
在这里插入图片描述

小结论:

由于ThreadLocal中存储的数据被ThreadLocal本身和当前Thread线程对象双重引用,原始情况下ThreadLocal中存放的数据要被正确回收的两种强条件:

  • 当前线程死亡销毁,对ThreadLocal数据引用链 -1
  • 当前ThreadLocal对象所在的内存区域被回收,对ThreadLocal数据引用链 -1

所以如果当前线程不死亡 当前ThreadLocal所在的对象不被回收 (两个强引用) ,那么ThreadLocal中的数据将永远无法被回收。

对内存泄漏的补救

上面我们了解了内存泄漏出现的原因。ThreadLocal对这种情况进行了补救(为了避免内存泄漏),那就是存储数据进ThreadLocal时,将ThreadLocal本身作为一个WeakReference弱引用,去引用当前ThreadLocal数据。弱引用我们都知道,随时会被GC掉。所以这里就解了一个条件:不需要ThreadLocal对象所在的内存区域被回收。只需要当前线程死亡。失去最后一个强引用。剩下的弱引用则随时会被GC掉 结构如下图所示
在这里插入图片描述

所以看ThreadLocal底层Entry类的声明,会继承一个WeakReference弱引用:
在这里插入图片描述

用完就要删除(最终解决)

上面的ThreadLocal补救措施虽然解决了一条强引用问题,但是并没有解决核心的问题,因为线程池中的核心线程是不会死亡的,这样ThreadLocal内存的数据也不会被回收,也还是会有内存泄漏问题。

到这种地步,已经没有办法帮我们自动进行管理回收了。需要我们在使用完ThreadLocal后。调用remove()方法进行手动回收数据。

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);return;}}}

remove()方法会在对key(当前ThreadLocal对象)hash计算后清除对应的下标数组数据,以及以当前对应下标数组为起点,往前开始遍历,遍历到key为空(key是弱引用,随便一次GC就为空了,但是由于当前线程对象还持有引用,所以数据并不受影响)的数组下标,就将value设置为null。从而实现数据的删除,解决内存泄漏问题。

**此外:**除调用remove()会去检查并清空其他key为空的数组下标数据之外,get()、set()方法在hash冲突,往后遍历的情况下也会去检查其他key为空的数组下标数据并删除。删除数据的核心方法均为:expungeStaleEntry(int staleSlot)

private int expungeStaleEntry(int staleSlot){……}

小结论:
删除数据一前一后。调用remove()、get()、set()三个方法均会帮助我们删除已经无用的ThreadLocal数据。区别是:

  • remove()除了删除自身key所在的下标数据,还会往前遍历其他key为空的数据并删除
  • get()、set()方法不会删除自身key所在的下标数据,但是会往后遍历其他key为空的数据并删除

总结

以上就是本文对于ThreadLoca所有的总结!

相关内容

热门资讯

快评丨放大区位优势 发展赛事经... 转自:河北新闻网2025京津冀第七届公开水域游泳挑战赛暨千人横渡滹沱河活动,1000多名来自京津冀等...
纺织工业碳排放强度大幅降低 记者日前从中国纺织工业联合会获悉,近两年纺织服装行业平均碳排放强度降幅超14%,行业绿色发展步伐加快...
马里多地遇袭 军方打死80多名... 新华社达喀尔7月1日电(记者司源)巴马科消息:马里军方1日发表声明说,马里中部和西部多地当天遭到恐怖...
民航局回应充电宝新规未设缓冲期 【#民航局回应充电宝新规未设缓冲期#】日前,中国民航局发布紧急通知,自6月28日起,禁止旅客携带没有...
【方正金工】6月行业组合战胜基... (转自:市场投研资讯)本文来自方正证券研究所于2025年7月1日发布的报告《6月行业组合战胜基准0....
兰州市城关区今年将试点“均衡入... 城关区将试点“均衡入学”“派位入学”每日甘肃网7月2日讯 据兰州晚报报道 2025年城关区小学招生方...
“最讨厌的功能终于取消了”,微... 7月1日下午,#微信可以不接收共同好友点赞提醒了#冲上热搜第一。近日,微信朋友圈灰度上线“不接收共同...
扬帆新材:暂未了解到产品可直接... 投资者提问:行业内众多企业都在积极探索巯基化合物在固态电池中的应用。扬帆新材在巯基化合物应用于固态电...
国内成品油价“三连涨” 机构:... 中国网财经7月2日讯 国家发展改革委7月1日发布通知,根据近期国际市场油价变化情况,按照现行成品油价...
国泰海通证券:长期看好固态电池... 每经AI快讯,国泰海通证券研报表示,固态电池具备高能量密度、高安全性,能够满足车端、低空、人形机器人...