ThreadLocal及InheritableThreadLocal基本原理及注意项

2023-10-24 02:11

本文主要是介绍ThreadLocal及InheritableThreadLocal基本原理及注意项,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

背景

        同事在使用ThreadLocal时,需要把主线程中的ThreadLocal值传输到子线程(线程池维护)中,故使用了InheritableThreadLocal作为传输。后发现,主线程执行ThreadLocal.remove()后,子线程中的ThreadLocal并不会被remove(),导致线程池中维护的ThreadLocal存储的值一直不变。于是深入进行了研究。

ThreadLocal原理

        要讲清楚InheritableThreadLocal原理,首先要知道ThreadLocal是如何做到在线程中存储变量的,因为两者本质上是一个东西,只是InheritableThreadLocal多了一个子线程可继承的功能(部分方法实现不同)。

        直觉上理解可能认为ThreadLocal是一个Map,保存着Thread和存储变量的一个映射,就像这样Map<Thread, V>,但实际不是。

        从Thread类的源码中,我们可以看到这样一个threadLocals变量

public
class Thread implements Runnable {
.../* ThreadLocal values pertaining to this thread. This map is maintained* by the ThreadLocal class. */ThreadLocal.ThreadLocalMap threadLocals = null;
...
}

       ThreadLocalMap中里面有一个Entry类,类似我们熟知的HashMap中,也有一个Entry类一样,ThreadLocalMap中的Entry存的则是<ThreadLocal<?>, value>,保存着ThreadLocal和value的映射。

static class ThreadLocalMap {
...static class Entry extends WeakReference<ThreadLocal<?>> {Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}
...private Entry[] table;
...
}

        同时ThreadLocalMap中,有一个Entry数组table作为存储。那么我们可以画出实际上Thread和ThreadLocal的关系实际上是这样的。

        

         也就是一个Thread可以拥有多个ThreadLocal,通过ThreadLocalMap来进行存储,这就是ThreadLocal和Thread之间的关系。

        顺带补充一下,我们知道HashMap底层结构是数组+链表,用Key值的HashCode来计算数组中存放的index,用链表来解决哈希冲突,即链地址法。

        而ThreadLocalMap类似,利用ThreadLocal的hashcode来计算数组中存放的index,但是解决哈希冲突方法是,如果计算出来的index已存放了元素,那么index+1,越界时则重新回到0。当然这样最终会导致无地址可以用,再一直这样循环查找会导致死循环,所以当数组中元素超出一定阈值(threshold)时,就会进行扩容,此处便不再进行展开。

        前面讲了ThreadLocal和Thread的联系,那么调用ThreadLocal.set的时候,是如何做到保存到对应线程中的,调用get时候又是怎么取出来的。

Set方法

        首先先看到 ThreadLocal的set源码:

   public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);}ThreadLocalMap getMap(Thread t) {return t.threadLocals;}void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);}

         当调用set时,首先会获取到当前执行线程,并获取当前线程的 threadLocals 属性,即上文我们着重讲的ThreadLocalMap,若不存在则创建,然后以当前的ThreadLocal对象作为key,存入ThreadLocalMap中。大致流程图如下:

        

         这里我们也可以知道Thread中的ThreadLocalMap并不是构建出来就创建了的,而是需要用到的时候再去创建,这种懒加载的思想其实可以用到我们日常开发中,对于在某些判断分支下不需要创建的数组或对象不要提前进行创建。

Get方法

        接下来看到 ThreadLocal的get源码:

    public T get() {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}return setInitialValue();}private T setInitialValue() {T value = initialValue();Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);return value;}ThreadLocalMap getMap(Thread t) {return t.threadLocals;}void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);}

        当调用get时,首先会获取当前执行线程,从线程中获取ThreadLocalMap,再从ThreadLocalMap中获取当前ThreadLocal对应的值。当获取不到ThreadLocalMap,或者当前ThreadLocal没有值时,则进行初始化,若当前线程ThreadLocalMap不存在,则创建当前线程的ThreadLocalMap,并存放入初始值(默认为null),若已存在,则存入当前ThreadLocal和初始值至ThreadLocalMap中。大致流程图如下:

        

         图上和代码上的判定分支流程略有差异,但意思一致,initialValue() 默认return null,所以这里用null表达,可以对照源码理解。

        至此,对ThreadLocal的基本原理我们有了大致上的理解,还有常用remove方法及其他建议自行阅读源码。这里仅对自己的研究做一个总结和分享。

        那么接下来,就可以研究 InheritableThreadLocal 是如何实现可继承性的。

InheritableThreadLocal原理

        InheritableThreadLocal作用是:将在父线程中放入ThreadLocal的值,在子线程中继承使用。正常我们使用ThreadLocal是无法做到这一步的,我们可以写一段程序实验一下。

         这里我们看到输出了null,当然,这是必然的。ThreadLocal设计就是为了保存线程各自的本地变量互不干扰,如果两个线程能访问到同一个,那就变得线程不安全,也违背了它的设计。

        然后我们将ThreadLocal换成InheritableThreadLocal,再试试。

         可以看到获取ThreadLocal中的值成功了。那这样是否会导致线程不安全呢,还能叫线程本地变量么,答案是可以的。接下来看下InheritableThreadLocal的实现,我们就可以理解了。

        首先看Thread类中,可以看到有两个ThreadLocalMap,threadLocals 和inheritableThreadLocals。

        


public
class Thread implements Runnable {
.../* ThreadLocal values pertaining to this thread. This map is maintained* by the ThreadLocal class. */ThreadLocal.ThreadLocalMap threadLocals = null;/** InheritableThreadLocal values pertaining to this thread. This map is* maintained by the InheritableThreadLocal class.*/ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
...
}

        从注释可以看到,简单来说,就是 ThreadLocal 的值放到 threadLocals 里,InheritableThreadLocal 的值放到 inheritableThreadLocals 里。

        那么我们首先可以有一个概念——InheritableThreadLocal 的值会放到 Thread的 inheritableThreadLocals 属性中进行操作。

InheritableThreadLocal源码

        再来看 InheritableThreadLocal 的源码,可以看到它继承了 ThreadLocal,并重写了其中几个方法。它的可继承能力,就在它重写的几个方法中。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {protected T childValue(T parentValue) {return parentValue;}ThreadLocalMap getMap(Thread t) {return t.inheritableThreadLocals;}void createMap(Thread t, T firstValue) {t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);}
}

大致看下这几个方法:       

  • childValue(): 直接返回传入的parentValue,这个看起来有点不明所以,之后再来展开 
  • getMap(Thread t): 返回了t.inheritableThreadLocals,结合之前的get,set源码的阅读,也就算说如果你是一个InheritableThreadLocal对象,那么它会从线程的inheritableThreadLocals来执行get,set操作
  • createMap(Thread t, T firstValue): 创建一个ThreadLocalMap,并赋值给线程的inheritableThreadLocals

        从上述getMap、createMap方法,结合我们之前看到get,set源码,那就可以理解到“InheritableThreadLocal 的值会放到 Thread的 inheritableThreadLocals 属性中进行操作”这句话的意义。

childValue与parentMap浅拷贝

        那么childValue()是干嘛的呢?看下哪里会调到它,就会发现一片新的天地。顺着childValue点开调用它的地方,可以看到是一个私有构造函数。

    private ThreadLocalMap(ThreadLocalMap parentMap) {Entry[] parentTable = parentMap.table;int len = parentTable.length;setThreshold(len);table = new Entry[len];for (int j = 0; j < len; j++) {Entry e = parentTable[j];if (e != null) {@SuppressWarnings("unchecked")ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();if (key != null) {Object value = key.childValue(e.value);Entry c = new Entry(key, value);int h = key.threadLocalHashCode & (len - 1);while (table[h] != null)h = nextIndex(h, len);table[h] = c;size++;}}}}

        大致浏览一下,可以看到,它作用是把传入的 parentMap 浅拷贝到当前子线程的ThreadLocalMap中。

        那么再看下这个私有构造函数被谁调用,依次跟下来可以看到这样一个调用链。

        > new Thread(...)

               > init(...)

                       >ThreadLocal.createInheritedMap(parent.inheritableThreadLocals)

                              >ThreadLocalMap(parentMap)

       

init方法

        init方法会在构建Thread的时候执行,在 init 方法中,我们可以看到这么一段:

private void init(ThreadGroup g, Runnable target, String name,long stackSize, AccessControlContext acc,boolean inheritThreadLocals) {
...
Thread parent = currentThread();
...
if (inheritThreadLocals && parent.inheritableThreadLocals != null)this.inheritableThreadLocals =ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
...
}

        当if条件不为 false 时,那么就会触发浅拷贝,将父线程中inheritableThreadLocals中的值复制到子线程的inheritableThreadLocals中。

        这里父线程 parent = currentThread(); 如果不理解的话,想一下,我们是从当前线程再创建线程,那么当前线程其实就是父线程了。

        这里我们再来看两个条件 inheritThreadLocals 和 parent.inheritableThreadLocals != null

        首先看 inheritThreadLocals,可以看到它是由方法入参来的,那么就看下它被调的时候,传了啥。     

        这里看到有两个地方调了它,有acc(AccessControlContext)传false,没有的时候传true,这里 AccessControlContext 是什么可以自行研究,正常new Thread()的时候,默认传true。

        搞定完 inheritThreadLocals,再来看 parent.inheritableThreadLocals != null,这个条件比较好理解。InheritableThreadLocal类重写了 createMap 和 getMap 方法,将所有操作指向了 Thread.inheritableThreadLocals。

        那么只要我们在父线程的 InheritableThreadLocal,调用get或set了,那么 parent.inheritableThreadLocals != null 就成立。

        上述两个条件 inheritThreadLocals && parent.inheritableThreadLocals != null 成立后,那就会触发浅拷贝到子线程的 inheritableThreadLocals 中了,在构建完成后,子线程便可以读取到父线程中ThreadLocal中的值。

        (注:父子线程中的 inheritableThreadLocals 并不共享,依旧保持ThreadLocal特性,仅仅是将父线程 inheritableThreadLocals 的元素浅拷贝到子线程中,子线程有自己的inheritableThreadLocals)

注意项

  1. ThreadLocal与当前线程绑定,如果不用的时候需及时进行remove,否则会导致内存泄露。
  2. InheritableThreadLocal 中是将父线程中的 inheritableThreadLocals 浅拷贝到到子线程中,代码上看就算不使用也会拷贝。(如理解有误请指正)
  3. 父子线程的 inheritableThreadLocals 并不共享,如果你在父线程中执行了remove,子线程中不会受影响,依旧可以get出来。如果有使用到判空get值做判空处理,需注意。
  4. inheritableThreadLocals 只会在初始化时进行拷贝,如果使用线程池需要注意。可看下面例子。
        ThreadLocal<String> t = new InheritableThreadLocal<>();t.set("test");ExecutorService executorService = Executors.newSingleThreadExecutor();executorService.execute(() -> {System.out.println(t.get());t.remove();});// 确保上面代码执行完毕Thread.sleep(1000);// 再次执行,在同一线程中,没有新建线程,所以不会进行重新拷贝,输出为nullexecutorService.execute(() -> {System.out.println(t.get());});

        输出为:

 

        

这篇关于ThreadLocal及InheritableThreadLocal基本原理及注意项的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/272034

相关文章

购买磨轮平衡机时应该注意什么问题和技巧

在购买磨轮平衡机时,您应该注意以下几个关键点: 平衡精度 平衡精度是衡量平衡机性能的核心指标,直接影响到不平衡量的检测与校准的准确性,从而决定磨轮的振动和噪声水平。高精度的平衡机能显著减少振动和噪声,提高磨削加工的精度。 转速范围 宽广的转速范围意味着平衡机能够处理更多种类的磨轮,适应不同的工作条件和规格要求。 振动监测能力 振动监测能力是评估平衡机性能的重要因素。通过传感器实时监

SpringMVC入参绑定特别注意

1.直接在controller中定义一个变量,但是此种传输方式有一个限制就是参数名和请求中的参数名必须保持一致,否则失效。 @RequestMapping("test2")@ResponseBodypublic DBHackResponse<UserInfoVo> test2(String id , String name){UserInfoVo userInfoVo = new UserInf

防盗链的基本原理与实现

我的实现防盗链的做法,也是参考该位前辈的文章。基本原理就是就是一句话:通过判断request请求头的refer是否来源于本站。(当然请求头是来自于客户端的,是可伪造的,暂不在本文讨论范围内)。首先我们去了解下什么是HTTP Referer。简言之,HTTP Referer是header的一部分,当浏览器向web服务器发送请求的时候,一般会带上Referer,告诉服务器我是从哪个页面链接过来的,服务

argodb自定义函数读取hdfs文件的注意点,避免FileSystem已关闭异常

一、问题描述 一位同学反馈,他写的argo存过中调用了一个自定义函数,函数会加载hdfs上的一个文件,但有些节点会报FileSystem closed异常,同时有时任务会成功,有时会失败。 二、问题分析 argodb的计算引擎是基于spark的定制化引擎,对于自定义函数的调用跟hive on spark的是一致的。udf要通过反射生成实例,然后迭代调用evaluate。通过代码分析,udf在

【CSS in Depth 2 精译_023】第四章概述 + 4.1 Flexbox 布局的基本原理

当前内容所在位置(可进入专栏查看其他译好的章节内容) 第一章 层叠、优先级与继承(已完结) 1.1 层叠1.2 继承1.3 特殊值1.4 简写属性1.5 CSS 渐进式增强技术1.6 本章小结 第二章 相对单位(已完结) 2.1 相对单位的威力2.2 em 与 rem2.3 告别像素思维2.4 视口的相对单位2.5 无单位的数值与行高2.6 自定义属性2.7 本章小结 第三章 文档流与盒模型(已

AI学习指南深度学习篇-带动量的随机梯度下降法的基本原理

AI学习指南深度学习篇——带动量的随机梯度下降法的基本原理 引言 在深度学习中,优化算法被广泛应用于训练神经网络模型。随机梯度下降法(SGD)是最常用的优化算法之一,但单独使用SGD在收敛速度和稳定性方面存在一些问题。为了应对这些挑战,动量法应运而生。本文将详细介绍动量法的原理,包括动量的概念、指数加权移动平均、参数更新等内容,最后通过实际示例展示动量如何帮助SGD在参数更新过程中平稳地前进。

js基础需要注意的点

1 js中单引号和双引号都能创建字符串,但是html的元素属性规定必须用双引号,所以js优先用单引号定义字符串。

用ajax json给后台action传数据要注意的问题

必须要有get和set方法   1 action中定义bean变量,注意写get和set方法 2 js中写ajax方法,传json类型数据 3 配置action在struts2中

平时工作学习重要注意的问题

总体原则:抓住重点,条理清晰,可回溯,过程都清楚。 1 要有问题跟踪表,有什么问题,怎么解决的,解决方案。 2 要有常用操作的手册,比如怎么连sqlplus,一些常用的信息,保存好,备查。

线程--(1)ThreadLocal简单使用

一、概念 ThreadLocal概念:线程局部变量,是一种并发线程访问变量的解决方案,与synchronized等加锁不同,ThreadLocal完全不提供锁,而使用空间换取时间的方式,为每一个线程变量提供一个副本,以保证线程之间的安全,因为它们之间是相互独立的。 二、代码说明 package com.flx.king.it_201707;/*** 功能:ThreadLocal的使