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

相关内容

热门资讯

122.(leaflet篇)l... 听老人家说:多看美女会长寿 地图之家总目录(订阅之前建议先查看该博客) 文章末尾处提供保证可运行...
育碧GDC2018程序化大世界... 1.传统手动绘制森林的问题 采用手动绘制的方法的话,每次迭代地形都要手动再绘制森林。这...
育碧GDC2018程序化大世界... 1.传统手动绘制森林的问题 采用手动绘制的方法的话,每次迭代地形都要手动再绘制森林。这...
Vue使用pdf-lib为文件... 之前也写过两篇预览pdf的,但是没有加水印,这是链接:Vu...
PyQt5数据库开发1 4.1... 文章目录 前言 步骤/方法 1 使用windows身份登录 2 启用混合登录模式 3 允许远程连接服...
Android studio ... 解决 Android studio 出现“The emulator process for AVD ...
Linux基础命令大全(上) ♥️作者:小刘在C站 ♥️个人主页:小刘主页 ♥️每天分享云计算网络运维...
再谈解决“因为文件包含病毒或潜... 前面出了一篇博文专门来解决“因为文件包含病毒或潜在的垃圾软件”的问题,其中第二种方法有...
南京邮电大学通达学院2023c... 题目展示 一.问题描述 实验题目1 定义一个学生类,其中包括如下内容: (1)私有数据成员 ①年龄 ...
PageObject 六大原则 PageObject六大原则: 1.封装服务的方法 2.不要暴露页面的细节 3.通过r...
【Linux网络编程】01:S... Socket多进程 OVERVIEWSocket多进程1.Server2.Client3.bug&...
数据结构刷题(二十五):122... 1.122. 买卖股票的最佳时机 II思路:贪心。把利润分解为每天为单位的维度,然后收...
浏览器事件循环 事件循环 浏览器的进程模型 何为进程? 程序运行需要有它自己专属的内存空间࿰...
8个免费图片/照片压缩工具帮您... 继续查看一些最好的图像压缩工具,以提升用户体验和存储空间以及网站使用支持。 无数图像压...
计算机二级Python备考(2... 目录  一、选择题 1.在Python语言中: 2.知识点 二、基本操作题 1. j...
端电压 相电压 线电压 记得刚接触矢量控制的时候,拿到板子,就赶紧去测各种波形,结...
如何使用Python检测和识别... 车牌检测与识别技术用途广泛,可以用于道路系统、无票停车场、车辆门禁等。这项技术结合了计...
带环链表详解 目录 一、什么是环形链表 二、判断是否为环形链表 2.1 具体题目 2.2 具体思路 2.3 思路的...
【C语言进阶:刨根究底字符串函... 本节重点内容: 深入理解strcpy函数的使用学会strcpy函数的模拟实现⚡strc...
Django web开发(一)... 文章目录前端开发1.快速开发网站2.标签2.1 编码2.2 title2.3 标题2.4 div和s...