上一篇: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);
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的使用及原理解析的更多相关文章

  1. Spring Boot干货系列:(三)启动原理解析

    Spring Boot干货系列:(三)启动原理解析 2017-03-13 嘟嘟MD 嘟爷java超神学堂 前言 前面几章我们见识了SpringBoot为我们做的自动配置,确实方便快捷,但是对于新手来说 ...

  2. android黑科技系列——Apk的加固(加壳)原理解析和实现

    一.前言 今天又到周末了,憋了好久又要出博客了,今天来介绍一下Android中的如何对Apk进行加固的原理.现阶段.我们知道Android中的反编译工作越来越让人操作熟练,我们辛苦的开发出一个apk, ...

  3. 定时组件quartz系列<二>quartz的集群原理

    1.基本信息:      Quartz是一个开源的作业调度框架,它完全由java写成,并设计用于J2Se和J2EE应用中.它提供了巨大的灵活性而不牺牲简单性.你能够用它 来为执行一个作业而创建简单的或 ...

  4. SpringBoot系列二:SpringBoot自动配置原理

    主程序类的注解 @SpringBootApplication 注解,它其实是个组合注解,源码如下: @Target({ElementType.TYPE}) @Retention(RetentionPo ...

  5. ELK系列二:Elasticsearch的架构原理和配置优化

    1.Elasticsearch的数据组织架构 1.1.Elasticsearch结构概念 集群(cluster):拥有相同cluster-name的elasticsearch结点的集合(每个结点其实就 ...

  6. 【转】Spring Boot干货系列:(三)启动原理解析

    前言 前面几章我们见识了SpringBoot为我们做的自动配置,确实方便快捷,但是对于新手来说,如果不大懂SpringBoot内部启动原理,以后难免会吃亏.所以这次博主就跟你们一起一步步揭开Sprin ...

  7. (转)Spring Boot干货系列:(三)启动原理解析

    转:http://tengj.top/2017/03/09/springboot3/ 前言 前面几章我们见识了SpringBoot为我们做的自动配置,确实方便快捷,但是对于新手来说,如果不大懂Spri ...

  8. 第十二章 Jetty的工作原理解析(待续)

    Jetty的基本架构 Jetty的启动过程 接受请求 处理请求 与JBoss集成 与Tomcat的比较

  9. ThreadLocal系列(三)-TransmittableThreadLocal的使用及原理解析

    ThreadLocal系列(三)-TransmittableThreadLocal的使用及原理解析 上一篇:ThreadLocal系列(二)-InheritableThreadLocal的使用及原理解 ...

随机推荐

  1. Laravel 多态关联使用的案例

    1.实现的功能,:短信发送,需要签名和模板审核,审核结果要插进审核记录表 2,在signature(签名表模型)和 template(模板表模型)添加多态对应关系代码其实一样,代码如下: 审核记录表需 ...

  2. 获取weibo用户所有的关注列表

    1.新浪微博Python SDK笔记——获取粉丝列表或关注列表 http://www.tuicool.com/articles/VnQ3ye 2.friendships/friends关注列表 fri ...

  3. 获取iOS设备唯一标识

    [获取iOS设备唯一标识] 1.已禁用-[UIDevice uniqueIdentifier] 苹果总是把用户的隐私看的很重要.-[UIDevice uniqueIdentifier]在iOS5实际在 ...

  4. jquery对属性和特性的操作

    attribute(特性)和property(属性)是两个不同的概念.attribute表示HTML文档节点的特性,property表示DOM元素的属性 这些属性例如selectedIndex, ta ...

  5. Redis是可以安装成windows服务-开机自启 win7 64位

    其实Redis是可以安装成windows服务的,开机自启动,命令如下: redis-server --service-install redis.windows.conf 安装完之后,就可看到Redi ...

  6. SpringMVC源码解读 - HandlerMapping

    SpringMVC在请求到handler处理器的分发这步是通过HandlerMapping模块解决的.handlerMapping 还处理拦截器. 先看看HandlerMapping的继承树吧 可以大 ...

  7. android虚拟机的垃圾收集

    Dalvik :http://zh.wikipedia.org/wiki/Dalvik%E8%99%9A%E6%8B%9F%E6%9C%BA ART :http://source.android.co ...

  8. Cacti部署

    1>监控概述   通常运维人员在一个企业当中所需要管理一台或者多台服务器,或者甚至更多,特别是BAT公司或者门户级别的公司,一个人管理的服务器可能上百甚至上千台                  ...

  9. 深刻理解Java编程的7个例子

    1. 阅读下列代码回答问题(第一个Java程序,理解PATH和CLASSPATH,学会使用javac和java命令) package cn.edu.uibe;    public class Hell ...

  10. MVC v5.1 Preview 包含 web api 2.1 web pages 3.1

    Includes ASP.NET MVC 5.1, Web API 2.1, and Web Pages 3.1 preview release. This was released marked a ...