一篇文章教你彻底理解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所有的总结!

相关内容

热门资讯

Python|位运算|数组|动... 目录 1、只出现一次的数字(位运算,数组) 示例 选项代...
张岱的人物生平 张岱的人物生平张岱(414年-484年),字景山,吴郡吴县(今江苏苏州)人。南朝齐大臣。祖父张敞,东...
西游西后传演员女人物 西游西后传演员女人物西游西后传演员女人物 孙悟空 六小龄童 唐僧 徐少华 ...
名人故事中贾岛作诗内容简介 名人故事中贾岛作诗内容简介有一次,贾岛骑驴闯了官道.他正琢磨着一句诗,名叫《题李凝幽居》全诗如下:闲...
和男朋友一起优秀的文案? 和男朋友一起优秀的文案?1.希望是惟一所有的人都共同享有的好处;一无所有的人,仍拥有希望。2.生活,...
戴玉手镯的好处 戴玉手镯好还是... 戴玉手镯的好处 戴玉手镯好还是碧玺好 女人戴玉?戴玉好还是碧玺好点佩戴手镯,以和田玉手镯为佳!相嫌滑...
依然什么意思? 依然什么意思?依然(汉语词语)依然,汉语词汇。拼音:yī    rán基本解释:副词,指照往常、依旧...
高尔基的散文诗 高尔基的散文诗《海燕》、《大学》、《母亲》、《童年》这些都是比较出名的一些代表作。
心在飞扬作者简介 心在飞扬作者简介心在飞扬作者简介如下。根据相关公开资料查询,心在飞扬是一位优秀的小说作者,他的小说作...
卡什坦卡的故事赏析? 卡什坦卡的故事赏析?讲了一只小狗的故事, 我也是近来才读到这篇小说. 作家对动物的拟人描写真是惟妙...
林绍涛为简艾拿绿豆糕是哪一集 林绍涛为简艾拿绿豆糕是哪一集第三十二集。 贾宽认为是阎帅间接导致刘映霞住了院,第二天上班,他按捺不...
小爱同学是女生吗小安同学什么意... 小爱同学是女生吗小安同学什么意思 小爱同学,小安同学说你是女生。小安是男的。
内分泌失调导致脸上长斑,怎么调... 内分泌失调导致脸上长斑,怎么调理内分泌失调导致脸上长斑,怎么调理先调理内分泌,去看中医吧,另外用好的...
《魔幻仙境》刺客,骑士人物属性... 《魔幻仙境》刺客,骑士人物属性加点魔幻仙境骑士2功1体质
很喜欢她,该怎么办? 很喜欢她,该怎么办?太冷静了!! 太理智了!爱情是需要冲劲的~不要考虑着考虑那~否则缘...
言情小说作家 言情小说作家我比较喜欢匪我思存的,很虐,很悲,还有梅子黄时雨,笙离,叶萱,还有安宁的《温暖的玄》 小...
两个以名人的名字命名的风景名胜... 两个以名人的名字命名的风景名胜?快太白楼,李白。尚志公园,赵尚志。
幼儿教育的代表人物及其著作 幼儿教育的代表人物及其著作卡尔威特的《卡尔威特的教育》,小卡尔威特,他儿子成了天才后写的《小卡尔威特...
海贼王中为什么说路飞打凯多靠霸... 海贼王中为什么说路飞打凯多靠霸气升级?凯多是靠霸气升级吗?因为之前刚到时确实打不过人家因为路飞的实力...
运气不好拜财神有用吗运气不好拜... 运气不好拜财神有用吗运气不好拜财神有没有用1、运气不好拜财神有用。2、拜财神上香前先点蜡烛,照亮人神...