微信公众号: 大黄奔跑
关注我,可了解更多有趣的面试相关问题。
写在之前
Hello,大家好,第一次周末发文,今天继续给大家带来《Offer到碗里来》系列的第五篇——一个问题,引发的ThreadLocal
一系列思考。
为啥突然想以ThreadLocal
为主题写一篇文章呢?最近组里来了很多新同学,对于项目中ThreadLocal的部分用应用不太理解,问的人多了,因此利用平时时间对于ThreadLocal
面试题,核心源码梳理,一则便于自己理解,二来分享给大家用于共同交流。
关于ThreadLocal
的原理了解的人相对较少,并且由于使用场景有限,因此对于如何使用很多人可能也知之甚少。因此ThreadLocal
同样会分为两个专题,面试、源码两个角度分析。
按照惯例,我们先来看看面试中会如何考察ThreadLocal
呢?
ThreadLocal常见面试问题
- ThreadLocal是什么? "为什么用
ThreadLocal
?",ThreadLocal
能解决什么问题? ThreadLocal
的原理【字节跳动】ThreadLocal
何时会被初始化【美团】ThreadLocal
底层结构 【美团】threadlocal
底层原理,怎么处理hash
冲突的【百度】ThreadLocal
如何使用,ThreadLocal
会产生内存泄露的原因【腾讯】- threadlocal的实现,原理,业务用来做什么?
- 那
ThreadLocal
需要加锁吗? - 平时工作用
ThreadLocal
的场景
.........
可以看到ThreadLocal
可以被问到的问题众多,但是仔细发现大类主要有,ThreadLocal
是什么,有什么作用,底层原理、底层的数据结构、初始化的流程、内存泄露原因....
面试场景模拟
叮铃叮铃....一个熟悉的电话响起,对面传来饱经沧桑的声音,一听就是刚熬夜解bug
的大哥。
面试官:大黄同学是吧,先做个自我介绍吧
大黄:面试官您好,我叫大黄......
面试官:平时工作中,有用过ThreadLocal
吗?能简单说说ThreadLocal
有啥作用吗?
大黄:之前对这块有一些了解,ThreadLocal
用于提供线程局部变量。这些变量与正常的变量不同,解决了基于线程维度的变量定义,每个线程访问一个独立初始化的变量副本(通过它的get()、set()或者remove())。
大黄小提醒ThreadLocal创建的目的不是为了解决多线程问题,因为各个变量都在自己的线程中,不存在变量共享、也就不存在竞争了,所以本身也就是线程安全的。
面试官:平时有没有了解过ThreadLocal
底层是如何实现的?
大黄: Jdk
底层内部类定义hash map
的ThreadLocalMap
,用于用于存储线程本地变量。ThreadLocalMap
本身就是一个hash Map,其中的key
为线程的Id、value
为Entry
对象值,其底层的数据结构同样是个数组,初始的大小为16。而ThreadLocalMap
的value
是内部定义的静态内部类Entry
,Entry
继承于WeakReference
大黄小提醒
Entry
为什么要继承于WeakReference
呢?
- 为了帮助处理大量的及长时间存活的引用对象,其内部的类
Entry
继承于WeakReference
,主要是便于垃圾回收。
WeakReference
作用是:利用弱引用时,过期的对象只有在内存空间不足的时候,垃圾回收器就会回收该对象。
面试官:初始大小为16,那什么时候会扩容,如何扩容呢?
大黄: 当前容量达到总容量的2/3时会开始扩容,比如初始大小为16,当数组中元素个数到达12个的时候开始扩容。扩容的流程大致可以概括为:把原数组扩容2倍,并把老数组中的数据重新哈希散列进新数组中,如果发生hash冲突,则往后继续寻找空位置元素。
如果面试官继续问扩容思路,可以参考下图,结合源码一起理解。
面试官:如果遇到了hash
冲突,threadLocal
如何解决的呢?
大黄: ThreadLocalMap
解决Hash
冲突的方式并非链表的方式,而是采用线性探测的方式
大黄小课堂
线性探测:根据初始key
的hashcode
值确定元素在table
数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。
注意哦,这里解决哈希冲突的方式与hashMap处理思路不一样。
面试官:那你了解threadLocal的初始化流程吗,何时会被初始化呢?
大黄: 当用户第一次调用get()
方法的时候会初始化值。
这里可以看一下源码的解释。
设置当前线程thread-local变量的初始值。
在调用get()方法时,如果使用者重写了initialValue()方法,则用重写的逻辑初始化,否则会自动调用set()方法,进行值的初始化,
正常情况下,该方法最多会被调用一次,但是如果在get()之后调用了remove()方法移除元素,则该方法可能被调用第二次。
方法默认返回null,如果使用者想要返回的非空值,则需要继承{@code ThreadLocal},子类去重写该方法。
// 使用者需要自己实现初始化方法
protected T initialValue() {
return null;
}
面试官: ThreadLocal有什么缺点吗?
记住,这里面试官大概是想问
ThreadLocal
的
内存泄露问题
大黄: 使用ThreadLocal
会发生内存泄露问题。threadlocal
底层依赖的threadlocalMap
中的key
使用的是ThreadLocal-Entry
弱引用,在没有其他地方对ThreadlocalMap-Entry
依赖,key
就会被回收掉,但是对应的value
不会被回收掉,key
为null
,但是value
不为null
,这样没有任何的对象在使用value,Gc不会回收该对象,这就造成了内存泄露。
面试官: 那有什么办法可以避免内存泄露问题吗
大黄: 底层实现中已经考虑了这种情况,在调用 set()、get()、remove()
方法的时候,会清理掉 key
为 null
的记录。因此使⽤完ThreadLocal
⽅法后,最好⼿动调⽤ remove()
⽅法。
内存泄露,只有在出现了 key 为 null 的记录后,没有手动调用 remove() 方法,并且之后也不再调用 get()、set()、remove() 方法的情况下。
面试官: 看你对这个还挺了解,工作中哪儿有用到吗?
大黄: 我们项目中的用户登录信息校验就是通过ThreadLocal
实现的,在ThreadLocal
的初始化方法中,去第三方服务方平台校验用户登录信息,并且将解析到的用户登录信息放到ThreadLocal
包裹的用户信息中,这样即可以避免频繁调用第三方用户获取用户信息、又可以避免线程。
面试官: 嗯嗯,那我们继续看看......
面试未完待续........
当然,如果每个问题都可以回答如此,Offer自然到碗里来。
总结
本意是想着更可能回答的十全十美,无奈知识有限,难免会有纰漏,如果你发现了错误的地方,可以加我的微信交流。(微信公众号没有留言。微信号:X1032838190
)
后续应该还会继续补充一篇关于《深入理解ThreadLocal源码》,给大家一起分享ThreadLocal
的源码。
番外
另外,关注大黄奔跑公众号,第一时间收获独家整理的面试实战记录及面试知识点总结。
我是大黄,一个只会写HelloWorld
的程序员,咱们下期见。