Java面试必问:ThreadLocal终极篇 淦!

开场白

张三最近天气很热心情不是很好,所以他决定出去面试跟面试官聊聊天排解一下,结果刚投递简历就有人约了面试。

我丢,什么情况怎么刚投递出去就有人约我面试了?诶。。。真烦啊,哥已经不在江湖这么久了,江湖还是有哥的传说,我还是这么抢手的么?太烦恼了,帅无罪。

暗自窃喜的张三来到了某东现场面试的办公室,我丢,这面试官?不是吧,这满是划痕的Mac,这发量,难道就是传说中的架构师?

张三的心态一下子就崩了,出来第一场面试就遇到一个顶级面试官,这谁顶得住啊。

我丢?这TM是人话?这是什么逻辑啊,说是问多线程然后一上来就来个这么冷门的ThreadLocal?心态崩了呀,再说你TM自己忘了不知道下去看看书么,来我这里找答案是什么鬼啊…

尽管十分不情愿,但是张三还是高速运转他的小脑袋,回忆起了ThreadLocal的种种细节…

面试官说实话我在实际开发过程中用到ThreadLocal的地方不是很多,我在写这个文章的时候还刻意去把我电脑上几十个项目打开之后去全局搜索ThreadLocal发现除了系统源码的使用,很少在项目中用到,不过也还是有的。

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

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

Spring采用Threadlocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,同时,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复。

Spring框架里面就是用的ThreadLocal来实现这种隔离,主要是在 TransactionSynchronizationManager这个类里面,代码如下所示:

<span class="hljs-keyword">private</span>&#xA0;<span class="hljs-keyword">static</span>&#xA0;<span class="hljs-keyword">final</span>&#xA0;Log&#xA0;logger&#xA0;=&#xA0;LogFactory.getLog(TransactionSynchronizationManager.class);<br><br>&#xA0;<span class="hljs-keyword">private</span>&#xA0;<span class="hljs-keyword">static</span>&#xA0;<span class="hljs-keyword">final</span>&#xA0;ThreadLocal<map<object, object>>&#xA0;resources&#xA0;=<br>&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">new</span>&#xA0;NamedThreadLocal<>(<span class="hljs-string">"Transactional&#xA0;resources"</span>);<br><br>&#xA0;<span class="hljs-keyword">private</span>&#xA0;<span class="hljs-keyword">static</span>&#xA0;<span class="hljs-keyword">final</span>&#xA0;ThreadLocal<set<transactionsynchronization>>&#xA0;synchronizations&#xA0;=<br>&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">new</span>&#xA0;NamedThreadLocal<>(<span class="hljs-string">"Transaction&#xA0;synchronizations"</span>);<br><br>&#xA0;<span class="hljs-keyword">private</span>&#xA0;<span class="hljs-keyword">static</span>&#xA0;<span class="hljs-keyword">final</span>&#xA0;ThreadLocal<string>&#xA0;currentTransactionName&#xA0;=<br>&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">new</span>&#xA0;NamedThreadLocal<>(<span class="hljs-string">"Current&#xA0;transaction&#xA0;name"</span>);<br><br>&#xA0;&#xA0;&#x2026;&#x2026;<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>&#xA0;&#xA0;<br><span class="hljs-keyword">void</span>&#xA0;<span class="hljs-title">work</span><span class="hljs-params">(User&#xA0;user)</span>&#xA0;</span>{<br>&#xA0;&#xA0;&#xA0;&#xA0;getInfo(user);<br>&#xA0;&#xA0;&#xA0;&#xA0;checkInfo(user);<br>&#xA0;&#xA0;&#xA0;&#xA0;setSomeThing(user);<br>&#xA0;&#xA0;&#xA0;&#xA0;log(user);<br>}<br><br><span class="hljs-function">then<br>&#xA0;&#xA0;<br><span class="hljs-keyword">void</span>&#xA0;<span class="hljs-title">work</span><span class="hljs-params">(User&#xA0;user)</span>&#xA0;</span>{<br><span class="hljs-keyword">try</span>{<br>&#xA0;&#xA0;&#xA0;threadLocalUser.set(user);<br>&#xA0;&#xA0;&#xA0;<br>&#xA0;&#xA0;&#xA0;&#xA0;getInfo();<br>&#xA0;&#xA0;&#xA0;&#xA0;checkInfo();<br>&#xA0;&#xA0;&#xA0;&#xA0;setSomeThing();<br>&#xA0;&#xA0;&#xA0;&#xA0;log();<br>&#xA0;&#xA0;&#xA0;&#xA0;}&#xA0;<span class="hljs-keyword">finally</span>&#xA0;{<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;threadLocalUser.remove();<br>&#xA0;&#xA0;&#xA0;&#xA0;}<br>}

我看了一下很多场景的cookie,session等数据隔离都是通过ThreadLocal去做实现的。

对了我面试官允许我再秀一下知识广度,在Android中,Looper类就是利用了ThreadLocal的特性,保证每个线程只存在一个Looper对象。

<span class="hljs-keyword">static</span>&#xA0;<span class="hljs-keyword">final</span>&#xA0;ThreadLocal<looper>&#xA0;sThreadLocal&#xA0;=&#xA0;<span class="hljs-keyword">new</span>&#xA0;ThreadLocal<looper>();<br><span class="hljs-function"><span class="hljs-keyword">private</span>&#xA0;<span class="hljs-keyword">static</span>&#xA0;<span class="hljs-keyword">void</span>&#xA0;<span class="hljs-title">prepare</span><span class="hljs-params">(<span class="hljs-keyword">boolean</span>&#xA0;quitAllowed)</span>&#xA0;</span>{<br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">if</span>&#xA0;(sThreadLocal.get()&#xA0;!=&#xA0;<span class="hljs-keyword">null</span>)&#xA0;{<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">throw</span>&#xA0;<span class="hljs-keyword">new</span>&#xA0;RuntimeException(<span class="hljs-string">"Only&#xA0;one&#xA0;Looper&#xA0;may&#xA0;be&#xA0;created&#xA0;per&#xA0;thread"</span>);<br>&#xA0;&#xA0;&#xA0;&#xA0;}<br>&#xA0;&#xA0;&#xA0;&#xA0;sThreadLocal.set(<span class="hljs-keyword">new</span>&#xA0;Looper(quitAllowed));<br>}<br></looper></looper>

好的面试官,我先说一下他的使用:

ThreadLocal<string>&#xA0;localName&#xA0;=&#xA0;<span class="hljs-keyword">new</span>&#xA0;ThreadLocal();<br>localName.set(<span class="hljs-string">"&#x5F20;&#x4E09;"</span>);<br>String&#xA0;name&#xA0;=&#xA0;localName.get();<br>localName.remove();<br></string>

其实使用真的很简单,线程进来之后初始化一个可以泛型的ThreadLocal对象,之后这个线程只要在remove之前去get,都能拿到之前set的值,注意这里我说的是remove之前。

他是能做到线程间数据隔离的,所以别的线程使用get()方法是没办法拿到其他线程的值的,但是有办法可以做到,我后面会说。

我们先看看他set的源码:

<span class="hljs-function"><span class="hljs-keyword">public</span>&#xA0;<span class="hljs-keyword">void</span>&#xA0;<span class="hljs-title">set</span><span class="hljs-params">(T&#xA0;value)</span>&#xA0;</span>{<br>&#xA0;&#xA0;&#xA0;&#xA0;Thread&#xA0;t&#xA0;=&#xA0;Thread.currentThread();<br>&#xA0;&#xA0;&#xA0;&#xA0;ThreadLocalMap&#xA0;map&#xA0;=&#xA0;getMap(t);<br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">if</span>&#xA0;(map&#xA0;!=&#xA0;<span class="hljs-keyword">null</span>)&#xA0;<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;map.set(<span class="hljs-keyword">this</span>,&#xA0;value);&#xA0;<br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">else</span><br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;createMap(t,&#xA0;value);&#xA0;<br>}

大家可以发现set的源码很简单,主要就是ThreadLocalMap我们需要关注一下,而ThreadLocalMap呢是当前线程Thread一个叫threadLocals的变量中获取的。

<span class="hljs-function">ThreadLocalMap&#xA0;<span class="hljs-title">getMap</span><span class="hljs-params">(Thread&#xA0;t)</span>&#xA0;</span>{<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">return</span>&#xA0;t.threadLocals;<br>&#xA0;&#xA0;&#xA0;&#xA0;}
<span class="hljs-keyword">public</span>&#xA0;<span class="hljs-class"><span class="hljs-keyword">class</span>&#xA0;<span class="hljs-title">Thread</span>&#xA0;<span class="hljs-keyword">implements</span>&#xA0;<span class="hljs-title">Runnable</span>&#xA0;</span>{<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#x2026;&#x2026;<br><br>&#xA0;&#xA0;&#xA0;&#xA0;<br>&#xA0;&#xA0;&#xA0;&#xA0;ThreadLocal.ThreadLocalMap&#xA0;threadLocals&#xA0;=&#xA0;<span class="hljs-keyword">null</span>;<br><br>&#xA0;&#xA0;&#xA0;&#xA0;<br>&#xA0;&#xA0;&#xA0;&#xA0;ThreadLocal.ThreadLocalMap&#xA0;inheritableThreadLocals&#xA0;=&#xA0;<span class="hljs-keyword">null</span>;<br>&#xA0;&#xA0;<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#x2026;&#x2026;

这里我们基本上可以找到ThreadLocal数据隔离的真相了,每个线程Thread都维护了自己的threadLocals变量,所以在每个线程创建ThreadLocal的时候,实际上数据是存在自己线程Thread的threadLocals变量里面的,别人没办法拿到,从而实现了隔离。

面试官这个问题问得好啊,内心暗骂,让我歇一会不行么?

张三笑着回答道,既然有个Map那他的数据结构其实是很像HashMap的,但是看源码可以发现,它并未实现Map接口,而且他的Entry是继承WeakReference(弱引用)的,也没有看到HashMap中的next,所以不存在链表了。

<span class="hljs-keyword">static</span>&#xA0;<span class="hljs-class"><span class="hljs-keyword">class</span>&#xA0;<span class="hljs-title">ThreadLocalMap</span>&#xA0;</span>{<br><br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">static</span>&#xA0;<span class="hljs-class"><span class="hljs-keyword">class</span>&#xA0;<span class="hljs-title">Entry</span>&#xA0;<span class="hljs-keyword">extends</span>&#xA0;<span class="hljs-title">WeakReference</span><<span class="hljs-title">ThreadLocal</span><?>>&#xA0;</span>{<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;Object&#xA0;value;<br><br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;Entry(ThreadLocal<?>&#xA0;k,&#xA0;Object&#xA0;v)&#xA0;{<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">super</span>(k);<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;value&#xA0;=&#xA0;v;<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;}<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;}<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#x2026;&#x2026;<br>&#xA0;&#xA0;&#xA0;&#xA0;}&#xA0;&#xA0;&#xA0;&#xA0;

结构大概长这样:

好呀,面试官你说。

用数组是因为,我们开发过程中可以一个线程可以有多个TreadLocal来存放不同类型的对象的,但是他们都将放到你当前线程的ThreadLocalMap里,所以肯定要数组来存。

至于Hash冲突,我们先看一下源码:

<span class="hljs-function"><span class="hljs-keyword">private</span>&#xA0;<span class="hljs-keyword">void</span>&#xA0;<span class="hljs-title">set</span><span class="hljs-params">(ThreadLocal<?>&#xA0;key,&#xA0;Object&#xA0;value)</span>&#xA0;</span>{<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;Entry[]&#xA0;tab&#xA0;=&#xA0;table;<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">int</span>&#xA0;len&#xA0;=&#xA0;tab.length;<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">int</span>&#xA0;i&#xA0;=&#xA0;key.threadLocalHashCode&#xA0;&&#xA0;(len-<span class="hljs-number">1</span>);<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">for</span>&#xA0;(Entry&#xA0;e&#xA0;=&#xA0;tab[i];<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;e&#xA0;!=&#xA0;<span class="hljs-keyword">null</span>;<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;e&#xA0;=&#xA0;tab[i&#xA0;=&#xA0;nextIndex(i,&#xA0;len)])&#xA0;{<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;ThreadLocal<?>&#xA0;k&#xA0;=&#xA0;e.get();<br><br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">if</span>&#xA0;(k&#xA0;==&#xA0;key)&#xA0;{<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;e.value&#xA0;=&#xA0;value;<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">return</span>;<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;}<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">if</span>&#xA0;(k&#xA0;==&#xA0;<span class="hljs-keyword">null</span>)&#xA0;{<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;replaceStaleEntry(key,&#xA0;value,&#xA0;i);<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">return</span>;<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;}<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;}<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;tab[i]&#xA0;=&#xA0;<span class="hljs-keyword">new</span>&#xA0;Entry(key,&#xA0;value);<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">int</span>&#xA0;sz&#xA0;=&#xA0;++size;<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">if</span>&#xA0;(!cleanSomeSlots(i,&#xA0;sz)&#xA0;&&&#xA0;sz&#xA0;>=&#xA0;threshold)<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;rehash();<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;}

我从源码里面看到ThreadLocalMap在存储的时候会给每一个ThreadLocal对象一个threadLocalHashCode,在插入过程中,根据ThreadLocal对象的hash值,定位到table中的位置i, int i = key.threadLocalHashCode & (len-1)

然后会判断一下:如果当前位置是空的,就初始化一个Entry对象放在位置i上;

<span class="hljs-keyword">if</span>&#xA0;(k&#xA0;==&#xA0;<span class="hljs-keyword">null</span>)&#xA0;{<br>&#xA0;&#xA0;&#xA0;&#xA0;replaceStaleEntry(key,&#xA0;value,&#xA0;i);<br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">return</span>;<br>}

如果位置i不为空,如果这个Entry对象的key正好是即将设置的key,那么就刷新Entry中的value;

<span class="hljs-keyword">if</span>&#xA0;(k&#xA0;==&#xA0;key)&#xA0;{<br>&#xA0;&#xA0;&#xA0;&#xA0;e.value&#xA0;=&#xA0;value;<br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">return</span>;<br>}

如果位置i的不为空,而且key不等于entry,那就找下一个空位置,直到为空为止。

这样的话,在get的时候,也会根据ThreadLocal对象的hash值,定位到table中的位置,然后判断该位置Entry对象中的key是否和get的key一致,如果不一致,就判断下一个位置,set和get如果冲突严重的话,效率还是很低的。

以下是get的源码,是不是就感觉很好懂了:

&#xA0;<span class="hljs-function"><span class="hljs-keyword">private</span>&#xA0;Entry&#xA0;<span class="hljs-title">getEntry</span><span class="hljs-params">(ThreadLocal<?>&#xA0;key)</span>&#xA0;</span>{<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">int</span>&#xA0;i&#xA0;=&#xA0;key.threadLocalHashCode&#xA0;&&#xA0;(table.length&#xA0;-&#xA0;<span class="hljs-number">1</span>);<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;Entry&#xA0;e&#xA0;=&#xA0;table[i];<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">if</span>&#xA0;(e&#xA0;!=&#xA0;<span class="hljs-keyword">null</span>&#xA0;&&&#xA0;e.get()&#xA0;==&#xA0;key)<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">return</span>&#xA0;e;<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">else</span><br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">return</span>&#xA0;getEntryAfterMiss(key,&#xA0;i,&#xA0;e);<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;}<br><br>&#xA0;<span class="hljs-function"><span class="hljs-keyword">private</span>&#xA0;Entry&#xA0;<span class="hljs-title">getEntryAfterMiss</span><span class="hljs-params">(ThreadLocal<?>&#xA0;key,&#xA0;<span class="hljs-keyword">int</span>&#xA0;i,&#xA0;Entry&#xA0;e)</span>&#xA0;</span>{<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;Entry[]&#xA0;tab&#xA0;=&#xA0;table;<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">int</span>&#xA0;len&#xA0;=&#xA0;tab.length;<br><br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">while</span>&#xA0;(e&#xA0;!=&#xA0;<span class="hljs-keyword">null</span>)&#xA0;{<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;ThreadLocal<?>&#xA0;k&#xA0;=&#xA0;e.get();<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">if</span>&#xA0;(k&#xA0;==&#xA0;key)<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">return</span>&#xA0;e;<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">if</span>&#xA0;(k&#xA0;==&#xA0;<span class="hljs-keyword">null</span>)<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;expungeStaleEntry(i);<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">else</span><br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;i&#xA0;=&#xA0;nextIndex(i,&#xA0;len);<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;e&#xA0;=&#xA0;tab[i];<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;}<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">return</span>&#xA0;<span class="hljs-keyword">null</span>;<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;}

Java中,栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存,而堆内存中的对象对所有线程可见,堆内存中的对象可以被所有线程访问。

其实不是的,因为ThreadLocal实例实际上也是被其创建的类持有(更顶端应该是被线程持有),而ThreadLocal的值其实也是被线程实例持有,它们都是位于堆上,只是通过一些技巧将可见性修改成了线程可见。

使用 InheritableThreadLocal可以实现多个线程访问ThreadLocal的值,我们在主线程中创建一个 InheritableThreadLocal的实例,然后在子线程中得到这个 InheritableThreadLocal实例设置的值。

<span class="hljs-function"><span class="hljs-keyword">private</span>&#xA0;<span class="hljs-keyword">void</span>&#xA0;<span class="hljs-title">test</span><span class="hljs-params">()</span>&#xA0;</span>{&#xA0;&#xA0;&#xA0;&#xA0;<br><span class="hljs-keyword">final</span>&#xA0;ThreadLocal&#xA0;threadLocal&#xA0;=&#xA0;<span class="hljs-keyword">new</span>&#xA0;InheritableThreadLocal();&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<br>threadLocal.set(<span class="hljs-string">"&#x5E05;&#x5F97;&#x4E00;&#x5339;"</span>);&#xA0;&#xA0;&#xA0;&#xA0;<br>Thread&#xA0;t&#xA0;=&#xA0;<span class="hljs-keyword">new</span>&#xA0;Thread()&#xA0;{&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<br>&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-function"><span class="hljs-keyword">public</span>&#xA0;<span class="hljs-keyword">void</span>&#xA0;<span class="hljs-title">run</span><span class="hljs-params">()</span>&#xA0;</span>{&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">super</span>.run();&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;Log.i(&#xA0;<span class="hljs-string">"&#x5F20;&#x4E09;&#x5E05;&#x4E48;&#xA0;="</span>&#xA0;+&#xA0;threadLocal.get());&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<br>&#xA0;&#xA0;&#xA0;&#xA0;}&#xA0;&#xA0;&#xA0;&#xA0;<br>&#xA0;&#xA0;};&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<br>&#xA0;&#xA0;t.start();&#xA0;<br>}&#xA0;

在子线程中我是能够正常输出那一行日志的,这也是我之前面试视频提到过的父子线程数据传递的问题。

传递的逻辑很简单,我在开头Thread代码提到threadLocals的时候,你们再往下看看我刻意放了另外一个变量:

Thread源码中,我们看看Thread.init初始化创建的时候做了什么:

<span class="hljs-keyword">public</span>&#xA0;<span class="hljs-class"><span class="hljs-keyword">class</span>&#xA0;<span class="hljs-title">Thread</span>&#xA0;<span class="hljs-keyword">implements</span>&#xA0;<span class="hljs-title">Runnable</span>&#xA0;</span>{<br>&#xA0;&#xA0;&#x2026;&#x2026;<br>&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">if</span>&#xA0;(inheritThreadLocals&#xA0;&&&#xA0;parent.inheritableThreadLocals&#xA0;!=&#xA0;<span class="hljs-keyword">null</span>)<br>&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;<span class="hljs-keyword">this</span>.inheritableThreadLocals=ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);<br>&#xA0;&#xA0;&#x2026;&#x2026;<br>}

我就截取了部分代码,如果线程的 inheritThreadLocals变量不为空,比如我们上面的例子,而且父线程的 inheritThreadLocals也存在,那么我就把父线程的 inheritThreadLocals给当前线程的 inheritThreadLocals

是不是很有意思?

你是说内存泄露么?

这个问题确实会存在的,我跟大家说一下为什么,还记得我上面的代码么?

ThreadLocal在保存的时候会把自己当做Key存在ThreadLocalMap中,正常情况应该是key和value都应该被外界强引用才对,但是现在key被设计成WeakReference弱引用了。

我先给大家介绍一下弱引用:

只具有弱引用的对象拥有更短暂的生命周期,在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

这就导致了一个问题,ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。

就比如线程池里面的线程,线程都是复用的,那么之前的线程实例处理完之后,出于复用的目的线程依然存活,所以,ThreadLocal设定的value值被持有,导致内存泄露。

按照道理一个线程使用完,ThreadLocalMap是应该要被清空的,但是现在线程被复用了。

在代码的最后使用remove就好了,我们只要记得在使用的最后用remove把值清空就好了。

ThreadLocal<string>&#xA0;localName&#xA0;=&#xA0;<span class="hljs-keyword">new</span>&#xA0;ThreadLocal();<br><span class="hljs-keyword">try</span>&#xA0;{<br>&#xA0;&#xA0;&#xA0;&#xA0;localName.set(<span class="hljs-string">"&#x5F20;&#x4E09;"</span>);<br>&#xA0;&#xA0;&#xA0;&#xA0;&#x2026;&#x2026;<br>}&#xA0;<span class="hljs-keyword">finally</span>&#xA0;{<br>&#xA0;&#xA0;&#xA0;&#xA0;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/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • Activiti7 多实例子流程

    顾名思义,子流程是一个包含其他活动、网关、事件等的活动,这些活动本身形成了一个流程,该流程是更大流程的一部分。 使用子流程确实有一些限制: 一个子流程只能有一个none类型的启动事…

    Java 2023年6月7日
    0103
  • Java——基础

    public class Hello{     public static void main(String[] args){         System.out.print(&…

    Java 2023年6月5日
    071
  • 130_RabbitMQ使用场景

    异步-同步异步问题 串行方式 并行方式 异步线程池 异步消息队列的方式 解耦-高内聚,低耦合 削峰-流量的削峰 分布式事务的可靠消费和可靠生产 索引、缓存、静态化处理的数据同步 流…

    Java 2023年6月7日
    068
  • 关于工资倒挂

    工资倒挂是指「新员工能力不如老员工,工资却高过老员工」。 如果你是上述老员工 你会觉得不爽,因为不公平。但在多数情况下,其实这件事并不坏: 你打算离职 从新员工工资来看, 你的市场…

    Java 2023年6月16日
    0109
  • MySQL 触发器

    触发器是 MySQL 的数据库对象之一,不需要程序调用或手工启动,而是由事件来触发、激活,从而实现执行,包括 INSERT 语句、UPDATE 语句和 DELETE 语句 创建触发…

    Java 2023年6月8日
    0111
  • 13.数组string关于length

    数组没有length()这个方法,有length属性 string有length()这个方法。 length不是方法,是属性,数组的属性;length()是字符串String的一个…

    Java 2023年6月9日
    094
  • idea使用教程-模板的使用

    一、代码模板是什么 它的原理就是配置一些常用代码字母缩写,在输入简写时可以出现你预定义的固定模式的代码,使得开发效率大大提高,同时也可以增加个性化。最简单的例子就是在Java中输入…

    Java 2023年6月5日
    073
  • 草图?不管黑猫白猫,能把你的设计理念讲清楚才行

    我在日常工作中,经常要参加一些技术活动,或被拉去参加一些需求会或运营会,时间比较分散。 上周在参加一个代码评审时,发现程序上该复用的没有复用,却写了两份逻辑几乎相同的代码。另外,还…

    Java 2023年6月15日
    093
  • Spring Security OAuth正式终止维护,已从官网下架

    Spring Security团队正式宣布 Spring Security OAuth终止维护。 目前官网的主页已经高亮提醒彻底停止维护。 旧的 Spring Security O…

    Java 2023年5月30日
    086
  • java学习之动态代理

    在后面的漏洞研究的学习中,必须要会的几个知识点。反射机制和动态代理机制。至于反射的前面已经讲到过了,这里就不做更多的赘述了。反射是通过class文件去获取对象对象的方法. &amp…

    Java 2023年6月13日
    085
  • Java 元注解

    学习地址:https://blog.csdn.net/sw5131899/article/details/54947192 java注解使用是相当频繁,特别是在搭建一些框架时,用到…

    Java 2023年6月5日
    092
  • Kotlin学习快速入门(9)—— 密封类的使用

    原文地址: Kotlin学习快速入门(9)—— 密封类的使用 – Stars-One的杂货小窝 代码逻辑中,很多时候我们会需要分支语句,来根据数据的情况走不同的处理逻辑…

    Java 2023年6月13日
    0115
  • java selenium (十一) 操作弹出对话框

    Web 开发人员通常需要利用JavaScript弹出对话框来给用户一些信息提示, 包括以下几种类型 对话框类型 警告框: 用于提示用户相关信息的验证结果, 错误或警告等 提示框: …

    Java 2023年5月29日
    079
  • EdgeX Foundry初体验(六)–协议介绍:MQTT

    1,MQTT 介绍 (1)MQTT(Message Queuing Telemetry Transport,消息队列遥测传输)是 IBM 开发的一个即时通讯协议,它是一种轻量级的、…

    Java 2023年5月29日
    076
  • java学习第二天

    Day02 &#x7B2C;&#x4E8C;&#x5929;&#x4E3B;&#x8981;&#x4E86;&#x89E3;…

    Java 2023年6月8日
    062
  • <3>Linux-文件操作命令(2)

    vi 编辑器的使用 vi 简介 vi可以执行输出、删除、查找、替换、块操作等众多文本操作,而且用户可以根据自己的需要对其环境进行定制.只是一个文本编辑器,不能排版。vi没有菜单,只…

    Java 2023年6月15日
    083
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球