threadlocal正确使用方法,thread local通俗易懂

首页 > 技术 > 作者:YD1662023-04-15 23:31:07

ThreadLocal的作用主要是做数据隔离,填充的数据只属于当前线程,变量的数据对别的线程而言是相对隔离的,在多线程环境下,如何防止自己的变量被其它线程篡改。

你能跟我说说它隔离有什么用,会用在什么场景么?

这,我都说了我很少用了,还问我,难受了呀,哦哦哦,有了想起来了,事务隔离级别。

面试官你好,其实我第一时间想到的就是Spring实现事务隔离级别的源码,这还是当时我大学被女朋友甩了,一个人在图书馆哭泣的时候无意间发现的。

threadlocal正确使用方法,thread local通俗易懂(5)

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的机会终于来了。

threadlocal正确使用方法,thread local通俗易懂(6)

有的有的面试官,这个我会!!!

之前我们上线后发现部分用户的日期居然不对了,排查下来是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; } } …… }

结构大概长这样:

threadlocal正确使用方法,thread local通俗易懂(7)

稍等,我有两个疑问你可以解答一下么?

好呀,面试官你说。

为什么需要数组呢?没有了链表怎么解决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,那就找下一个空位置,直到为空为止。

threadlocal正确使用方法,thread local通俗易懂(8)

上一页1234下一页

栏目热文

文档排行

本站推荐

Copyright © 2018 - 2021 www.yd166.com., All Rights Reserved.