作者:@adamhand
zybuluo.com/adamhand/note/1370920
ThreadLocal是什么
首先说明,ThreadLocal与线程同步无关。ThreadLocal虽然提供了一种解决多线程环境下成员变量的问题,但是它并不是解决多线程共享变量的问题。
ThreadLocal类提供了一种线程局部变量(ThreadLocal),即每一个线程都会保存一份变量副本,每个线程都可以独立地修改自己的变量副本,而不会影响到其他线程,是一种线程隔离的思想。
实现原理
ThreadLocal提供四个方法:
publicTget(){}
publicvoidset(Tvalue){}
publicvoidremove(){}
protectedTinitialValue(){}
get()方法是用来获取ThreadLocal在当前线程中保存的变量副本,set()用来设置当前线程中变量的副本,remove()用来移除当前线程中变量的副本,initialValue()是一个protected方法,一般是用来在使用时进行重写的,它是一个延迟加载方法。这四种方法都是基于ThreadLocalMap的。
ThreadLocalMap
ThreadLocal内部有一个静态内部类ThreadLocalMap,该内部类是实现线程隔离机制的关键。ThreadLocalMap提供了一种用键值对方式存储每一个线程的变量副本的方法,key为当前ThreadLocal对象,value则是对应线程的变量副本。该Map默认的大小是16,即能存储16个键值对,超过后会扩容。
具体源码如下:
Entry类
ThreadLocalMap其内部利用Entry来实现key-value的存储,如下:
staticclassEntryextendsWeakReference<ThreadLocal<?>>{
/**ThevalueassociatedwiththisThreadLocal.*/
Objectvalue;
Entry(ThreadLocal<?>k,Objectv){
super(k);
value=v;
}
}
从上面代码中可以看出Entry的key就是ThreadLocal,而value就是值。同时,Entry也继承WeakReference,所以说Entry所对应key(ThreadLocal实例)的引用为一个弱引用。
set方法
ThreadLocalMap的set方法和Map的put方法差不多,但是有一点区别是:put方法处理哈希冲突使用的是链地址法,而set方法使用的开放地址法。
set()操作除了存储元素外,还有一个很重要的作用,就是replaceStaleEntry()和cleanSomeSlots(),这两个方法可以清除掉key == null 的实例,防止内存泄漏。在set()方法中还有一个变量很重要:threadLocalHashCode,定义如下:
privatefinalintthreadLocalHashCode=nextHashCode();
threadLocalHashCode是ThreadLocal的散列值,定义为final,表示ThreadLocal一旦创建其散列值就已经确定了,生成过程则是调用nextHashCode():
privatestaticAtomicIntegernextHashCode=newAtomicInteger();
privatestaticfinalintHASH_INCREMENT=0x61c88647;
privatestaticintnextHashCode(){
returnnextHashCode.getAndAdd(HASH_INCREMENT);
}
nextHashCode表示分配下一个ThreadLocal实例的threadLocalHashCode的值,HASH_INCREMENT则表示分配两个ThradLocal实例的threadLocalHashCode的增量。
getEntry()
privateEntrygetEntry(ThreadLocal<?>key){
inti=key.threadLocalHashCode&(table.length-1);
Entrye=table[i];
if(e!=null&&e.get()==key)
returne;
else
returngetEntryAfterMiss(key,i,e);
}
由于采用了开放定址法,所以当前key的散列值和元素在数组的索引并不是完全对应的,首先取一个探测数(key的散列值),如果所对应的key就是我们所要找的元素,则返回,否则调用getEntryAfterMiss(),如下:
privateEntrygetEntryAfterMiss(ThreadLocal<?>key,inti,Entrye){
Entry[]tab=table;
intlen=tab.length;
while(e!=null){
ThreadLocal<?>k=e.get();
if(k==key)
returne;
if(k==null)
expungeStaleEntry(i);
else
i=nextIndex(i,len);
e=tab[i];
}
returnnull;
}
这里有一个重要的地方,当key == null时,调用了expungeStaleEntry()方法,该方法用于处理key == null,有利于GC回收,能够有效地避免内存泄漏。
get()方法
publicTget(){
//获取当前线程
Threadt=Thread.currentThread();
//获取当前线程的成员变量threadLocal
ThreadLocalMapmap=getMap(t);
if(map!=null){
//从当前线程的ThreadLocalMap获取相对应的Entry
ThreadLocalMap.Entrye=map.getEntry(this);
if(e!=null){
@SuppressWarnings("unchecked")
//获取目标值
Tresult=(T)e.value;
returnresult;
}
}
returnsetInitialValue();
}
首先通过当前线程获取所对应的成员变量ThreadLocalMap,然后通过ThreadLocalMap获取当前ThreadLocal的Entry,最后通过所获取的Entry获取目标值result。
getMap()方法可以获取当前线程所对应的ThreadLocalMap,如下:
ThreadLocalMapgetMap(Threadt){
returnt.threadLocals;
}
set(T value)
publicvoidset(Tvalue){
Threadt=Thread.currentThread();
ThreadLocalMapmap=getMap(t);
if(map!=null)
map.set(this,value);
else
createMap(t,value);
}
获取当前线程所对应的ThreadLocalMap,如果不为空,则调用ThreadLocalMap的set()方法,key就是当前ThreadLocal,如果不存在,则调用createMap()方法新建一个,如下:
voidcreateMap(Threadt,TfirstValue){
t.threadLocals=newThreadLocalMap(this,firstValue);
}
initialValue()
protectedTinitialValue(){
returnnull;
}
该方法定义为protected级别且返回为null,很明显是要子类实现它的,所以我们在使用ThreadLocal的时候一般都应该覆盖该方法。
注意:如果想在get之前不需要调用set就能正常访问的话,必须重写initialValue()方法。
因为在上面的代码分析过程中,我们发现如果没有先set的话,即在map中查找不到对应的存储,则会通过调用setInitialValue方法返回i,而在setInitialValue方法中,有一个语句是T value = initialValue(), 而默认情况下,initialValue方法返回的是null。
remove()
publicvoidremove(){
ThreadLocalMapm=getMap(Thread.currentThread());
if(m!=null)
m.remove(this);
}
该方法的目的是减少内存的占用。当然,我们不需要显示调用该方法,因为一个线程结束后,它所对应的局部变量就会被垃圾回收。
ThreadLocal使用示例
结果如下:
Thread-1seqCount:1
Thread-3seqCount:1
Thread-2seqCount:1
Thread-0seqCount:1
Thread-2seqCount:2
Thread-3seqCount:2
Thread-1seqCount:2
Thread-3seqCount:3
Thread-2seqCount:3
Thread-0seqCount:2
Thread-1seqCount:3
Thread-0seqCount:3
ThreadLocal与内存泄漏
为什么会出现内存泄漏
首先看一下运行时ThreadLocal变量的内存图:
运行时,会在栈中产生两个引用,指向堆中相应的对象。
可以看到,ThreadLocalMap使用ThreadLocal的弱引用作为key,这样一来,当ThreadLocal ref和ThreadLocal之间的强引用断开 时候,即ThreadLocal ref被置为null,下一次GC时,threadLocal对象势必会被回收。
这样,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,比如使用线程池,线程使用完成之后会被放回线程池中,不会被销毁,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。
其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。
但是这些被动的预防措施并不能保证不会内存泄漏:
- 使用static的ThreadLocal,延长了ThreadLocal的生命周期,可能导致的内存泄漏。
- 分配使用了ThreadLocal又不再调用get(),set(),remove()方法,那么就会导致内存泄漏。
为什么要使用弱引用?
使用弱引用,是为了更好地对ThreadLocal对象进行回收。如果使用强引用,当ThreadLocal ref = null的时候,意味着ThreadLocal对象已经没用了,ThreadLocal对象应该被回收,但由于Entry中还存着这对ThreadLocal对象的强引用,导致ThreadLocal对象不能回收,可能会发生内存泄漏。
为什么不将value也设置成弱引用?
为什么呢?
如何避免内存泄漏?
每次使用完ThreadLocal,都调用它的remove()方法,清除数据。
ThreadLocal与脏读
前面说了,ThreadLocal中的set()、get()和remove()方法都会对key==null的value进行处理,其中set()和get()方法是将key==null的value置为null。但是如果ThreadLocal是static类型的,并且配合线程池使用,线程池会重用Thread对象,同时会重用与Thread绑定的ThreadLocal变量。倘若下一个线程不调用set()方法重新设置初始值,也不调用remove()方法处理旧值,直接调用get()方法获取,就会出现脏读问题。
例子如下。
publicclassDirtyDataInThreadLocal{
publicstaticThreadLocal<String>threadLocal=newThreadLocal<>();
publicstaticvoidmain(String[]args){
//使用固定大小为1的线程池,说明上一个线程属性会被下一个线程属性复用
ExecutorServicepool=Executors.newFixedThreadPool(1);
for(inti=0;i<2;i ){
MyThreadthread=newMyThread();
pool.execute(thread);
}
}
privatestaticclassMyThreadextendsThread{
privatestaticbooleanflag=true;
@Override
publicvoidrun(){
if(flag){
//第一个线程set后,没有remove,第二个线程也没有进行set操作
threadLocal.set(this.getName() ",Sessioninfo.");
flag=false;
}
System.out.println(this.getName() "线程是" threadLocal.get());
}
}
}
打印结果如下:
Thread-0线程是Thread-0,sessioninfo.
Thread-1线程是Thread-0,sessioninfo.
ThreadLocal使用场景
数据连接和Session管理
最常见的ThreadLocal使用场景为 用来解决 数据库连接、Session管理等。
如:
privatestaticThreadLocal<Connection>connectionHolder=newThreadLocal<Connection>(){
publicConnectioninitialValue(){
returnDriverManager.getConnection(DB_URL);
}
};
publicstaticConnectiongetConnection(){
returnconnectionHolder.get();
}
privatestaticfinalThreadLocalthreadSession=newThreadLocal();
publicstaticSessiongetSession()throwsInfrastructureException{
Sessions=(Session)threadSession.get();
try{
if(s==null){
s=getSessionFactory().openSession();
threadSession.set(s);
}
}catch(HibernateExceptionex){
thrownewInfrastructureException(ex);
}
returns;
}