Threalocal的使用及其原理
虽然现在可以说很多程序员会用ThreadLocal,但是我相信大多数程序员还不知道ThreadLocal,而使用ThreadLocal的程序员大多只是知道其然而不知其所以然,因此,使用ThreadLocal的程序员很多时候会被它导入到陷进中去,其实Java很多高级机制系列的很多东西都是一把双刃剑,也就是有利必有其弊,那么我们的方法是找到利和弊的中间平衡点,最佳的方式去解决问题。
本文首先说明ThreadLocal能做什么,然后根据功能为什么要用它,如何使用它,最后通过内部说明讲解他的坑在哪里,使用的人应该如何避免坑。
ThreadLocal的定义和用途的概述(我的理解):
它是一个线程级别变量,在并发模式下是绝对安全的变量,也是线程封闭的一种标准用法(除了局部变量外),即使你将它定义为static,它也是线程安全的。
ThreadLocal能做什么呢?
这个一句话不好说,我们不如来看看实际项目中遇到的一些困解:当你在项目中根据一些参数调用进入一些方法,然后方法再调用方法,进而跨对象调用方法,很多层次,这些方法可能都会用到一些相似的参数,例如,A中需要参数a、b、c,A调用B后,B中需要b、c参数,而B调用C方法需要a、b参数,此时不得不将所有的参数全部传递给B,以此类推,若有很多方法的调用,此时的参数就会越来越繁杂,另外,当程序需要增加参数的时候,此时需要对相关的方法逐个增加参数,是的,很麻烦,相信你也遇到过,这也是在C语言面向对象过来的一些常见处理手段,不过我们简单的处理方法是将它包装成对象传递进去,通过增加对象的属性就可以解决这个问题,不过对象通常是有意义的,所以有些时候简单的对象包装增加一些扩展不相关的属性会使得我们class的定义变得十分的奇怪,所以在这些情况下我们在架构这类复杂的程序的时候,我们通过使用一些类似于Scope的作用域的类来处理,名称和使用起来都会比较通用,类似web应用中会有context、session、request、page等级别的scope,而ThreadLocal也可以解决这类问题,只是他并不是很适合解决这类问题,它面对这些问题通常是初期并没有按照scope以及对象的方式传递,认为不会增加参数,当增加参数时,发现要改很多地方的地方,为了不破坏代码的结构,也有可能参数已经太多,已经使得方法的代码可读性降低,增加ThreadLocal来处理,例如,一个方法调用另一个方法时传入了8个参数,通过逐层调用到第N个方法,传入了其中一个参数,此时最后一个方法需要增加一个参数,第一个方法变成9个参数是自然的,但是这个时候,相关的方法都会受到牵连,使得代码变得臃肿不堪。
上面提及到了ThreadLocal一种亡羊补牢的用途,不过也不是特别推荐使用的方式,它还有一些类似的方式用来使用,就是在框架级别有很多动态调用,调用过程中需要满足一些协议,虽然协议我们会尽量的通用,而很多扩展的参数在定义协议时是不容易考虑完全的以及版本也是随时在升级的,但是在框架扩展时也需要满足接口的通用性和向下兼容,而一些扩展的内容我们就需要ThreadLocal来做方便简单的支持。
简单来说,ThreadLocal是将一些复杂的系统扩展变成了简单定义,使得相关参数牵连的部分变得非常容易,以下是我们例子说明:
spring的事务管理器中,对数据源获取的Connection放入了ThreadLocal中,程序执行完后由ThreadLocal中获取connection然后做commit和rollback,使用中,要保证程序通过DataSource获取的connection就是从spring中获取的,为什么要做这样的操作呢,因为业务代码完全由应用程序来决定,而框架不能要求业务代码如何去编写,否则就失去了框架不让业务代码去管理connection的好处了,此时业务代码被切入后,spring不会向业务代码区传入一个connection,它必须保存在一个地方,当底层通过ibatis、spring jdbc等框架获取同一个datasource的connection的时候,就会调用按照spring约定的规则去获取,由于执行过程都是在同一个线程中处理,从而获取到相同的connection,以保证commit、rollback以及业务操作过程中,使用的connection是同一个,因为只有同一个conneciton才能保证事务,否则数据库本身也是不支持的。
其实在很多并发编程的应用中,ThreadLocal起着很重要的重要,它不加锁,非常轻松的将线程封闭做得天衣无缝,又不会像局部变量那样每次需要从新分配空间,很多空间由于是线程安全,所以,可以反复利用线程私有的缓冲区。
如何使用ThreadLocal?
在系统中任意一个适合的位置定义个 ThreadLocal 变量,可以定义为 public static 类型(直接new出来一个ThreadLocal对象),要向里面放入数据就使用set(Object),要获取数据就用get()操作,删除元素就用remove(),其余的方法是非 public 的方法,不推荐使用。
下面是一个简单例子(代码片段1):
public class ThreadLocalTest2 { public final static ThreadLocal <String>TEST_THREAD_NAME_LOCAL = new ThreadLocal<String>(); public final static ThreadLocal <String>TEST_THREAD_VALUE_LOCAL = new ThreadLocal<String>(); public static void main(String[]args) {
for(int i = 0 ; i < 100 ; i++) {
final String name = "线程-【" + i + "】";
final String value = String.valueOf(i);
new Thread() {
public void run() {
try {
TEST_THREAD_NAME_LOCAL.set(name);
TEST_THREAD_VALUE_LOCAL.set(value);
callA();
}finally {
TEST_THREAD_NAME_LOCAL.remove();
TEST_THREAD_VALUE_LOCAL.remove();
}
}
}.start();
}
} public static void callA() {
callB();
} public static void callB() {
new ThreadLocalTest2().callC();
} public void callC() {
callD();
} public void callD() {
System.out.println(TEST_THREAD_NAME_LOCAL.get() + "/t=/t" + TEST_THREAD_VALUE_LOCAL.get());
}
}
这里模拟了100个线程去访问分别设置 name 和 value ,中间故意将 name 和 value 的值设置成一样,看是否会存在并发的问题,通过输出可以看出,线程输出并不是按照顺序输出,说明是并行执行的,而线程 name 和 value 是可以对应起来的,中间通过多个方法的调用,以模实际的调用中参数不传递,如何获取到对应的变量的过程,不过实际的系统中往往会跨类,这里仅仅在一个类中模拟,其实跨类也是一样的结果,大家可以自己去模拟就可以。
相信看到这里,很多程序员都对 ThreadLocal 的原理深有兴趣,看看它是如何做到的,尽然参数不传递,又可以像局部变量一样使用它,的确是蛮神奇的,其实看看就知道是一种设置方式,看到名称应该是是和Thread相关,那么废话少说,来看看它的源码吧,既然我们用得最多的是set、get和remove,那么就从set下手:
set(T obj)方法为(代码片段2):
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
首先获取了当前的线程,和猜测一样,然后有个 getMap 方法,传入了当前线程,我们先可以理解这个map是和线程相关的map,接下来如果 不为空,就做set操作,你跟踪进去会发现,这个和HashMap的put操作类似,也就是向map中写入了一条数据,如果为空,则调用createMap方法,进去后,看看( 代码片段3 ):
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
返现创建了一个ThreadLocalMap,并且将传入的参数和当前ThreadLocal作为K-V结构写入进去( 代码片段4 ):
ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
这里就不说明ThreadLocalMap的结构细节,只需要知道它的实现和HashMap类似,只是很多方法没有,也没有implements Map,因为它并不想让你通过某些方式(例如反射)获取到一个Map对他进一步操作,它是一个ThreadLocal里面的一个static内部类,default类型,仅仅在java.lang下面的类可以引用到它,所以你可以想到Thread可以引用到它。
我们再回过头来看看getMap方法,因为上面我仅仅知道获取的Map是和线程相关的,而通过 代码片段3 ,有一个t.threadLocalMap = new ThreadLocalMap(this, firstValue)的时候,相信你应该大概有点明白,这个变量应该来自Thread里面,我们根据getMap方法进去看看:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
是的,是来自于Thread,而这个Thread正好又是当前线程,那么进去看看定义就是:
ThreadLocal.ThreadLocalMap threadLocals = null;
这个属性就是在Thread类中,也就是每个Thread默认都有一个ThreadLocalMap,用于存放线程级别的局部变量,通常你无法为他赋值,因为这样的赋值通常是不安全的。
好像是不是有点乱,不着急,我们回头先摸索下思路:
1、Thread里面有个属性是一个类似于HashMap一样的东西,只是它的名字叫ThreadLocalMap,这个属性是default类型的,因此同一个package下面所有的类都可以引用到,因为是Thread的局部变量,所以每个线程都有一个自己单独的Map,相互之间是不冲突的,所以即使将ThreadLocal定义为static线程之间也不会冲突。
2、ThreadLocal和Thread是在同一个package下面,可以引用到这个类,可以对他做操作,此时ThreadLocal每定义一个,用this作为Key,你传入的值作为value,而this就是你定义的ThreadLocal,所以不同的ThreadLocal变量,都使用set,相互之间的数据不会冲突,因为他们的Key是不同的,当然同一个ThreadLocal做两次set操作后,会以最后一次为准。
3、综上所述,在线程之间并行,ThreadLocal可以像局部变量一样使用,且线程安全,且不同的ThreadLocal变量之间的数据毫无冲突。
我们继续看看get方法和remove方法,其实就简单了:
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}
通过根据当前线程调用getMap方法,也就是调用了t.threadLocalMap,然后在map中查找,注意Map中找到的是Entry,也就是K-V基本结构,因为你set写入的仅仅有值,所以,它会设置一个e.value来返回你写入的值,因为Key就是ThreadLocal本身。你可以看到map.getEntry也是通过this来获取的。
同样remove方法为:
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
同样根据当前线程获取map,如果不为空,则remove,通过this来remove。
补充下(2013-6-29),搞忘写有什么坑了,这个ThreadLocal有啥坑呢,大家从前面应该可以看出来,这个ThreadLocal相关的对象是被绑定到一个Map中的,而这个Map是Thread线程的中的一个属性,那么就有一个问题是,如果你不自己remove的话或者说如果你自己的程序中不知道什么时候去remove的话,那么线程不注销,这些被set进去的数据也不会被注销。
反过来说,写代码中除非你清晰的认识到这个对象应该在哪里set,哪里remove,如果是模糊的,很可能你的代码中不会走remove的位置去,或导致一些逻辑问题,另外,如果不remove的话,就要等线程注销,我们在很多应用服务器中,线程是被复用的,因为在内核分配线程还是有开销的,因此在这些应用中线程很难会被注销掉,那么向ThreadLocal写入的数据自然很不容易被注销掉,这些可能在我们使用某些开源框架的时候无意中被隐藏用到,都有可能会导致问题,最后发现OOM得时候数据竟然来自ThreadLocalMap中,还不知道这些数据是从哪里设置进去的,所以你应当注意这个坑,可能不止一个人掉进这个坑里去过。
Threalocal的使用及其原理的更多相关文章
- 奇异值分解(SVD)原理与在降维中的应用
奇异值分解(Singular Value Decomposition,以下简称SVD)是在机器学习领域广泛应用的算法,它不光可以用于降维算法中的特征分解,还可以用于推荐系统,以及自然语言处理等领域.是 ...
- node.js学习(三)简单的node程序&&模块简单使用&&commonJS规范&&深入理解模块原理
一.一个简单的node程序 1.新建一个txt文件 2.修改后缀 修改之后会弹出这个,点击"是" 3.运行test.js 源文件 使用node.js运行之后的. 如果该路径下没有该 ...
- 线性判别分析LDA原理总结
在主成分分析(PCA)原理总结中,我们对降维算法PCA做了总结.这里我们就对另外一种经典的降维方法线性判别分析(Linear Discriminant Analysis, 以下简称LDA)做一个总结. ...
- [原] KVM 虚拟化原理探究(1)— overview
KVM 虚拟化原理探究- overview 标签(空格分隔): KVM 写在前面的话 本文不介绍kvm和qemu的基本安装操作,希望读者具有一定的KVM实践经验.同时希望借此系列博客,能够对KVM底层 ...
- H5单页面手势滑屏切换原理
H5单页面手势滑屏切换是采用HTML5 触摸事件(Touch) 和 CSS3动画(Transform,Transition)来实现的,效果图如下所示,本文简单说一下其实现原理和主要思路. 1.实现原理 ...
- .NET Core中间件的注册和管道的构建(1)---- 注册和构建原理
.NET Core中间件的注册和管道的构建(1)---- 注册和构建原理 0x00 问题的产生 管道是.NET Core中非常关键的一个概念,很多重要的组件都以中间件的形式存在,包括权限管理.会话管理 ...
- python自动化测试(2)-自动化基本技术原理
python自动化测试(2) 自动化基本技术原理 1 概述 在之前的文章里面提到过:做自动化的首要本领就是要会 透过现象看本质 ,落实到实际的IT工作中就是 透过界面看数据. 掌握上面的这样的本领 ...
- CRC、反码求和校验 原理分析
3月份开始从客户端转后台,算是幸运的进入全栈工程师的修炼阶段.这段时间一边是老项目的客户端加服务器两边的维护和交接,一边是新项目加加加班赶工,期间最长经历了连续工作三天只睡了四五个小时的煎熬,人生也算 ...
- 菜鸟学Struts2——Struts工作原理
在完成Struts2的HelloWorld后,对Struts2的工作原理进行学习.Struts2框架可以按照模块来划分为Servlet Filters,Struts核心模块,拦截器和用户实现部分,其中 ...
随机推荐
- ASCII码表(0-255)
- Caffe模型读取
caffe模型最终保存使用过的protobuf形式,将一个已经训练好的caffe模型读取出来,可以参考如下: 1,包含的头文件: #include <google/protobuf/io/cod ...
- ios - 上下滚动的新闻
#import <UIKit/UIKit.h> @interface ScrollUpDownView : UIView //设置 要现实的文字 @property(nonatomic, ...
- scrapy 简介
Scrapy是用纯Python实现一个为了爬取网站数据.提取结构性数据而编写的应用框架. Scrapy架构图(绿线是数据流向): Scrapy Engine(引擎): 负责Spider.ItemPip ...
- 关于angularjs的ng-repeat指令
(如果有说的不对,欢迎指教,更欢迎大家一起交流.) 关于angularjs的ng-repeat指令,想必每个学习angularjs的初学者都很有映像.那我也总结一下我使用ng-repeat的时候经验, ...
- android菜鸟学习笔记28----Android中的Service生命周期及本地和远程服务绑定的实现
Service是Android中长期在后台运行的没有界面的组件,使用服务的优势在于:能够提高进程的优先级,系统不容易回收掉进程,即便回收了,内存充足的时候,会把进程重新创建. 1.服务的简单使用示例: ...
- PLSQL 用的让自己更舒心点!
每天开发时候 ,有时候会疲劳! 改下你的PLSQL 字体吧!
- ArcGIS for window mobile 数据打开
前言 环境信息:ArcGIS for windows mobile 10.1.1,ArcGIS runtime sdk for windows mobile 10.1.1 一.MapCache的打开 ...
- 关于android编程中service和activity的区别
一. 绝大部分情况下,Service的作用是用来“执行”后台的.耗时的.重要的任务,三者缺一不可,而最重要的原因是第三点:要执行重要的任务. 因为当一个进程启动了Service后,进程的优先级变高了, ...
- YAMLException: can not read a block mapping entry; a multiline key may not be an implicit key at line 5, column 1:
创建的md文件头部声明中没有加空格.