虽然ThreadLocal与并发问题相关,可是许多程序员仅仅将它做为一种用于“方便传参”的工具,胖哥认为这也许并非ThreadLocal设计的目的,它自己是为线程安全和某些特定场景的问题而设计的。
ThreadLocal是什么呢!
每一个ThreadLocal能够放一个线程级别的变量,可是它自己能够被多个线程共享使用,并且又能够达到线程安全的目的,且绝对线程安全。
例如:java
[java] view plain copy程序员
RESOURCE表明一个能够存放String类型的ThreadLocal对象,此时任何一个线程能够并发访问这个变量,对它进行写入、读取操做,都是线程安全的。好比一个线程经过RESOURCE.set(“aaaa”);将数据写入ThreadLocal中,在任何一个地方,均可以经过RESOURCE.get();将值获取出来。
可是它也并不完美,有许多缺陷,就像你们依赖于它来作参数传递同样,接下来咱们就来分析它的一些很差的地方。
为何有些时候会将ThreadLocal做为方便传递参数的方式呢?例如当许多方法相互调用时,最初的设计可能没有想太多,有多少个参数就传递多少个变量,那么整个参数传递的过程就是零散的。进一步思考:若A方法调用B方法传递了8个参数,B方法接下来调用C方法->D方法->E方法->F方法等只须要5个参数,此时在设计API时就涉及5个参数的入口,这些方法在业务发展的过程当中被许多地方所复用。
某一天,咱们发现F方法须要加一个参数,这个参数在A方法的入口参数中有,此时,若是要改中间方法牵涉面会很大,并且不知道修改后会不会有Bug。做为程序员的咱们可能会随性一想,ThreadLocal反正是全局的,就放这里吧,确实好解决。
可是此时你会发现系统中这种方式有点像在贴补丁,越贴越多,咱们必需要求调用相关的代码都使用ThreadLocal传递这个参数,有可能会搞得乱七八糟的。换句话说,并非不让用,而是咱们要明确它的入口和出口是可控的。
诡异的ThreadLocal最难琢磨的是“做用域”,尤为是在代码设计之初很乱的状况下,若是再增长许多ThreadLocal,系统就会逐渐变成神龙见首不见尾的状况。有了这样一个省事的东西,可能许多小伙伴更加不在乎设计,由于你们都认为这些问题均可以经过变化的手段来解决。胖哥认为这是一种恶性循环。
对于这类业务场景,应当提早有所准备,须要粗粒度化业务模型,即便要用ThreadLocal,也不是加一个参数就加一个ThreadLocal变量。例如,咱们能够设计几种对象来封装入口参数,在接口设计时入口参数都以对象为基础。
也许一个类没法表达全部的参数意思,并且那样容易致使强耦合。
一般咱们按照业务模型分解为几大类型对象做为它们的参数包装,而且将按照对象属性共享状况进行抽象,在继承关系的每个层次各自扩展相应的参数,或者说加参数就在对象中加,共享参数就在父类中定义,这样的参数就逐步规范化了。
咱们回到正题,探讨一下ThreadLocal究竟是用来作什么的?为此咱们探讨下文中的几个话题。spring
为了说明ThreadLocal的应用场景,咱们来看一个框架的例子。Spring的事务管理器经过AOP切入业务代码,在进入业务代码前,会根据对应的事务管理器提取出相应的事务对象,假如事务管理器是DataSourceTransactionManager,就会从DataSource中获取一个链接对象,经过必定的包装后将其保存在ThreadLocal中。而且Spring也将DataSource进行了包装,重写了其中的getConnection()方法,或者说该方法的返回将由Spring来控制,这样Spring就能让线程内屡次获取到的Connection对象是同一个。
为何要放在ThreadLocal里面呢?由于Spring在AOP后并不能向应用程序传递参数,应用程序的每一个业务代码是事先定义好的,Spring并不会要求在业务代码的入口参数中必须编写Connection的入口参数。此时Spring选择了ThreadLocal,经过它保证链接对象始终在线程内部,任什么时候候都能拿到,此时Spring很是清楚何时回收这个链接,也就是很是清楚何时从ThreadLocal中删除这个元素(在9.2节中会详细讲解)。
从Spring事务管理器的设计上能够看出,Spring利用ThreadLocal获得了一个很完美的设计思路,同时它在设计时也十分清楚ThreadLocal中元素应该在何时删除。由此,咱们简单地认为ThreadLocal尽可能使用在一个全局的设计上,而不是一种打补丁的间接方法。
了解了基本应用场景后,接下来看一个例子。定义一个类用于存放静态的ThreadLocal对象,经过多个线程并行地对ThreadLocal对象进行set、get操做,并将值进行打印,来看看每一个线程本身设置进去的值和取出来的值是不是同样的。代码以下:
代码清单5-8 简单的ThreadLocal例子安全
[java] view plain copy多线程
关于这段代码,咱们先说几点。
◎ 定义了两个ThreadLocal变量,最终的目的就是要看最后两个值是否能对应上,这样才有机会证实ThreadLocal所保存的数据多是线程私有的。
◎ 使用两个内部类只是为了使测试简单,方便你们直观理解,你们也能够将这个例子的代码拆分到多个类中,获得的结果是相同的。
◎ 测试代码更像是为了方便传递参数,由于它确实传递参数很方便,但这仅仅是为了测试。
◎ 在finally里面有remove()操做,是为了清空数据而使用的。为什么要清空数据,在后文中会继续介绍细节。
测试结果以下:
线程-6: value = (6)
线程-9: value = (9)
线程-0: value = (0)
线程-10: value = (10)
线程-12: value = (12)
线程-14: value = (14)
线程-11: value = (11)
线程-3: value = (3)
线程-5: value = (5)
线程-13: value = (13)
线程-2: value = (2)
线程-4: value = (4)
线程-8: value = (8)
线程-7: value = (7)
线程-1: value = (1)
你们能够看到输出的线程顺序并不是最初定义线程的顺序,理论上能够说明多线程应当是并发执行的,可是依然能够保持每一个线程里面的值是对应的,说明这些值已经达到了线程私有的目的。
不是说共享变量没法作到线程私有吗?它又是如何作到线程私有的呢?这就须要咱们知道一点点原理上的东西,不然用起来也没那么放心,请看下面的介绍。并发
从前面的操做能够发现,ThreadLocal最多见的操做就是set、get、remove三个动做,下面来看看这三个动做到底作了什么事情。首先看set操做,源码片断如图5-5所示。
图5-5 ThreadLcoal.set源码片断
图5-5中的第一条代码取出了当前线程t,而后调用getMap(t)方法时传入了当前线程,换句话说,该方法返回的ThreadLocalMap和当前线程有点关系,咱们先记录下来。进一步断定若是这个map不为空,那么设置到Map中的Key就是this,值就是外部传入的参数。这个this是什么呢?就是定义的ThreadLocal对象。
代码中有两条路径须要追踪,分别是getMap(Thread)和createMap(Thread , T)。首先来看看getMap(t)操做,如图5-6所示。框架
图5-6 getMap(Thread)操做
在这里,咱们看到ThreadLocalMap其实就是线程里面的一个属性,它在Thread类中的定义是:
ThreadLocal.ThreadLocalMap threadLocals = null;
这种方法很容易让人混淆,由于这个ThreadLocalMap是ThreadLocal里面的内部类,放在了Thread类里面做为一个属性而存在,ThreadLocal自己成为这个Map里面存放的Key,用户输入的值是Value。太乱了,理不清楚了,画个图来看看(见图5-7)。
简单来说,就是这个Map对象在Thread里面做为私有的变量而存在,因此是线程安全的。ThreadLocal经过Thread.currentThread()获取当前的线程就能获得这个Map对象,同时将自身做为Key发起写入和读取,因为将自身做为Key,因此一个ThreadLocal对象就能存放一个线程中对应的Java对象,经过get也天然能找到这个对象。异步
图5-7 Thread与ThreadLocal的伪代码关联关系
若是尚未理解,则能够将思惟放宽一点。当定义变量String a时,这个“a”其实只是一个名称(在第3章中已经说到了常量池),虚拟机须要经过符号表来找到相应的信息,而这种方式正好就像一种K-V结构,底层的处理方式也确实很接近这样,这里的处理方式是显式地使用Map来存放数据,这也是一种实现手段的变通。
如今有了思路,继续回到上面的话题,为了验证前面的推断和理解,来看看createMap方法的细节,如图5-8所示。
图5-8 createMap操做
这段代码是执行一个建立新的Map的操做,而且将第一个值做为这个Map的初始化值,因为这个Map是线程私有的,不可能有另外一个线程同时也在对它作put操做,所以这里的赋值和初始化是绝对线程安全的,也同时保证了每个外部写入的值都将写入到Map对象中。
最后来看看get()、remove()代码,或许看到这里就能够认定咱们的理论是正确的,如图5-9所示。
图5-9 get()/remove()方法的代码片断
给咱们的感受是,这样实现是一种技巧,而不是一种技术。
实际上是技巧仍是技术彻底是从某种角度来看的,或者说是从某种抽象层次来看的,若是这段代码在C++中实现,难道就叫技术,不是技巧了吗?固然不是!胖哥认为技术依然是创建在思想和方法基础上的,只是看实现的抽象层次在什么级别。就像在本书中多个地方探讨的一些基础原理同样,咱们探讨了它的思想,其实它的实现也是基于某种技巧和手段的,只是对程序封装后就变成了某种语法和API,所以胖哥认为,一旦学会使用技巧思考问题,就学会了经过技巧去看待技术自己。咱们应当经过这种设计,学会一种变通和发散的思惟,学会理解各类各样的场景,这样即可以积累许多真正的财富,这些财富不是经过某些工具的使用或测试就能够得到的。
ThreadLocal的这种设计很完美吗?
不是很完美,它依然有许多坑,在这里对它容易误导程序员当成传参工具就再也不多提了,下面咱们来看看它的使用不当会致使什么技术上的问题。工具
经过上面的分析,咱们能够认识到ThreadLocal实际上是与线程绑定的一个变量,如此就会出现一个问题:若是没有将ThreadLocal内的变量删除(remove)或替换,它的生命周期将会与线程共存。所以,ThreadLocal的一个很大的“坑”就是当使用不当时,致使使用者不知道它的做用域范围。
你们可能认为线程结束后ThreadLocal应该就回收了,若是线程真的注销了确实是这样的,可是事实有可能并不是如此,例如在线程池中对线程管理都是采用线程复用的方法(Web容器一般也会采用线程池),在线程池中线程很难结束甚至于永远不会结束,这将意味着线程持续的时间将不可预测,甚至与JVM的生命周期一致。那么相应的ThreadLocal变量的生命周期也将不可预测。
也许系统中定义少许几个ThreadLocal变量也无所谓,由于每次set数据时是用ThreadLocal自己做为Key的,相同的Key确定会替换原来的数据,原来的数据就能够被释放了,理论上不会致使什么问题。但世事无绝对,若是ThreadLocal中直接或间接包装了集合类或复杂对象,每次在同一个ThreadLocal中取出对象后,再对内容作操做,那么内部的集合类和复杂对象所占用的空间可能会开始膨胀。
抛开代码自己的问题,举一个极端的例子。若是不想定义太多的ThreadLocal变量,就用一个HashMap来存放,这貌似没什么问题。因为ThreadLocal在程序的任何一个地方均可以用获得,在某些设计不当的代码中很难知道这个HashMap写入的源头,在代码中为了保险起见,一般会先检查这个HashMap是否存在,若不存在,则建立一个HashMap写进去;若存在,一般也不会替换掉,由于代码编写者一般会“惧怕”由于这种替换会丢掉一些来自“其余地方写入HashMap的数据”,从而致使许多不可预见的问题。
在这样的状况下,HashMap第一次放入ThreadLocal中也许就一直不会被释放,而这个HashMap中可能开始存放许多Key-Value信息,若是业务上存放的Key值在不断变化(例如,将业务的ID做为Key),那么这个HashMap就开始不断变长,而且极可能在每一个线程中都有一个这样的HashMap,逐渐地造成了间接的内存泄漏。曾经有不少人吃过这个亏,并且吃亏的时候发现这样的代码可能不是在本身的业务系统中,而是出如今某些二方包、三方包中(开源并不保证没有问题)。
要处理这种问题很复杂,不过首先要保证本身编写的代码是没问题的,要保证没问题不是说咱们不去用ThreadLocal,甚至不去学习它,由于它确定有其应用价值。在使用时要明白ThreadLocal最难以捉摸的是“不知道哪里是源头”(一般是代码设计不当致使的),只有知道了源头才能控制结束的部分,或者说咱们从设计的角度要让ThreadLocal的set、remove善始善终,一般在外部调用的代码中使用finally来remove数据,只要咱们仔细思考和抽象是能够达到这个目的的。有些是二方包、三方包的问题,对于这些问题咱们须要学会的是找到问题的根源后解决,关于二方包、三方包的运行跟踪,可参看第3.7.9节介绍的BTrace工具。
补充:在任何异步程序中(包括异步I/O、非阻塞I/O),ThreadLocal的参数传递是不靠谱的,由于线程将请求发送后,就再也不等待远程返回结果继续向下执行了,真正的返回结果获得后,处理的线程多是另外一个。学习
#####################################我的总结 ####################################
Thread.java源码中:
[java] view plain copy
即:每一个Thread对象都有一个ThreadLocal.ThreadLocalMap成员变量,ThreadLocal.ThreadLocalMap是一个ThreadLocal类的静态内部类(以下所示),因此Thread类能够进行引用.
[java] view plain copy
因此每一个线程都会有一个ThreadLocal.ThreadLocalMap对象的引用
当在ThreadLocal中进行设值的时候:
[java] view plain copy
[java] view plain copy
首先获取当前线程的引用,而后获取当前线程的ThreadLocal.ThreadLocalMap对象(t.threadLocals变量就是ThreadLocal.ThreadLocalMap的变量),若是该对象为空就建立一个,以下所示:
[java] view plain copy
这个this变量就是ThreadLocal的引用,对于同一个ThreadLocal对象每一个线程都是相同的,可是每一个线程各自有一个ThreadLocal.ThreadLocalMap对象保存着各自ThreadLocal引用为key的值,因此互不影响,并且:若是你新建一个ThreadLocal的对象,这个对象仍是保存在每一个线程同一个ThreadLocal.ThreadLocalMap对象之中,由于一个线程只有一个ThreadLocal.ThreadLocalMap对象,这个对象是在第一个ThreadLocal第一次设值的时候进行建立,如上所述的createMap方法.
[java] view plain copy
总结:
深刻研究java.lang.ThreadLocal类:http://blog.csdn.net/xiaohulunb/article/details/19603611
API说明:
ThreadLocal(),T get(),protected T initialValue(),void remove(),void set(T value)
典型实例:
1.Hiberante的Session 工具类HibernateUtil
2.经过不一样的线程对象设置Bean属性,保证各个线程Bean对象的独立性。
ThreadLocal使用的通常步骤:
[plain] view plain copy
与Synchonized的对比:
[plain] view plain copy
一句话理解ThreadLocal:向ThreadLocal里面存东西就是向它里面的Map存东西的,而后ThreadLocal把这个Map挂到当前的线程底下,这样Map就只属于这个线程了。
使用ThreadLocal改进你的层次的划分(spring事务的实现):http://blog.csdn.net/zhouyong0/article/details/7761835
源码剖析之ThreadLocal:http://wangxinchun.iteye.com/blog/1884228
Java中的ThreadLocal源码解析(上):http://maosidiaoxian.iteye.com/blog/1939142
ThreadLocal与synchronized:http://blog.csdn.net/yangairong1984/article/details/2294572
Java线程:深刻ThreadLocal:http://lavasoft.blog.51cto.com/62575/258459(一个ThreadLocal的模拟实现)
Java多线程(六)、ThreadLocal类:http://blog.csdn.net/lonelyroamer/article/details/7998137