ThreadLocal的作用主要是做数据隔离,填充的数据只属于当前线程,变量的数据对别的线程而言是相对隔离的,在多线程环境下,如何防止自己的变量被其它线程篡改。
你能跟我说说它隔离有什么用,会用在什么场景么?这,我都说了我很少用了,还问我,难受了呀,哦哦哦,有了想起来了,事务隔离级别。
面试官你好,其实我第一时间想到的就是Spring实现事务隔离级别的源码,这还是当时我大学被女朋友甩了,一个人在图书馆哭泣的时候无意间发现的。
Spring采用Threadlocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,同时,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复。
Spring框架里面就是用的ThreadLocal来实现这种隔离,主要是在TransactionSynchronizationManager这个类里面,代码如下所示:
privatestaticfinalLoglogger=LogFactory.getLog(TransactionSynchronizationManager.class);
privatestaticfinalThreadLocal<Map<Object,Object>>resources=
newNamedThreadLocal<>("Transactionalresources");
privatestaticfinalThreadLocal<Set<TransactionSynchronization>>synchronizations=
newNamedThreadLocal<>("Transactionsynchronizations");
privatestaticfinalThreadLocal<String>currentTransactionName=
newNamedThreadLocal<>("Currenttransactionname");
……
Spring的事务主要是ThreadLocal和AOP去做实现的,我这里提一下,大家知道每个线程自己的链接是靠ThreadLocal保存的就好了,继续的细节我会在Spring章节细说的,暖么?
除了源码里面使用到ThreadLocal的场景,你自己有使用他的场景么?一般你会怎么用呢?来了来了,加分项来了,这个我还真遇到过,装B的机会终于来了。
有的有的面试官,这个我会!!!
之前我们上线后发现部分用户的日期居然不对了,排查下来是SimpleDataFormat的锅,当时我们使用SimpleDataFormat的parse()方法,内部有一个Calendar对象,调用SimpleDataFormat的parse()方法会先调用Calendar.clear(),然后调用Calendar.add(),如果一个线程先调用了add()然后另一个线程又调用了clear(),这时候parse()方法解析的时间就不对了。
其实要解决这个问题很简单,让每个线程都new 一个自己的 SimpleDataFormat就好了,但是1000个线程难道new1000个SimpleDataFormat?
所以当时我们使用了线程池加上ThreadLocal包装SimpleDataFormat,再调用initialValue让每个线程有一个SimpleDataFormat的副本,从而解决了线程安全的问题,也提高了性能。
那……还有还有,我还有,您别着急问下一个,让我再加点分,拖延一下面试时间。
我在项目中存在一个线程经常遇到横跨若干方法调用,需要传递的对象,也就是上下文(Context),它是一种状态,经常就是是用户身份、任务信息等,就会存在过渡传参的问题。
使用到类似责任链模式,给每个方法增加一个context参数非常麻烦,而且有些时候,如果调用链有无法修改源码的第三方库,对象参数就传不进去了,所以我使用到了ThreadLocal去做了一下改造,这样只需要在调用前在ThreadLocal中设置参数,其他地方get一下就好了。
before
voidwork(Useruser){
getInfo(user);
checkInfo(user);
setSomeThing(user);
log(user);
}
then
voidwork(Useruser){
try{
threadLocalUser.set(user);
//他们内部Useru=threadLocalUser.get();就好了
getInfo();
checkInfo();
setSomeThing();
log();
}finally{
threadLocalUser.remove();
}
}
我看了一下很多场景的cookie,session等数据隔离都是通过ThreadLocal去做实现的。
对了我面试官允许我再秀一下知识广度,在Android中,Looper类就是利用了ThreadLocal的特性,保证每个线程只存在一个Looper对象。
staticfinalThreadLocal<Looper>sThreadLocal=newThreadLocal<Looper>();
privatestaticvoidprepare(booleanquitAllowed){
if(sThreadLocal.get()!=null){
thrownewRuntimeException("OnlyoneLoopermaybecreatedperthread");
}
sThreadLocal.set(newLooper(quitAllowed));
}
面试官:我丢,这货怎么知道这么多场景?还把Android都扯了出来,不是吧阿sir,下面我要考考他原理了。嗯嗯,你回答得很好,那你能跟我说说他底层实现的原理么?
好的面试官,我先说一下他的使用:
ThreadLocal<String>localName=newThreadLocal();
localName.set("张三");
Stringname=localName.get();
localName.remove();
其实使用真的很简单,线程进来之后初始化一个可以泛型的ThreadLocal对象,之后这个线程只要在remove之前去get,都能拿到之前set的值,注意这里我说的是remove之前。
他是能做到线程间数据隔离的,所以别的线程使用get()方法是没办法拿到其他线程的值的,但是有办法可以做到,我后面会说。
我们先看看他set的源码:
publicvoidset(Tvalue){
Threadt=Thread.currentThread();//获取当前线程
ThreadLocalMapmap=getMap(t);//获取ThreadLocalMap对象
if(map!=null)//校验对象是否为空
map.set(this,value);//不为空set
else
createMap(t,value);//为空创建一个map对象
}
大家可以发现set的源码很简单,主要就是ThreadLocalMap我们需要关注一下,而ThreadLocalMap呢是当前线程Thread一个叫threadLocals的变量中获取的。
ThreadLocalMapgetMap(Threadt){
returnt.threadLocals;
}
publicclassThreadimplementsRunnable{
……
/*ThreadLocalvaluespertainingtothisthread.Thismapismaintained
*bytheThreadLocalclass.*/
ThreadLocal.ThreadLocalMapthreadLocals=null;
/*
*InheritableThreadLocalvaluespertainingtothisthread.Thismapis
*maintainedbytheInheritableThreadLocalclass.
*/
ThreadLocal.ThreadLocalMapinheritableThreadLocals=null;
……
这里我们基本上可以找到ThreadLocal数据隔离的真相了,每个线程Thread都维护了自己的threadLocals变量,所以在每个线程创建ThreadLocal的时候,实际上数据是存在自己线程Thread的threadLocals变量里面的,别人没办法拿到,从而实现了隔离。
ThreadLocalMap底层结构是怎么样子的呢?面试官这个问题问得好啊,内心暗骂,让我歇一会不行么?
张三笑着回答道,既然有个Map那他的数据结构其实是很像HashMap的,但是看源码可以发现,它并未实现Map接口,而且他的Entry是继承WeakReference(弱引用)的,也没有看到HashMap中的next,所以不存在链表了。
staticclassThreadLocalMap{
staticclassEntryextendsWeakReference<ThreadLocal<?>>{
/**ThevalueassociatedwiththisThreadLocal.*/
Objectvalue;
Entry(ThreadLocal<?>k,Objectv){
super(k);
value=v;
}
}
……
}
结构大概长这样:
好呀,面试官你说。
为什么需要数组呢?没有了链表怎么解决Hash冲突呢?用数组是因为,我们开发过程中可以一个线程可以有多个TreadLocal来存放不同类型的对象的,但是他们都将放到你当前线程的ThreadLocalMap里,所以肯定要数组来存。
至于Hash冲突,我们先看一下源码:
privatevoidset(ThreadLocal<?>key,Objectvalue){
Entry[]tab=table;
intlen=tab.length;
inti=key.threadLocalHashCode&(len-1);
for(Entrye=tab[i];
e!=null;
e=tab[i=nextIndex(i,len)]){
ThreadLocal<?>k=e.get();
if(k==key){
e.value=value;
return;
}
if(k==null){
replaceStaleEntry(key,value,i);
return;
}
}
tab[i]=newEntry(key,value);
intsz= size;
if(!cleanSomeSlots(i,sz)&&sz>=threshold)
rehash();
}
我从源码里面看到ThreadLocalMap在存储的时候会给每一个ThreadLocal对象一个threadLocalHashCode,在插入过程中,根据ThreadLocal对象的hash值,定位到table中的位置i,int i = key.threadLocalHashCode & (len-1)。
然后会判断一下:如果当前位置是空的,就初始化一个Entry对象放在位置i上;
if(k==null){
replaceStaleEntry(key,value,i);
return;
}
如果位置i不为空,如果这个Entry对象的key正好是即将设置的key,那么就刷新Entry中的value;
if(k==key){
e.value=value;
return;
}
如果位置i的不为空,而且key不等于entry,那就找下一个空位置,直到为空为止。