本文转载自微信公众号《Angela的博客》,作者Angela的博客。转载本文请联系Angela博客公众号。开场,杭州某商务楼内,一场应聘者与面试官的较量正在上演。面试官:请先自我介绍一下。Angela:面试官你好,我是草地三贱人,最强中单(妲己不服),草地摩托司机,21套广播体操发起人,火系传人Angela,这就是我的简历,请看看。面试官:根据你的简历,你熟悉多线程编程。你对它熟悉到什么程度?安吉拉:精通。正确的。..,你没看错,问的就是“精通”,在评论区打666。采访者:[思考]难道他是个天真的批评家,一上来就说自己精通,说精通的人就是傻子!面试官:那我们开始吧。你用过Threadlocal吗?安吉拉:是的。面试官:那说说你项目中ThreadLocal的使用情况吧。Angela:我们的项目是保密项目,所以我不予置评。你应该改变问题!面试官:说一个非机密的项目,或者说说Threadlocal的实现原理。主题安吉拉:表演时间。..Angela:比如支付宝每秒同时有很多用户请求,每个请求都带有用户信息。我们知道通常一个线程处理一个用户请求,我们可以把用户信息丢给Threadlocal里面,让每个线程处理自己的用户信息,线程之间互不干扰。面试官:等等,我问你一个私人问题,你为什么从支付宝出来面试,受不了PUA?Angela:PUA我,没有人,能PUA我的人还没出生呢!公司食堂吃腻了,想换换口味。面试官:那你说说Threadlocal是做什么的?Angela:Threadlocal主要是用来隔离线程变量的,所以可能不是很直观。还是上面提到的例子,我们的程序在处理用户请求的时候,通常后端服务器有一个线程池,一个请求交给一个线程去处理,所以为了防止多个线程并发处理请求。数据,比如线程AB分别处理安吉拉和妲己的请求,线程A本来处理安吉拉的请求,却访问了妲己的数据,将妲己的支付宝钱转走了。所以可以把Angela的数据绑定到线程A上,等线程处理完再解除绑定。面试官:那把你刚才说的那个场景用伪代码实现一下,我写给你!安吉拉:好的//ThreadLocalprivatestaticfinalThreadLocaluserInfoThreadLocal=newThreadLocal<>();publicResponsehandleRequest(UserInfouserInfo){Responseresponse=newResponse();try{//1.设置用户信息到线程局部变量userInfoThreadLocal.set(userInfo);doHandle();}finally{//3。RemoveuserInfoThreadLocal.remove();}returnresponse;}//业务逻辑处理privatevoiddoHandle(){//2.取出UserInfouserInfo=userInfoThreadLocal.get();//查询用户资产queryUserAsset(userInfo);}1.2.3步骤很清楚。面试官:那你说说Threadlocal是怎么实现线程变量隔离的?Angela:哦,这么快就进入正题了,我先给你画个图,如下:面试官:我看到图了,那你是对的,根据你的代码说一下对应图中的流程之前写过。安吉拉:没问题。首先我们通过ThreadLocaluserInfoThreadLocal=newThreadLocal()初始化一个Threadlocal对象,也就是上图中提到的Threadlocal引用。这个引用指向堆中的ThreadLocal对象;然后我们调用userInfoThreadLocal.set(userInfo);这里发生了什么?我们把源码拿出来,一看就清楚了。我们知道Thread类有一个ThreadLocalMap成员变量,Map的key是Threadlocal对象,value是你要存储的线程局部变量。#ThreadlocalclassThreadlocal.classpublicvoidset(Tvalue){//获取当前线程Thread,也就是上图中的Thread引用Threadt=Thread.currentThread();//Thread类有一个成员变量ThreadlocalMap,获取这个MapThreadLocalMapmap=getMap(t);if(map!=null)//this引用Threadlocal对象map.set(this,value);elsecreateMap(t,value);}ThreadLocalMapgetMap(Threadt){//获取线程的ThreadLocalMappreturnt.threadLocals;}voidcreateMap(Threadt,TfirstValue){//初始化t.threadLocals=newThreadLocalMap(this,firstValue);}#ThreadclassThread.classpublicclassThreadimplementsRunnable{//每个线程都有自己的ThreadLocalMap成员变量ThreadLocal.ThreadLocalMapthreadLocals=null;}这里是current在线程对象的ThreadlocalMap中放入了一个元素(Entry),key是Threadlocal对象,value是userInfo。明白两点就很清楚了:ThreadLocalMap类是在Threadlocal中定义的。首先,Thread对象是Java语言中线程运行的载体。每个线程都有一个对应的Thread对象,里面存放着一些与线程相关的信息。其次,Thread类中有一个成员变量ThreadlocalMap。你可以把它当成一个普通的Map,key存放的是Threadlocal对象,value是你要绑定到线程的值(线程隔离变量),比如这里是用户信息对象(UserInfo)。面试官:你刚才说Thread类有一个ThreadlocalMap属性的成员变量,但是ThreadlocalMap的定义是在Threadlocal中。你为什么这么做?Angela:看一下ThreadlocalMap类ThreadLocalMap*ThreadLocalMapiscustomizedhashmapsuitableonlyfor*maintainingthreadlocalvalues的描述。*允许声明类线程中的字段。为了帮助处理*非常大和长期使用,哈希表条目对键使用*弱引用。但是,由于不*使用引用队列,只有当表开始运行空间不足时,才能保证删除陈旧的条目。这可能意味着ThreadLocalMap旨在维护线程局部变量。这也是为什么ThreadLocalMap是Thread的成员变量,但它是Threadlocal的内部类(非public,只有包访问权限,Thread和Threadlocal都在java.lang包下),就是让用户知道ThreadLocalMap只保存线程局部变量这个东西。面试官:既然是线程局部变量,为什么不用线程对象(Threadobject)作为key呢?以线程为key获取线程变量不是更清楚吗?Angela:这样设计会有问题,比如:我把用户信息保存在thread变量中。这时候需要添加一个新的线程变量。例如,添加用户的地理位置信息。我们ThreadlocalMap的key使用了一个thread,另外存储了一个地理位置信息。key是同一线程(key),原来的用户信息会被覆盖。Map.put(key,value)操作你很熟悉,所以网上有些文章说ThreadlocalMap使用线程作为key,这是无稽之谈。面试官:添加地理位置信息应该怎么做?Angela:新建一个Threadlocal对象就可以了,因为ThreadLocalMap的key就是一个Threadlocal对象。比如添加一个地理位置,我会添加Threadlocalgeo=newThreadlocal()存储地理位置信息,这样线程的ThreadlocalMap中就会有两个元素,一个是用户信息,一个是地理位置。面试官:ThreadlocalMap实现的是什么数据结构?Angela:和HashMap一样,也是用数组来实现的。代码如下:classThreadLocalMap{//初始容量privatestaticfinalintINITIAL_CAPACITY=16;//存放元素的数组privateEntry[]table;//元素个数privateintsize=0;}table是存放线程局部变量的数组,数组元素是Entry类,Entry由key和value组成,key是一个Threadlocal对象,value是我们前面例子中存储的对应的线程变量。数组存储结构如下图所示:面试官:ThreadlocalMap出现hash冲突怎么办?它与HashMap有什么不同?Angela:【思考】第一次遇到有人问ThreadlocalMaphash冲突的问题。这次采访越来越有趣了。曰:有分别。HashMap为了处理hash冲突,采用了链表+红黑树的形式,如下图所示。如果链表长度过长(>8),会转为红黑树:ThreadlocalMap既没有链表也没有红黑树。该树使用链地址法。链地址法是指如果有冲突,ThreadlocalMap会直接去寻找下一个相邻的节点。如果相邻节点为空,则直接存储。如果不为空,则继续查找。直到找到一个空的,将元素放入,或者元素个数超过数组长度的阈值,扩容。如下图:还是用前面的例子来说明,ThreadlocalMap数组长度为4,现在在保存地理位置的时候出现hash冲突(位置1已经有数据),再回头看发现位置2为空,直接存入2这个位置。源码(如果难以阅读,可以稍后阅读):privatevoidset(ThreadLocal>key,Objectvalue){Entry[]tab=table;intlen=tab.length;//hashcode&运算其实就是%取数组长度的余数,例如:如果数组长度为4,hashCode%(4-1)会找到数组的下标存放元素inti=key.threadLocalHashCode&(len-1);//找数组的空槽(=null),一般ThreadlocalMap不会存储很多元素for(Entrye=tab[i];e!=null;//找数组的空槽(=null)e=tab[i=nextIndex(i,len)]){ThreadLocal>k=e.get();//如果key值相同则视为更新操作,直接替换if(k==key){e.value=value;return;}//key为空,做替换清理动作,后面讲WeakReference的时候,if(k==null){replaceStaleEntry(key,value,i);return;}}//newnewEntrytab[i]=newEntry(key,value);//数组元素Number+1intsz=++size;//如果元素没有被清理或者数存储元素的个数超过数组的阈值,扩容if(!cleanSomeSlots(i,sz)&&sz>=threshold)rehash();}//顺序遍历+1到达数组末尾,返回到头部数组的(0位置)privatestaticintnextIndex(inti,intlen){return((i+1key){//计算hash值&操作其实就是%数组长度的余数,例如:数组长度为4,hashCode%(4-1)会找到要查询的数组地址inti=key.threadLocalHashCode&(table.length-1);Entrye=table[我]。然后依次查找(链地址法,这个后面会介绍)}面试官:我看到你第一张图画的ThreadlocalMap里面的key是WeakReference类型的。你能告诉我几个Java中类似的引用,有什么区别吗??Angela:强引用是最常用的引用。如果一个对象有强引用,垃圾收集器将永远不会回收它。当内存空间不足时,Java虚拟机宁愿抛出OutOfMemoryError错误,导致程序异常终止,也不会通过随意回收强引用对象来解决内存不足的问题。如果一个对象只有软引用,当内存空间足够时,垃圾回收器不会回收它;如果内存空间不足,就会回收这些对象的内存。弱引用和软引用的区别在于只有弱引用的对象生命周期更短。垃圾回收线程在扫描内存区域时,一旦发现只有弱引用的对象,无论当前内存空间是否足够,都会回收其内存。然而,由于垃圾收集器是一个非常低优先级的线程,只有弱引用的对象可能无法快速找到。顾名思义,虚假引用是无用的。与其他类型的引用不同,虚引用不决定对象的生命周期。如果一个对象只持有虚引用,就好像它没有引用一样,随时可能被垃圾收集器回收。妥妥的八字作文!尴尬(─.─|||。面试官:那你能告诉我为什么ThreadlocalMap中的key要设计成WeakReference(弱引用)类型吗?Angela:是的,为了尽量避免内存泄漏。面试官:能详细解释一下吗?你为什么要做到最好?前面你也说过,WeakReference引用的对象会直接被GC(内存收集器)回收。为什么不直接避免内存泄漏呢?Angela:我们还是看下图:privatestaticfinalThreadLocaluserInfoThreadLocal=newThreadLocal<>();userInfoThreadLocal.set(userInfo);这里的引用关系是userInfoThreadLocal引用了ThreadLocal对象,是强引用,ThreadLocal对象也被ThreadlocalMap的key引用,这是WeakReference引用。前面我们说过,GC回收ThreadLocal对象的前提是它只被WeakReference引用,没有任何强引用。为了让大家更容易理解弱引用,我写了一个demo程序//angela和弱引用指向同一个对象System.out.println(angela);//java.lang.Object@4550017cSystem.out.println(weakReference.get());//java.lang.Object@4550017c//设置angela的强引用如果为null,则此对象只剩下弱引用。如果内存足够,弱引用也会被回收(angela);//nullSystem.out.println(weakReference.get());//null}可以看到,一旦一个对象只被弱引用引用,对象将在GC期间被回收。所以只要ThreadLocal对象还被userInfoThreadLocal引用(强引用),G??C就不会回收WeakReference引用的对象。面试官:既然ThreadLocal对象是强引用,不能回收,为什么还要设计成WeakReference类型呢?Angela:ThreadLocal的设计者考虑到线程往往有很长的生命周期,比如经常使用线程池,线程是一直存活的。根据JVM的寻根算法,总有一个Thread->ThreadLocalMap->Entry(element)这样的引用链接,如下图。如果key没有设计成WeakReference类型,并且是强引用,则永远不会被使用。GC回收,key永远不会为null,不为null则不会清理Entry元素(ThreadLocalMap根据key是否为null来判断是否清理Entry),所以ThreadLocal的设计者认为只要ThreadLocal所在的作用域结束工作被清理后,GC回收时就会回收key引用对象,key会被置为null。ThreadLocal会尽量保证Entry被清理,尽可能避免内存泄漏。我们看代码://ElementclassstaticclassEntryextendsWeakReference>{/**ThevalueassociatedwiththisThreadLocal.*/Objectvalue;//key继承自父类,所以只有valueEntry(ThreadLocal>k,Objectv){super(k);value=v;}}//WeakReference继承Reference,key为referentpublicabstractclassReference{//this为继承keyprivateTreferent;Reference(Treferent){this(referent,null);}}Entry继承了WeakReference类。Entry中的键是WeakReference类型。在Java中,当一个对象只被WeakReference引用而没有引用其他对象时,WeakReference引用的对象在GC发生时会直接被回收。面试官:那如果Threadlocal对象总是有强引用怎么办?是不是有内存泄漏的风险。Angela:最佳做法是在完成后手动调用删除函数。我们看源码:classThreadlocal{publicvoidremove(){//这是ThreadLocalMapThreadLocalMapm=getMap(Thread.currentThread());if(m!=null)m.remove(this);//这是ThreadLocalobject,remove,方法如下(entrye=tab[i];e!=null;e=tab[i=nextIndex(i,len)]){//清除if(e.get()==key){e.clear();expungeStaleEntry(i);//清理空槽return;}}}}//这个方法是做元素清理privateintexpungeStaleEntry(intstaleSlot){Entry[]tab=table;intlen=tab.length;//设置值staleSlot为空,然后设置数组元素为空staleSlot,len);(e=tab[i])!=null;i=nextIndex(i,len)){ThreadLocal>k=e.get();//k为null表示引用对象为被GC回收if(k==null){e.value=null;tab[i]=null;size--;}else{//因为减少了元素个数,re-hashinth=k.threadLocalHashCode&(len-1);//hash地址不相等,说明这个元素之前有hash冲突(本来应该放这里却没有放在这里),//现在因为去掉了一些元素,很有可能是原来的冲突位置为空,重试if(h!=i){tab[i]=null;//继续使用链地址法存储元素while(tab[h]!=null)h=nextIndex(h,len);tab[h]=e;}}}returni;}面试官:你有使用Threadlocal的实际项目经验吗?安吉拉:是的!我之前和你的一位面试官谈过。负责支付宝后台的系统40多个核心rpc接口如何大幅提升性能,下面是其中一个接口切换流后的效果,其中使用了Threadlocal。图片采访者:嗯,说吧。Angela:刚才说了有40多个接口需要升级优化。风险非常高。我要保证接口切换后业务不受影响,也叫等价切换。流程如下:根据接口常量名的业务含义定义40多个接口,如接口名alipay.quickquick.follow.angela;根据接口的流量从低到高进行流量切换,配置中心预先配置好每一个。各个接口的截止比例和用户白名单;流量切分也有讲究,先按userId白名单切分,再按userId尾号切分百分比,如果完全没问题,则完成切分;在顶层抽象模板方法的入口处,传入ThreadLocalSetinterfacename,将接口名放入其中;然后在切流的地方通过ThreadLocal获取接口名,用来判断接口的流向;面试官:最后一个问题,如果我有很多变量要塞进ThreadlocalMap,那我怎么可能不需要声明很多Threadlocal对象呢?有没有好的解决办法。Angela:我们最好的做法是重新封装,把ThreadLocalMap的值做成一个Map,这样只有一个Threadlocal对象就够了。面试官:能详细说说吗?安吉拉:我不能再这样了,我太累了。采访者:告诉我。安吉拉:我真的不想谈论它。面试官:今天先来,出了这个门右转,回去等通知!