ThreadLocal系列(二)-InheritableThreadLocal的使用及原理解析
一、基本使用
我们继续来看之前写的例子:
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);
else
createMap(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(); //获取key
if (key != null) {
Object value = key.childValue(e.value); //获取value
ThreadLocal.ThreadLocalMap.Entry c = new ThreadLocal.ThreadLocalMap.Entry(key, value); //根据k-v重新生成一个Entry
int 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的实现我们基本上有了清晰的认识了,根据其实现性质,可以总结出在使用ITL时可能存在的问题:
3.1:线程不安全
写在前面:这里讨论的线程不安全对象不包含Integer等类型,因为这种对象被重新赋值,变掉的是整个引用,这里说的是那种不改变对象引用,直接可以修改其内容的对象(典型的就是自定义对象的set方法)
如果说线程本地变量是只读变量不会受到影响,但是如果是可写的,那么任意子线程针对本地变量的修改都会影响到主线程的本地变量(本质上是同一个对象),参考上面的第三个例子,子线程写入后会覆盖掉主线程的变量,也是通过这个结果,我们确认了子线程TLMap里变量指向的对象和父线程是同一个。
3.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的用法及原理。
ThreadLocal系列(二)-InheritableThreadLocal的使用及原理解析的更多相关文章
- Spring Boot干货系列:(三)启动原理解析
Spring Boot干货系列:(三)启动原理解析 2017-03-13 嘟嘟MD 嘟爷java超神学堂 前言 前面几章我们见识了SpringBoot为我们做的自动配置,确实方便快捷,但是对于新手来说 ...
- android黑科技系列——Apk的加固(加壳)原理解析和实现
一.前言 今天又到周末了,憋了好久又要出博客了,今天来介绍一下Android中的如何对Apk进行加固的原理.现阶段.我们知道Android中的反编译工作越来越让人操作熟练,我们辛苦的开发出一个apk, ...
- 定时组件quartz系列<二>quartz的集群原理
1.基本信息: Quartz是一个开源的作业调度框架,它完全由java写成,并设计用于J2Se和J2EE应用中.它提供了巨大的灵活性而不牺牲简单性.你能够用它 来为执行一个作业而创建简单的或 ...
- SpringBoot系列二:SpringBoot自动配置原理
主程序类的注解 @SpringBootApplication 注解,它其实是个组合注解,源码如下: @Target({ElementType.TYPE}) @Retention(RetentionPo ...
- ELK系列二:Elasticsearch的架构原理和配置优化
1.Elasticsearch的数据组织架构 1.1.Elasticsearch结构概念 集群(cluster):拥有相同cluster-name的elasticsearch结点的集合(每个结点其实就 ...
- 【转】Spring Boot干货系列:(三)启动原理解析
前言 前面几章我们见识了SpringBoot为我们做的自动配置,确实方便快捷,但是对于新手来说,如果不大懂SpringBoot内部启动原理,以后难免会吃亏.所以这次博主就跟你们一起一步步揭开Sprin ...
- (转)Spring Boot干货系列:(三)启动原理解析
转:http://tengj.top/2017/03/09/springboot3/ 前言 前面几章我们见识了SpringBoot为我们做的自动配置,确实方便快捷,但是对于新手来说,如果不大懂Spri ...
- 第十二章 Jetty的工作原理解析(待续)
Jetty的基本架构 Jetty的启动过程 接受请求 处理请求 与JBoss集成 与Tomcat的比较
- ThreadLocal系列(三)-TransmittableThreadLocal的使用及原理解析
ThreadLocal系列(三)-TransmittableThreadLocal的使用及原理解析 上一篇:ThreadLocal系列(二)-InheritableThreadLocal的使用及原理解 ...
随机推荐
- Eclipse配置PyDev插件(配置Python环境) 及javascript相关配置
Eclipse开发Javascript环境配置(含EXTJs配置) 来自:sayo http://www.cnblogs.com/sayo/archive////.html Eclipse开发JQue ...
- K8S中RC与Deployment的区别
原文:http://fx114.net/qa-81-152379.aspx replication controller与deployment的区别 replication controller Re ...
- MarkDown语法练习笔记
MarkDown使用规则 标题Markdown 支持两种标题的语法,类 Setext 和类 atx 形式 Setext 形式:用底线的形式 Selext形式采用: 1.最高阶标题(=)2.第二阶标题( ...
- swift - 歌曲列表动画
// // ViewController.swift // songAnimation // // Created by su on 15/12/10. // Copyright © 2015 ...
- MySQL—练习2
参考链接:https://www.cnblogs.com/edisonchou/p/3878135.html 感谢博主 https://blog.csdn.net/flycat296/articl ...
- LR中的迭代次数设置
在参数化时,对于一次压力测试中均只能用一次的资源应该怎么参数化呢?就是说这些资源用了一次就不能在用了的. --参数化时,在select next row选择unique,update value o ...
- C# winform截图、web Cropper图片剪切、上传
今天又来一弹,写了个小功能,windows 桌面截图,web剪切图片上传的功能. 废话不多说,直接上图: 1.winform 截屏功能 图1 主窗体 点击全屏截图,就已经全屏截图了,截图后,图片保存在 ...
- c#设计模式之策略者模式(Strategy Pattern)
场景出发 假设存在如下游戏场景: 1:角色可以装备木剑,铁剑,魔剑3种装备,分别对怪物造成20HP,50HP,100HP伤害(未佩戴装备则无法攻击); 2角色可以向怪物攻击,一次攻击后损失角色所佩戴装 ...
- form 认证 读取
class Program { public static CookieContainer cc { get; set; } static void Main(string[] args) { str ...
- 【Oracle 12c】最新CUUG OCP-071考试题库(54题)
54.(12-15) choose the best answer: View the Exhibit and examine the structure of the ORDER_ITEMS and ...