开场白
张三最近天气很热心情不是很好,所以他决定出去面试跟面试官聊聊天排解一下,结果刚投递简历就有人约了面试。
我丢,什么情况怎么刚投递出去就有人约我面试了?诶。。。真烦啊,哥已经不在江湖这么久了,江湖还是有哥的传说,我还是这么抢手的么?太烦恼了,帅无罪。
暗自窃喜的张三来到了某东现场面试的办公室,我丢,这面试官?不是吧,这满是划痕的Mac,这发量,难道就是传说中的架构师?
张三的心态一下子就崩了,出来第一场面试就遇到一个顶级面试官,这谁顶得住啊。
我丢?这TM是人话?这是什么逻辑啊,说是问多线程然后一上来就来个这么冷门的ThreadLocal?心态崩了呀,再说你TM自己忘了不知道下去看看书么,来我这里找答案是什么鬼啊…
尽管十分不情愿,但是张三还是高速运转他的小脑袋,回忆起了ThreadLocal的种种细节…
面试官说实话我在实际开发过程中用到ThreadLocal的地方不是很多,我在写这个文章的时候还刻意去把我电脑上几十个项目打开之后去全局搜索ThreadLocal发现除了系统源码的使用,很少在项目中用到,不过也还是有的。
这,我都说了我很少用了,还问我,难受了呀,哦哦哦,有了想起来了,事务隔离级别。
面试官你好,其实我第一时间想到的就是Spring实现事务隔离级别的源码,这还是当时我大学被女朋友甩了,一个人在图书馆哭泣的时候无意间发现的。
Spring采用Threadlocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,同时,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复。
Spring框架里面就是用的ThreadLocal来实现这种隔离,主要是在 TransactionSynchronizationManager
这个类里面,代码如下所示:
<span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> Log logger = LogFactory.getLog(TransactionSynchronizationManager.class);<br><br> <span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> ThreadLocal<map<object, object>> resources =<br>   <span class="hljs-keyword">new</span> NamedThreadLocal<>(<span class="hljs-string">"Transactional resources"</span>);<br><br> <span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> ThreadLocal<set<transactionsynchronization>> synchronizations =<br>   <span class="hljs-keyword">new</span> NamedThreadLocal<>(<span class="hljs-string">"Transaction synchronizations"</span>);<br><br> <span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> ThreadLocal<string> currentTransactionName =<br>   <span class="hljs-keyword">new</span> NamedThreadLocal<>(<span class="hljs-string">"Current transaction name"</span>);<br><br>  ……<br></string></set<transactionsynchronization></map<object, object>
Spring的事务主要是ThreadLocal和AOP去做实现的,我这里提一下,大家知道每个线程自己的链接是靠ThreadLocal保存的就好了,继续的细节我会在Spring章节细说的,暖么?
来了来了,加分项来了,这个我还真遇到过,装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一下就好了。
<span class="hljs-function">before<br>  <br><span class="hljs-keyword">void</span> <span class="hljs-title">work</span><span class="hljs-params">(User user)</span> </span>{<br>    getInfo(user);<br>    checkInfo(user);<br>    setSomeThing(user);<br>    log(user);<br>}<br><br><span class="hljs-function">then<br>  <br><span class="hljs-keyword">void</span> <span class="hljs-title">work</span><span class="hljs-params">(User user)</span> </span>{<br><span class="hljs-keyword">try</span>{<br>   threadLocalUser.set(user);<br>   <br>    getInfo();<br>    checkInfo();<br>    setSomeThing();<br>    log();<br>    } <span class="hljs-keyword">finally</span> {<br>     threadLocalUser.remove();<br>    }<br>}
我看了一下很多场景的cookie,session等数据隔离都是通过ThreadLocal去做实现的。
对了我面试官允许我再秀一下知识广度,在Android中,Looper类就是利用了ThreadLocal的特性,保证每个线程只存在一个Looper对象。
<span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> ThreadLocal<looper> sThreadLocal = <span class="hljs-keyword">new</span> ThreadLocal<looper>();<br><span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">prepare</span><span class="hljs-params">(<span class="hljs-keyword">boolean</span> quitAllowed)</span> </span>{<br>    <span class="hljs-keyword">if</span> (sThreadLocal.get() != <span class="hljs-keyword">null</span>) {<br>        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> RuntimeException(<span class="hljs-string">"Only one Looper may be created per thread"</span>);<br>    }<br>    sThreadLocal.set(<span class="hljs-keyword">new</span> Looper(quitAllowed));<br>}<br></looper></looper>
好的面试官,我先说一下他的使用:
ThreadLocal<string> localName = <span class="hljs-keyword">new</span> ThreadLocal();<br>localName.set(<span class="hljs-string">"张三"</span>);<br>String name = localName.get();<br>localName.remove();<br></string>
其实使用真的很简单,线程进来之后初始化一个可以泛型的ThreadLocal对象,之后这个线程只要在remove之前去get,都能拿到之前set的值,注意这里我说的是remove之前。
他是能做到线程间数据隔离的,所以别的线程使用get()方法是没办法拿到其他线程的值的,但是有办法可以做到,我后面会说。
我们先看看他set的源码:
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">set</span><span class="hljs-params">(T value)</span> </span>{<br>    Thread t = Thread.currentThread();<br>    ThreadLocalMap map = getMap(t);<br>    <span class="hljs-keyword">if</span> (map != <span class="hljs-keyword">null</span>) <br>        map.set(<span class="hljs-keyword">this</span>, value); <br>    <span class="hljs-keyword">else</span><br>        createMap(t, value); <br>}
大家可以发现set的源码很简单,主要就是ThreadLocalMap我们需要关注一下,而ThreadLocalMap呢是当前线程Thread一个叫threadLocals的变量中获取的。
<span class="hljs-function">ThreadLocalMap <span class="hljs-title">getMap</span><span class="hljs-params">(Thread t)</span> </span>{<br>        <span class="hljs-keyword">return</span> t.threadLocals;<br>    }
<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Thread</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Runnable</span> </span>{<br>      ……<br><br>    <br>    ThreadLocal.ThreadLocalMap threadLocals = <span class="hljs-keyword">null</span>;<br><br>    <br>    ThreadLocal.ThreadLocalMap inheritableThreadLocals = <span class="hljs-keyword">null</span>;<br>  <br>     ……
这里我们基本上可以找到ThreadLocal数据隔离的真相了,每个线程Thread都维护了自己的threadLocals变量,所以在每个线程创建ThreadLocal的时候,实际上数据是存在自己线程Thread的threadLocals变量里面的,别人没办法拿到,从而实现了隔离。
面试官这个问题问得好啊,内心暗骂,让我歇一会不行么?
张三笑着回答道,既然有个Map那他的数据结构其实是很像HashMap的,但是看源码可以发现,它并未实现Map接口,而且他的Entry是继承WeakReference(弱引用)的,也没有看到HashMap中的next,所以不存在链表了。
<span class="hljs-keyword">static</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ThreadLocalMap</span> </span>{<br><br>        <span class="hljs-keyword">static</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Entry</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">WeakReference</span><<span class="hljs-title">ThreadLocal</span><?>> </span>{<br>            <br>            Object value;<br><br>            Entry(ThreadLocal<?> k, Object v) {<br>                <span class="hljs-keyword">super</span>(k);<br>                value = v;<br>            }<br>        }<br>        ……<br>    }    
结构大概长这样:
好呀,面试官你说。
用数组是因为,我们开发过程中可以一个线程可以有多个TreadLocal来存放不同类型的对象的,但是他们都将放到你当前线程的ThreadLocalMap里,所以肯定要数组来存。
至于Hash冲突,我们先看一下源码:
<span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title">set</span><span class="hljs-params">(ThreadLocal<?> key, Object value)</span> </span>{<br>           Entry[] tab = table;<br>            <span class="hljs-keyword">int</span> len = tab.length;<br>            <span class="hljs-keyword">int</span> i = key.threadLocalHashCode & (len-<span class="hljs-number">1</span>);<br>            <span class="hljs-keyword">for</span> (Entry e = tab[i];<br>                 e != <span class="hljs-keyword">null</span>;<br>                 e = tab[i = nextIndex(i, len)]) {<br>                ThreadLocal<?> k = e.get();<br><br>                <span class="hljs-keyword">if</span> (k == key) {<br>                    e.value = value;<br>                    <span class="hljs-keyword">return</span>;<br>                }<br>                <span class="hljs-keyword">if</span> (k == <span class="hljs-keyword">null</span>) {<br>                    replaceStaleEntry(key, value, i);<br>                    <span class="hljs-keyword">return</span>;<br>                }<br>            }<br>            tab[i] = <span class="hljs-keyword">new</span> Entry(key, value);<br>            <span class="hljs-keyword">int</span> sz = ++size;<br>            <span class="hljs-keyword">if</span> (!cleanSomeSlots(i, sz) && sz >= threshold)<br>                rehash();<br>        }
我从源码里面看到ThreadLocalMap在存储的时候会给每一个ThreadLocal对象一个threadLocalHashCode,在插入过程中,根据ThreadLocal对象的hash值,定位到table中的位置i, int i = key.threadLocalHashCode & (len-1)。
然后会判断一下:如果当前位置是空的,就初始化一个Entry对象放在位置i上;
<span class="hljs-keyword">if</span> (k == <span class="hljs-keyword">null</span>) {<br>    replaceStaleEntry(key, value, i);<br>    <span class="hljs-keyword">return</span>;<br>}
如果位置i不为空,如果这个Entry对象的key正好是即将设置的key,那么就刷新Entry中的value;
<span class="hljs-keyword">if</span> (k == key) {<br>    e.value = value;<br>    <span class="hljs-keyword">return</span>;<br>}
如果位置i的不为空,而且key不等于entry,那就找下一个空位置,直到为空为止。
这样的话,在get的时候,也会根据ThreadLocal对象的hash值,定位到table中的位置,然后判断该位置Entry对象中的key是否和get的key一致,如果不一致,就判断下一个位置,set和get如果冲突严重的话,效率还是很低的。
以下是get的源码,是不是就感觉很好懂了:
 <span class="hljs-function"><span class="hljs-keyword">private</span> Entry <span class="hljs-title">getEntry</span><span class="hljs-params">(ThreadLocal<?> key)</span> </span>{<br>            <span class="hljs-keyword">int</span> i = key.threadLocalHashCode & (table.length - <span class="hljs-number">1</span>);<br>            Entry e = table[i];<br>            <span class="hljs-keyword">if</span> (e != <span class="hljs-keyword">null</span> && e.get() == key)<br>                <span class="hljs-keyword">return</span> e;<br>            <span class="hljs-keyword">else</span><br>                <span class="hljs-keyword">return</span> getEntryAfterMiss(key, i, e);<br>        }<br><br> <span class="hljs-function"><span class="hljs-keyword">private</span> Entry <span class="hljs-title">getEntryAfterMiss</span><span class="hljs-params">(ThreadLocal<?> key, <span class="hljs-keyword">int</span> i, Entry e)</span> </span>{<br>            Entry[] tab = table;<br>            <span class="hljs-keyword">int</span> len = tab.length;<br><br>            <span class="hljs-keyword">while</span> (e != <span class="hljs-keyword">null</span>) {<br>                ThreadLocal<?> k = e.get();<br>              <br>                <span class="hljs-keyword">if</span> (k == key)<br>                    <span class="hljs-keyword">return</span> e;<br>                <span class="hljs-keyword">if</span> (k == <span class="hljs-keyword">null</span>)<br>                    expungeStaleEntry(i);<br>                <span class="hljs-keyword">else</span><br>                    i = nextIndex(i, len);<br>                e = tab[i];<br>            }<br>            <span class="hljs-keyword">return</span> <span class="hljs-keyword">null</span>;<br>        }
在Java中,栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存,而堆内存中的对象对所有线程可见,堆内存中的对象可以被所有线程访问。
其实不是的,因为ThreadLocal实例实际上也是被其创建的类持有(更顶端应该是被线程持有),而ThreadLocal的值其实也是被线程实例持有,它们都是位于堆上,只是通过一些技巧将可见性修改成了线程可见。
使用 InheritableThreadLocal
可以实现多个线程访问ThreadLocal的值,我们在主线程中创建一个 InheritableThreadLocal
的实例,然后在子线程中得到这个 InheritableThreadLocal
实例设置的值。
<span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title">test</span><span class="hljs-params">()</span> </span>{    <br><span class="hljs-keyword">final</span> ThreadLocal threadLocal = <span class="hljs-keyword">new</span> InheritableThreadLocal();       <br>threadLocal.set(<span class="hljs-string">"帅得一匹"</span>);    <br>Thread t = <span class="hljs-keyword">new</span> Thread() {        <br>            <br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">run</span><span class="hljs-params">()</span> </span>{            <br>      <span class="hljs-keyword">super</span>.run();            <br>      Log.i( <span class="hljs-string">"张三帅么 ="</span> + threadLocal.get());        <br>    }    <br>  };          <br>  t.start(); <br>} 
在子线程中我是能够正常输出那一行日志的,这也是我之前面试视频提到过的父子线程数据传递的问题。
传递的逻辑很简单,我在开头Thread代码提到threadLocals的时候,你们再往下看看我刻意放了另外一个变量:
Thread源码中,我们看看Thread.init初始化创建的时候做了什么:
<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Thread</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Runnable</span> </span>{<br>  ……<br>   <span class="hljs-keyword">if</span> (inheritThreadLocals && parent.inheritableThreadLocals != <span class="hljs-keyword">null</span>)<br>      <span class="hljs-keyword">this</span>.inheritableThreadLocals=ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);<br>  ……<br>}
我就截取了部分代码,如果线程的 inheritThreadLocals
变量不为空,比如我们上面的例子,而且父线程的 inheritThreadLocals
也存在,那么我就把父线程的 inheritThreadLocals
给当前线程的 inheritThreadLocals
。
是不是很有意思?
你是说内存泄露么?
这个问题确实会存在的,我跟大家说一下为什么,还记得我上面的代码么?
ThreadLocal在保存的时候会把自己当做Key存在ThreadLocalMap中,正常情况应该是key和value都应该被外界强引用才对,但是现在key被设计成WeakReference弱引用了。
我先给大家介绍一下弱引用:
只具有弱引用的对象拥有更短暂的生命周期,在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
这就导致了一个问题,ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。
就比如线程池里面的线程,线程都是复用的,那么之前的线程实例处理完之后,出于复用的目的线程依然存活,所以,ThreadLocal设定的value值被持有,导致内存泄露。
按照道理一个线程使用完,ThreadLocalMap是应该要被清空的,但是现在线程被复用了。
在代码的最后使用remove就好了,我们只要记得在使用的最后用remove把值清空就好了。
ThreadLocal<string> localName = <span class="hljs-keyword">new</span> ThreadLocal();<br><span class="hljs-keyword">try</span> {<br>    localName.set(<span class="hljs-string">"张三"</span>);<br>    ……<br>} <span class="hljs-keyword">finally</span> {<br>    localName.remove();<br>}<br></string>
remove的源码很简单,找到对应的值全部置空,这样在垃圾回收器回收的时候,会自动把他们回收掉。
key不设置成弱引用的话就会造成和entry中value一样内存泄漏的场景。
补充一点:ThreadLocal的不足,我觉得可以通过看看netty的fastThreadLocal来弥补,大家有兴趣可以康康。
什么鬼,突然这么煽情,不是很为难我的么?难道是为了锻炼我?难为大师这样为我着想,我还一直心里暗骂他,不说了回去好好学了。
其实ThreadLocal用法很简单,里面的方法就那几个,算上注释源码都没多少行,我用了十多分钟就过了一遍了,但是在我深挖每一个方法背后逻辑的时候,也让我不得不感慨Josh Bloch 和 Doug Lea的厉害之处。
在细节设计的处理其实往往就是我们和大神的区别,我认为很多不合理的点,在Google和自己不断深入了解之后才发现这才是合理,真的不服不行。
ThreadLocal是多线程里面比较冷门的一个类,使用频率比不上别的方法和类,但是通过我这篇文章,不知道你是否有新的认知呢?
另外,敖丙把自己的面试文章整理成了一本电子书,共 1630页!目录如下,还有我复习时总结的面试题以及简历模板
现在免费送给大家,在我的公众号 三太子敖丙回复 【888】 即可获取。
我是敖丙,你知道的越多,你不知道的越多,我们下期见!
人才们的 【三连】 就是敖丙创作的最大动力,如果本篇博客有任何错误和建议,欢迎人才们留言!
文章持续更新,可以微信搜一搜「 敖丙 」第一时间阅读,关注后回复【 资料】有我准备的一线大厂面试资料和简历模板,本文 GitHub https://github.com/JavaFamily 已经收录,有大厂面试完整考点,欢迎Star。
Original: https://www.cnblogs.com/aobing/p/13382184.html
Author: 敖丙
Title: Java面试必问:ThreadLocal终极篇 淦!
原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/592437/
转载文章受原作者版权保护。转载请注明原作者出处!