ThreadLocal系列(二)-InheritableThreadLocal的使用及原理解析

ThreadLocal系列之InheritableThreadLocal的使用及原理解析(源码基于java8)

上一篇:ThreadLocal系列(一)-ThreadLocal的使用及原理解析

下一篇:ThreadLocal系列(三)-TransmittableThreadLocal的使用及原理解析

一、基本使用

我们继续来看之前写的例子:

private static ThreadLocal tl = new ThreadLocal<>();public static void main(String[] args) throws Exception {tl.set(1);System.out.println(String.format("当前线程名称: %s, main方法内获取线程内数据为: %s",Thread.currentThread().getName(), tl.get()));fc();new Thread(() -> {fc();}).start();Thread.sleep(1000L); //保证下面fc执行一定在上面异步代码之后执行fc(); //继续在主线程内执行,验证上面那一步是否对主线程上下文内容造成影响}private static void fc() {System.out.println(String.format("当前线程名称: %s, fc方法内获取线程内数据为: %s",Thread.currentThread().getName(), tl.get()));}

输出为:


当前线程名称: main, main方法内获取线程内数据为: 1
当前线程名称: main, fc方法内获取线程内数据为: 1
当前线程名称: Thread-0, fc方法内获取线程内数据为: null
当前线程名称: main, fc方法内获取线程内数据为: 1

我们会发现,父线程的本地变量是无法传递给子线程的,这当然是正常的,因为线程本地变量来就不应该相互有交集,但是有些时候,我们的确是需要子线程里仍然可以获取到父线程里的本地变量,现在就需要借助TL的一个子类:InheritableThreadLocal(下面简称ITL),来完成上述要求 现在我们将例子里的


private static ThreadLocal tl = new ThreadLocal<>();

改为:


private static ThreadLocal tl = new InheritableThreadLocal<>();

然后我们再来运行下结果:


当前线程名称: main, main方法内获取线程内数据为: 1
当前线程名称: main, fc方法内获取线程内数据为: 1
当前线程名称: Thread-0, fc方法内获取线程内数据为: 1
当前线程名称: main, fc方法内获取线程内数据为: 1

可以发现,子线程里已经可以获得父线程里的本地变量了。

结合之前讲的TL的实现,简单理解起来并不难,基本可以认定,是在创建子线程的时候,父线程的ThreadLocalMap(下面简称TLMap)里的值递给了子线程,子线程针对上述tl对象持有的k-v进行了copy,其实这里不是真正意义上对象copy,只是给v的值多了一条子线程TLMap的引用而已,v的值在父子线程里指向的均是同一个对象,因此任意线程改了这个值,对其他线程是可见的,为了验证这一点,我们可以改造以上测试代码:


private static ThreadLocal tl = new InheritableThreadLocal<>();private static ThreadLocal tl2 = new InheritableThreadLocal<>();public static void main(String[] args) throws Exception {tl.set(1);Hello hello = new Hello();hello.setName("init");tl2.set(hello);System.out.println(String.format("当前线程名称: %s, main方法内获取线程内数据为: tl = %s,tl2.name = %s",Thread.currentThread().getName(), tl.get(), tl2.get().getName()));fc();new Thread(() -> {Hello hello1 = tl2.get();hello1.setName("init2");fc();}).start();Thread.sleep(1000L); //保证下面fc执行一定在上面异步代码之后执行fc(); //继续在主线程内执行,验证上面那一步是否对主线程上下文内容造成影响}private static void fc() {System.out.println(String.format("当前线程名称: %s, fc方法内获取线程内数据为: tl = %s,tl2.name = %s",Thread.currentThread().getName(), tl.get(), tl2.get().getName()));}

输出结果为:


当前线程名称: main, main方法内获取线程内数据为: tl = 1,tl2.name = init
当前线程名称: main, fc方法内获取线程内数据为: tl = 1,tl2.name = init
当前线程名称: Thread-0, fc方法内获取线程内数据为: tl = 1,tl2.name = init2
当前线程名称: main, fc方法内获取线程内数据为: tl = 1,tl2.name = init2

可以确认,子线程里持有的本地变量跟父线程里那个是同一个对象。

 

二、原理分析

通过上述的测试代码,基本可以确定父线程的TLMap被传递到了下一级,那么我们基本可以确认ITL是TL派生出来专门解决线程本地变量父传子问题的,那么下面通过源码来分析一下ITL到底是怎么完成这个操作的。

先来了解下Thread类,上节说到,其实最终线程本地变量是通过TLMap存储在Thread对象内的,那么来看下Thread对象内关于TLMap的两个属性:

ThreadLocal.ThreadLocalMap threadLocals = null;ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

Thread类里其实有两个TLMap属性,第一个就是普通TL对象为其赋值,第二个则由ITL对象为其赋值,来看下TL的set方法的实现,这次针对该方法介绍下TL子类的相关方法实现:


// TL的set方法,如果是子类的实现,那么获取(getMap)和初始化赋值(createMap)都是ITL对象里的方法// 其余操作不变(因为hash计算、查找、扩容都是TLMap里需要做的,这里子类ITL只起到一个为Thread对象里哪个TLMap属性赋值的作用)public void set(T value) {Thread t = Thread.currentThread();ThreadLocal.ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);}// ITL里getMap方法的实现ThreadLocal.ThreadLocalMap getMap(Thread t) {return t.inheritableThreadLocals; //返回的其实是Thread对象的inheritableThreadLocals属性}// ITL里createMap方法的实现void createMap(Thread t, T firstValue) {// 也是给Thread的inheritableThreadLocals属性赋值t.inheritableThreadLocals = new ThreadLocal.ThreadLocalMap(this, firstValue);}

而inheritableThreadLocals里的信息通过Thread的init方法是可以被传递下去的:


// 初始化一个Thread对象时的代码段(Thread类的init方法)Thread parent = currentThread();if (parent.inheritableThreadLocals != null){ //可以看到,如果父线程存在inheritableThreadLocals的时候,会赋值给子线程(当前正在被初始化的线程)// 利用父线程的TLMap对象,初始化一个TLMap,赋值给自己的inheritableThreadLocals(这就意味着这个TLMap里的值会一直被传递下去)this.inheritableThreadLocals =ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);}// 看下TL里对应的方法static ThreadLocal.ThreadLocalMap createInheritedMap(ThreadLocal.ThreadLocalMap parentMap) {return new ThreadLocal.ThreadLocalMap(parentMap); //这里就开始初始化TLMap对象了}// 根据parentMap来进行初始化子线程的TLMap对象private ThreadLocalMap(ThreadLocal.ThreadLocalMap parentMap) {ThreadLocal.ThreadLocalMap.Entry[] parentTable = parentMap.table; //拿到父线程里的哈希表int len = parentTable.length;setThreshold(len); // 设置阈值(具体方法参考上一篇)table = new ThreadLocal.ThreadLocalMap.Entry[len];for (int j = 0; j < len; j++) {ThreadLocal.ThreadLocalMap.Entry e = parentTable[j]; //将父线程里的Entry取出if (e != null) {@SuppressWarnings("unchecked")ThreadLocal<Object> key = (ThreadLocal<Object>) e.get(); //获取keyif (key != null) {Object value = key.childValue(e.value); //获取valueThreadLocal.ThreadLocalMap.Entry c = new ThreadLocal.ThreadLocalMap.Entry(key, value); //根据k-v重新生成一个Entryint h = key.threadLocalHashCode & (len - 1); //计算哈希值while (table[h] != null)h = nextIndex(h, len); //线性探查解决哈希冲突问题(具体方法参考上一篇)table[h] = c; //找到合适的位置后进行赋值size++;}}}}// ITL里的childValue的实现protected T childValue(T parentValue) {return parentValue; //直接将父线程里的值返回}

看过上述代码后,现在关于ITL的实现我们基本上有了清晰的认识了,根据其实现性质,可以总结出在使用ITL时可能存在的问题:

1.线程不安全

写在前面:这里讨论的线程不安全对象不包含Integer等类型,因为这种对象被重新赋值,变掉的是整个引用,这里说的是那种不改变对象引用,直接可以修改其内容的对象(典型的就是自定义对象的set方法)

如果说线程本地变量是只读变量不会受到影响,但是如果是可写的,那么任意子线程针对本地变量的修改都会影响到主线程的本地变量(本质上是同一个对象),参考上面的第三个例子,子线程写入后会覆盖掉主线程的变量,也是通过这个结果,我们确认了子线程TLMap里变量指向的对象和父线程是同一个。

2.线程池中可能失效

按照上述实现,在使用线程池的时候,ITL会完全失效,因为父线程的TLMap是通过init一个Thread的时候进行赋值给子线程的,而线程池在执行异步任务时可能不再需要创建新的线程了,因此也就不会再传递父线程的TLMap给子线程了。

针对上述2,我们来做个实验,来证明下猜想:


// 为了方便观察,我们假定线程池里只有一个线程private static ExecutorService executorService = Executors.newFixedThreadPool(1);private static ThreadLocal tl = new InheritableThreadLocal<>();public static void main(String[] args) {tl.set(1);System.out.println(String.format("线程名称-%s, 变量值=%s", Thread.currentThread().getName(), tl.get()));executorService.execute(()->{System.out.println(String.format("线程名称-%s, 变量值=%s", Thread.currentThread().getName(), tl.get()));});executorService.execute(()->{System.out.println(String.format("线程名称-%s, 变量值=%s", Thread.currentThread().getName(), tl.get()));});System.out.println(String.format("线程名称-%s, 变量值=%s", Thread.currentThread().getName(), tl.get()));}

输出结果为:


线程名称-main, 变量值=1
线程名称-pool-1-thread-1, 变量值=1
线程名称-main, 变量值=1
线程名称-pool-1-thread-1, 变量值=1

会发现,并没有什么问题,和我们预想的并不一样,原因是什么呢?因为线程池本身存在一个初始化的过程,第一次使用的时候发现里面的线程数(worker数)少于核心线程数时,会进行创建线程,既然是创建线程,一定会执行Thread的init方法,参考上面提到的源码,在第一次启用线程池的时候,类似做了一次new Thread的操作,因此是没有什么问题的,父线程的TLMap依然可以传递下去。

现在我们改造下代码,把tl.set(1)改到第一次启用线程池的下面一行,然后再看看:


public static void main(String[] args) throws Exception{System.out.println(String.format("线程名称-%s, 变量值=%s", Thread.currentThread().getName(), tl.get()));executorService.execute(()->{System.out.println(String.format("线程名称-%s, 变量值=%s", Thread.currentThread().getName(), tl.get()));});tl.set(1); // 等上面的线程池第一次启用完了,父线程再给自己赋值executorService.execute(()->{System.out.println(String.format("线程名称-%s, 变量值=%s", Thread.currentThread().getName(), tl.get()));});System.out.println(String.format("线程名称-%s, 变量值=%s", Thread.currentThread().getName(), tl.get()));}

输出结果为:


线程名称-main, 变量值=null
线程名称-main, 变量值=1
线程名称-pool-1-thread-1, 变量值=null
线程名称-pool-1-thread-1, 变量值=null

很明显,第一次启用时没有递进去的值,在后续的子线程启动时就再也传递不进去了。

 

但是,在实际项目中我们大多数采用线程池进行做异步任务,假如真的需要传递主线程的本地变量,使用ITL的问题显然是很大的,因为是有极大可能性拿不到任何值的,显然在实际项目中,ITL的位置实在是尴尬,所以在启用线程池的情况下,不建议使用ITL做值传递。为了解决这种问题,阿里做了transmittable-thread-local(TTL)来解决线程池异步值传递问题,下一篇,我们将会分析TTL的用法及原理。

 

转载于:https://www.cnblogs.com/hama1993/p/10400265.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/412209.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

oracle数据库跟mysql的区别_关于Oracle数据库与MySQL数据库的几点区别

Oracle数据库与MySQL数据库的区别是本文我们主要要介绍的内容&#xff0c;接下来我们就开始介绍这部分内容&#xff0c;希望能够对您有所帮助。Oracle与MySQL的区别&#xff1a;1.在Oracle中用select * from all_users显示所有的用户&#xff0c;而在MYSQL中显示所有数据库的命…

年轻讨厌而又美丽可爱的城市

年轻讨厌而又美丽可爱的城市&#xff0d;&#xff0d;深圳。上海是什么样的&#xff1f;真要去看看。一只乌鸦坐在高高的树枝上&#xff0c;一只兔子看见后就问它&#xff1a;“我可以像你一样 坐着不动吗&#xff1f;”乌鸦说&#xff1a;“当然可以。”于是兔子便坐在树底下。…

汤姆逊灯

由 MIT (Massachusetts Institute of Technology) 哲学教授在1954年提出&#xff1a;考虑一盏开关由一个复杂的定时器控制的灯。实验开始时&#xff0c;灯是开着的&#xff0c;并且正好开一分钟。这一分钟结束时定时器把灯关闭&#xff0c;这样持续半分钟。之后&#xff0c;又把…

python def函数_Python教程之Lambda表达式知识概述

在Python中&#xff0c;除了def之外&#xff0c;还提供了一种生成函数对象的表达式形式&#xff0c;即Lambda表达式&#xff0c;它可以创建小的匿名函数&#xff0c;起到一个函数速写的作用。接下来的好程序员Python学习课程就给大家分享Lambda表达式相关的知识点。Lambda表达式…

提示以下的错误信息:“未能在设计视图中打开, 块中,以不同方式将值括起来 ”...

问题搞定&#xff0c;其实这是个string的语法问题&#xff0c;里面和外面的引号不能相同&#xff0c;否则无法识别哪个是右引号的结束点。 举个例子说吧&#xff0c;就想刚才我的 "<%#"list.aspx?typeid"DataBinder.Eval(Container.DataItem,"IDs"…

Markdown简单上手

标题 # 内容 一级标题 二级标题 三级标题 四级标题 五级标题 六级标题 字体 1. 加粗(CtrlB) **加粗** 2. 斜体(CtrlI) *斜体* 3. 斜体加粗(CtrlBI) ***斜体加粗*** 4. 删除线(CtrlT) ~~删除线~~ 加粗斜体斜体加粗删除线 引用 >引用 >>引用 引用 分割线 --- ---- ___ *…

MySQL全文索引模糊查询_mysql全文索引之模糊查询

旧版的MySQL的全文索引只能用在MyISAM表格的char、varchar和text的字段上。不过新版的MySQL5.6.24上InnoDB引擎也加入了全文索引&#xff0c;所以具体信息大家可以随时关注官网&#xff0c;下面我来谈谈mysql全文索引的用法,网上很多啦&#xff0c;我只讲讲我所了解滴部分哈&am…

html中内容超出显示省略号的方法

html中内容超出显示省略号的方法 本博客主要介绍 前端开发中文本过多&#xff0c;以省略号显示。 效果如图&#xff1a; 单行&#xff1a; <!--单行--> <p class"pl">这个属性定义溢出元素内容区的内容会如何处理。如果值为 hidden&#xff0c;当点击hid…

vue 多选自动触发_Vue,初次邂逅(二)

一、前言二、Vue常用指令2.1 什么是指令&#xff1f;指令 (Directives) 是带有 v- 前缀的特殊特性。指令特性的预期值是&#xff1a;单个 JavaScript 表达式。指令的职责是&#xff0c;当表达式的值改变时&#xff0c;将其产生的连带影响&#xff0c;响应式地作用于 DOM。 例如…

string.Empty 和 并不总是可以互换的

在 C# 中&#xff0c;大多数情况下 "" 和 string.Empty 可以互换使用。比如&#xff1a;strings "";strings2 string.Empty;if(s string.Empty) { // }但是我发现有一种情况下只能是用常数形式&#xff1a; "", 而不能使用 string.Empty 这个静…

面向对象--类

一、成员变量和局部变量的区别&#xff1a; 1. 在类中的位置不同 a. 成员变量&#xff1a;在类中方法外 b. 局部变量&#xff1a;在方法定义中或者方法声明上 2. 在内存中的位置不同 a. 成员变量&#xff1a;在堆内存&#xff08;成员变量属于对象&#xff0c;对象进堆内存&…

搜索引擎的十大秘密(收藏)

要记住&#xff0c;在大多数情况下&#xff0c;登录搜索引擎可不是宣传和推广你网站的唯一手段。要取得真正的成功&#xff0c;你还需要使用很多其他的技术和方法。然而&#xff0c;当你适当的登录到搜索引擎后&#xff0c;也同样可以为你的站点带来大量的流量&#xff0c;而你…

pythonweb服务器部署iis_IIS部署python Web(FLASK试例)

开发环境&#xff1a;python3.6 、win7、pycharm20171、安装及配置IIS控制面板中>-程序和功能>-打开或关闭WINDOWS功能配置Internet信息服务配置万维网服务2、安装URL重写组件下载安装Web平台安装程序 5.0 (WEB PLATFORM INSTALLER 5.0)安装URL Rewrite 2.03、安装wfastc…

WPF开源框架项目

好久博客未更新新博文了&#xff0c;今天介绍一个WPF开源框架MaterialDesignInXamlToolkit废话不多说先让我们来看看框架得几张截图 让我们一起来看看源代码得结构如下图 接下我们运行代码看看运行后得截图 通过查看源代码, 由于是基于原生得状态进行修改样式及动画达到, 所以引…

rust房屋建造蓝图_都说蓝图,而不是白图、红图,你知道为什么?

文学上喜欢把对未来的构想或计划&#xff0c;称为蓝图。蓝图(英文&#xff1a;blueprint)在工业上指“蓝图纸”(晒图纸的俗称)&#xff0c;因为图纸是蓝色的&#xff0c;所以被称为“蓝图”。也许是因其具有易于保存&#xff0c;不会模糊&#xff0c;不会掉色&#xff0c;不易玷…