ThreadLocal夺命11连问
前言
前一段时间,有同事使用 ThreadLocal 踩坑了,正好引起了我的兴趣。
所以近期,我抽空把ThreadLocal的源码再研究了一下,越看越有意思,发现里面的东西还真不少。
我把精华浓缩了一下,汇集成了下面11个问题,看看你能顶住第几个?
1. 为什么要用ThreadLocal?
并发编程是一项非常重要的技术,它让我们的程序变得更加高效。
但在并发的场景中,如果有多个线程同时修改公共变量,可能会出现线程安全问题,即该变量最终结果可能出现异常。
为了解决线程安全问题, JDK 出现了很多技术手段,比如:使用synchronized 或Lock ,给访问公共资源的代码上锁,保证了代码的原子性 。
但在高并发的场景中,如果多个线程同时竞争一把锁,这时会存在大量的锁等待,可能会浪费很多时间,让系统的响应时间一下子变慢。
因此, JDK 还提供了另外一种用空间换时间的新思路:ThreadLocal 。
它的核心思想是:共享变量在每个 线程 都有一个副本 ,每个线程操作的都是自己的副本,对另外的线程没有影响。
例如: @Service public class ThreadLocalService { private static final ThreadLocal threadLocal = new ThreadLocal<>(); public void add() { threadLocal.set(1); doSamething(); Integer integer = threadLocal.get(); } } 2. ThreadLocal的原理是什么?
为了搞清楚ThreadLocal的底层实现原理,我们不得不扒一下源码。
ThreadLocal 的内部有一个静态的内部类叫:ThreadLocalMap 。 public class ThreadLocal { ... public T get() { //获取当前线程 Thread t = Thread.currentThread(); //获取当前线程的成员变量ThreadLocalMap对象 ThreadLocalMap map = getMap(t); if (map != null) { //根据threadLocal对象从map中获取Entry对象 ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") //获取保存的数据 T result = (T)e.value; return result; } } //初始化数据 return setInitialValue(); } private T setInitialValue() { //获取要初始化的数据 T value = initialValue(); //获取当前线程 Thread t = Thread.currentThread(); //获取当前线程的成员变量ThreadLocalMap对象 ThreadLocalMap map = getMap(t); //如果map不为空 if (map != null) //将初始值设置到map中,key是this,即threadLocal对象,value是初始值 map.set(this, value); else //如果map为空,则需要创建新的map对象 createMap(t, value); return value; } public void set(T value) { //获取当前线程 Thread t = Thread.currentThread(); //获取当前线程的成员变量ThreadLocalMap对象 ThreadLocalMap map = getMap(t); //如果map不为空 if (map != null) //将值设置到map中,key是this,即threadLocal对象,value是传入的value值 map.set(this, value); else //如果map为空,则需要创建新的map对象 createMap(t, value); } static class ThreadLocalMap { ... } ... }
ThreadLocal 的get 方法、set 方法和setInitialValue 方法,其实最终操作的都是ThreadLocalMap 类中的数据。
其中 ThreadLocalMap 类的内部如下: static class ThreadLocalMap { static class Entry extends WeakReference> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } ... private Entry[] table; ... }
ThreadLocalMap 里面包含一个静态的内部类Entry ,该类继承于WeakReference 类,说明Entry 是一个弱引用。
ThreadLocalMap 内部还包含了一个Entry 数组,其中:Entry = ThreadLocal + value 。
而 ThreadLocalMap 被定义成了Thread 类的成员变量。 public class Thread implements Runnable { ... ThreadLocal.ThreadLocalMap threadLocals = null; }
下面用一张图从宏观上,认识一下ThreadLocal的整体结构:
从上图中看出,在每个 Thread 类中,都有一个ThreadLocalMap 的成员变量,该变量包含了一个Entry数组 ,该数组真正保存了ThreadLocal类set的数据。
Entry 是由threadLocal和value组成,其中threadLocal对象是弱引用,在GC 的时候,会被自动回收。而value就是ThreadLocal类set的数据。
下面用一张图总结一下引用关系:
上图中除了Entry的key对ThreadLocal对象是 弱引用 ,其他的引用都是强引用 。
需要特别说明的是,上图中ThreadLocal对象我画到了堆上,其实在实际的业务场景中不一定在堆上。因为如果ThreadLocal被定义成了static的,ThreadLocal的对象是类共用的,可能出现在方法区。 3. 为什么用ThreadLocal做key?
不知道你有没有思考过这样一个问题: ThreadLocalMap 为什么要用ThreadLocal 做key,而不是用Thread 做key?
如果在你的应用中,一个线程中只使用了一个 ThreadLocal 对象,那么使用Thread 做key也未尝不可。 @Service public class ThreadLocalService { private static final ThreadLocal threadLocal = new ThreadLocal<>(); }
但实际情况中,你的应用,一个线程中很有可能不只使用了一个ThreadLocal对象。这时使用 Thread 做key不就出有问题? @Service public class ThreadLocalService { private static final ThreadLocal threadLocal1 = new ThreadLocal<>(); private static final ThreadLocal threadLocal2 = new ThreadLocal<>(); private static final ThreadLocal threadLocal3 = new ThreadLocal<>(); }
假如使用 Thread 做key时,你的代码中定义了3个ThreadLocal对象,那么,通过Thread对象,它怎么知道要获取哪个ThreadLocal对象呢?
如下图所示:
因此,不能使用 Thread 做key,而应该改成用ThreadLocal 对象做key,这样才能通过具体ThreadLocal对象的get 方法,轻松获取到你想要的ThreadLocal对象。
如下图所示:
4. Entry的key为什么设计成弱引用?
前面说过,Entry的key,传入的是ThreadLocal对象,使用了 WeakReference 对象,即被设计成了弱引用。
那么,为什么要这样设计呢?
假如key对ThreadLocal对象的弱引用,改为强引用。
我们都知道ThreadLocal变量对ThreadLocal对象是有强引用存在的。
即使ThreadLocal变量生命周期完了,设置成null了,但由于key对ThreadLocal还是强引用。
此时,如果执行该代码的 线程 使用了线程池 ,一直长期存在,不会被销毁。
就会存在这样的 强引用链 :Thread变量 -> Thread对象 -> ThreadLocalMap -> Entry -> key -> ThreadLocal对象。
那么,ThreadLocal对象和ThreadLocalMap都将不会被 GC 回收,于是产生了内存泄露 问题。
为了解决这个问题,JDK的开发者们把Entry的key设计成了 弱引用 。
弱引用 的对象,在GC做垃圾清理的时候,就会被自动回收了。
如果key是弱引用,当ThreadLocal变量指向null之后,在GC做垃圾清理的时候,key会被自动回收,其值也被设置成null。
如下图所示:
接下来,最关键的地方来了。
由于当前的ThreadLocal变量已经被指向 null 了,但如果直接调用它的get 、set 或remove 方法,很显然会出现空指针异常 。因为它的生命已经结束了,再调用它的方法也没啥意义。
此时,如果系统中还定义了另外一个ThreadLocal变量b,调用了它的 get 、set 或remove ,三个方法中的任何一个方法,都会自动触发清理机制,将key为null的value值清空。
如果key和value都是null,那么Entry对象会被GC回收。如果所有的Entry对象都被回收了,ThreadLocalMap也会被回收了。
这样就能最大程度的解决 内存泄露 问题。
需要特别注意的地方是: key为null的条件是,ThreadLocal变量指向 null ,并且key是弱引用。如果ThreadLocal变量没有断开对ThreadLocal的强引用,即ThreadLocal变量没有指向null,GC就贸然的把弱引用的key回收了,不就会影响正常用户的使用? 如果当前ThreadLocal变量指向 null 了,并且key也为null了,但如果没有其他ThreadLocal变量触发get 、set 或remove 方法,也会造成内存泄露。
下面看看弱引用的例子: public static void main(String[] args) { WeakReference
iPadmini6渲染图曝光,改用直角边框,回归TouchID?近日,有媒体发布了多张iPadmini6的最新渲染图,进一步揭示了这款新品在外观设计等方面的信息。结合爆料图可以看到,iPadmini6这次外观变化比较大,它采用了iPhone12
中国移动5G手机质量报告三星GalaxyS21Ultra5G获三项第一今日,中国移动发布了中国移动2021智能硬件质量报告(第一期)及中国移动5G通信指数报告(第四期)。在此次的报告中,中国移动选取了国内外11个热门品牌中的43款5G手机产品,对它们
用半个月工资入手的洗地机值不值得在吸尘器和扫地机器人相当普及的当下,市面上又出现了针对家庭地面清洁的新产品洗地机。相较于吸尘器和扫地机的它,究竟又有哪些优势与特点?相信你和我一样,对此充满好奇。今天我要推荐的这款
什么是家用中央空调?普通的家用分体式空调我们都不陌生,无论是柜机还是挂壁机,都是一台室外机对应一台室内机。而家用中央空调(又称为家庭中央空调户式中央空调)是一个小型化的独立家庭用空调系统,可通过一台室
活久见,大反攻终于来了内容来源于公众号好好说港股,追踪港股市场复盘,了解更多交易干货和技巧。复盘总结科技板块部分龙头个股估值已经降至历史区间的底部,出现逢低买进机会,互联网公司投资价值已经凸显。01hr
2021最全整理微信微医保PK支付宝好医保(意外险篇)前两周,保妹依次对比测评了微信和支付宝的医疗险和重疾险,这次给你们安排了意外险。意外险算是大家比较熟悉的产品了,保障简单得很,价格便宜得很。关键是门槛低,对被保人的健康年龄等要求少
蔚来风波不止,技术也该知耻文羊城晚报全媒体评论员戚耀琪蔚来车主林文钦在自动驾驶功能(NOP领航状态)下,发生交通事故不幸遇难,事件让人伤心。接着又出现多起连带的风波,此番从单一车祸演变成的社会事件,确实令人
1天上6次厕所,亚马逊员工被解雇了上厕所太多会被老板开除,这个网上流传已久的段子现在变成真事了。亚马逊仓库资料图新华社法新据商业内幕8月24日报道,一名前亚马逊仓库员工近日正在起诉该公司,她声称该公司因她上厕所次数
未来有前景的行业什么行业是即将要火起来的行业随着社会人口的老龄化,但是目前的养老产业围绕老年人的服务产业相对还不发达,可以说是行业蓝海,养老产业以及围绕老年人建立的服务医疗等行业将是朝阳产业。新医
果粉来辩,IPhone手机短信乱序问题!无法解决的简单bug去年换苹果手机后,把之前多年几千条短信导入Iphone12后,所有短信乱序,时间都变成导入日期,再接收的新短信可以正常显示日期。之后发现点击查看一下之前导入的短信后,该信息直接变成
爆料丨苹果iPadmini6更多配置曝光随著9月的接近,最近开始大量出现新iPhone系列的产品信息。此前,关于即将到来的iPhone系列命名出现了多种说法,其中就包括iPhone12s和iPhone13两种方案。现在,