一篇文章教你彻底理解ThreadLocal
创始人
2024-06-01 19:09:00
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所有的总结!

相关内容

热门资讯

安卓双系统添加应用,轻松实现多... 你有没有想过,你的安卓手机里可以同时运行两个系统呢?听起来是不是很酷?想象一边是熟悉的安卓系统,一边...
pipo安卓进系统慢,探究pi... 最近是不是发现你的Pipo安卓系统更新或者运行起来特别慢?别急,今天就来给你好好分析分析这个问题,让...
怎样使用安卓手机系统,安卓手机... 你有没有发现,安卓手机已经成为我们生活中不可或缺的一部分呢?从早晨闹钟响起,到晚上睡前刷剧,安卓手机...
双系统安卓安装caj,轻松实现... 你有没有想过,你的安卓手机里装上双系统,是不是就能同时享受安卓和Windows系统的乐趣呢?没错,这...
安卓使用ios系统教程,安卓用... 你是不是也和我一样,对安卓手机上的iOS系统充满了好奇?想要体验一下苹果的优雅和流畅?别急,今天我就...
安卓系统gps快速定位,畅享便... 你有没有遇到过这样的情况:手机里装了各种地图导航软件,但每次出门前都要等上好几分钟才能定位成功,急得...
安卓手机系统更新原理,原理与流... 你有没有发现,你的安卓手机最近是不是总在提醒你更新系统呢?别急,别急,让我来给你揭秘一下安卓手机系统...
安卓系统通知管理,全面解析与优... 你有没有发现,手机里的通知就像是一群调皮的小精灵,时不时地跳出来和你互动?没错,说的就是安卓系统的通...
安卓系统手机哪买,揭秘哪里购买... 你有没有想过,拥有一部安卓系统手机是多么酷的事情呢?想象你可以自由安装各种应用,不受限制地探索各种功...
安卓系统 ipv4,基于安卓系... 你知道吗?在智能手机的世界里,有一个系统可是无人不知、无人不晓,那就是安卓系统。而在这个庞大的安卓家...
目前安卓是什么系统,探索安卓系... 亲爱的读者,你是否曾好奇过,如今安卓系统究竟是什么模样?在这个科技飞速发展的时代,操作系统如同人体的...
安卓6.0系统比5.0,从5.... 你有没有发现,自从手机更新了安卓6.0系统,感觉整个人都清爽了不少呢?没错,今天咱们就来聊聊这个话题...
安卓2.36系统升级,功能革新... 你知道吗?最近安卓系统又来了一次大变身,那就是安卓2.36系统升级!这可不是一个小打小闹的更新,而是...
安卓系统源码怎么打开,并可能需... 你有没有想过,安卓系统的源码就像是一扇神秘的门,隐藏着无数的技术秘密?想要打开这扇门,你得掌握一些小...
安卓8.0系统体验视频,智能革... 你有没有听说安卓8.0系统最近可是火得一塌糊涂啊!作为一个紧跟科技潮流的数码达人,我当然要来给你好好...
宣传系统漫画app安卓,探索安... 亲爱的读者们,你是否曾在某个午后,百无聊赖地打开手机,想要寻找一些轻松愉悦的读物?今天,我要给你介绍...
鸿蒙替换安卓系统吗,开启智能生... 你知道吗?最近科技圈里可是炸开了锅,因为华为的新操作系统鸿蒙系统,据说要大举进军手机市场,替换掉安卓...
手机安卓系统深度清理,解锁手机... 手机里的东西是不是越来越多,感觉就像一个装满了杂物的储物柜?别急,今天就来教你一招——手机安卓系统深...
安卓上的windows系统,融... 你有没有想过,在安卓手机上也能体验到Windows系统的魅力呢?没错,这就是今天我要跟你分享的神奇故...
安卓系统焦点变化事件,Andr... 你知道吗?在安卓系统的世界里,最近发生了一件超级有趣的事情——焦点变化事件。这可不是什么小打小闹,它...