java threadlocal 缺点_Java 多线程 — ThreadLocal 的应用及原理

原标题:Java 多线程 — ThreadLocal 的应用及原理

在涉及到多线程需要共享变量的时候,一般有两种方法:其一就是使用互斥锁,使得在每个时刻只能有一个线程访问该变量,好处就是便于编码(直接使用synchronized关键字进行同步访问),缺点在于这增加了线程间的竞争,降低了效率;其二就是使用本文要讲的ThreadLocal。

如果说synchronized是以“时间换空间”,那么ThreadLocal就是 “以空间换时间” —— 因为ThreadLocal的原理就是为每个线程都提供一个这样的变量,使得这些变量是线程级别的变量,不同线程之间互不影响,从而达到可以并发访问而不出现并发问题的目的

首先我们来看一个客观的事实:当一个可变对象被多个线程访问时,可能会得到非预期的结果 —— 所以先让我们来看一个例子。在讲到并发访问的问题的时候,SimpleDateFormat总是会被拿来当成一个绝好的例子(从这点看感谢 JDK 提供了这么一个有设计缺陷的类方便我们当成反面教材 :) )。

因为SimpleDateFormat的format和parse方法共享从父类DateFormat继承而来的Calendar对象:

并且在format和parse方法中都会改变这个Calendar对象:

format方法片段:

12414a4810b7871a54f39f94f5814e2f.png

parse方法片段:

60bf8f576d37d1de80d8940ed820e871.png

就拿format方法来说,考虑如下的并发情景:

线程A 此时调用calendar.setTime(date1),然后 线程A 被中断;

接着 线程B 执行,然后调用calendar.setTime(date2),然后 线程B 被中断;

接着又是 线程A 执行,但是此时的calendar已经和之前的不一致了,所以便导致了并发问题。

所以因为这个共享的calendar对象,SimpleDateFormat并不是一个线程安全的类,我们写一段代码来测试下。

(1)定义DateFormatWrapper类,来包装对SimpleDateFormat的调用:

38ff23c94c52243f2ea17ac7e7464037.png

(2)然后写一个DateFormatTest,开启多个线程来使用DateFormatWrapper:

b015f81115e6b83492dacdcfd663e4a0.png

3dbc7d44dbf236a03eb10232aecfb925.png

某次运行的结果:

36f68cdf529e04c38222fe8926154701.png

可以发现,SimpleDateFormat在多线程共享的情况下,不仅可能会出现结果错误的情况,还可能会由于并发访问导致运行异常。当然,我们肯定有解决的办法:

为DateFormatWrapper的format和parse方法加上synchronized关键字,坏处就是前面提到的这会加大线程间的竞争和切换而降低效率;

不使用全局的SimpleDateFormat对象,而是每次使用format和parse方法都新建一个SimpleDateFormat对象,坏处也很明显,每次调用format或者parse方法都要新建一个SimpleDateFormat,这会加大 GC 的负担;

使用ThreadLocal。ThreadLocal可以为每个线程提供一个独立的SimpleDateFormat对象,创建的SimpleDateFormat对象个数最多和线程个数相同,相比于 (1),使用ThreadLocal不存在线程间的竞争;相比于 (2),使用ThreadLocal创建的SimpleDateFormat对象个数也更加合理(不会超过线程的数量)。

我们使用ThreadLocal来对DateFormatWrapper进行修改,使得每个线程使用单独的SimpleDateFormat:

5692724cb1d3e6a57ac387bc4c60a03e.png

如果使用 Java8,则初始化ThreadLocal对象的代码可以改为:

然后再运行DateFormatTest,便始终是预期的结果:

13aecf8233b36cd155724f1e3863ae52.png

我们已经看到了ThreadLocal的功能,那ThreadLocal是如何实现为每个线程提供一份共享变量的拷贝呢?

在使用ThreadLocal时,当前线程访问ThreadLocal中包含的变量是通过get()方法,所以首先来看这个方法的实现:

d1fc290fefd6074b65e3e7cdf59b200e.png

通过代码可以猜测:

在某个地方(其实就是在ThreadLocal的内部),JDK 实现了一个类似于HashMap的类,叫ThreadLocalMap,该 “Map” 的键类型为ThreadLocal,值类型为T;

然后每个线程都关联着一个ThreadLocalMap对象,并且可以通过getMap(Thread t)方法来获得 线程t 关联的ThreadLocalMap对象;

ThreadLocalMap类有个以ThreadLocal对象为参数的getEntry(ThreadLocal)的方法,用来获得当前ThreadLocal对象关联的Entry对象。一个Entry对象就是一个键值对,键(key)是ThreadLocal对象,值(value)是该ThreadLocal对象包含的变量(即 T)。

查看getMap(Thread)方法:

5d0bf3d2cbcde3213e26fe5b67fce2b5.png

直接返回的就是t.threadLocals,原来在Thread类中有一个就叫 threadLocals的ThreadLocalMap的变量:

80c32cd0ad1f7a7bec9a7927dc16d1dd.png

所以每个Thread都会拥有一个ThreadLocalMap变量,来存放属于该Thread的所有ThreadLocal变量。这样来看的话,ThreadLocal就相当于一个调度器,每次调用get方法的时候,都会先找到当前线程的ThreadLocalMap,然后再在这个ThreadLocalMap中找到对应的线程本地变量。

844ef1b7cb2603a55eead97ad32359b0.png

然后我们来看看当 map为null(即第一次调用get())时调用的setInitialValue()方法:

d8bb51688181f985bddab75241e4623d.png

该方法首先会调用initialValue()方法来获得该ThreadLocal对象中需要包含的变量 —— 所以这就是为什么使用ThreadLocal是需要继承ThreadLocal时并覆写initialValue()方法,因为这样才能让setInitialValue()调用initialValue()从而得到ThreadLocal包含的初始变量;然后就是当 map不为null的时候,将该变量(value)与当前ThreadLocal对象(this)在 map中进行关联;如果 map为null,则调用createMap方法:

createMap会调用ThreadLocalMap的构造方法来创建一个ThreadLocalMap对象:

3fd7608a3d435acf40e345e2a0fd80ce.png

可以看到该方法通过一个ThreadLocal对象(firstKey)和该ThreadLocal包含的对象(firstValue)构造了一个ThreadLocalMap对象,使得该 map在构造完毕时候就包含了这样一个键值对(firstKey-> firstValue)

为啥需要使用 Map呢?因为一个线程可能有多个ThreadLocal对象,可能是包含SimpleDateFormat,也可能是包含一个数据库连接Connection,所以不同的变量需要通过对应的ThreadLocal对象来快速查找 —— 那么 Map当然是最好的方式

ThreadLocal还提供了修改和删除当前包含对象的方法,修改的方法为set,删除的方法为remove:

0281c35f96e32b6e740fbaa1e7aaf973.png

很好理解,如果当前ThredLocal还没有包含值,那么就调用createMap来初始化当前线程的ThreadLocalMap对象,否则直接在 map中修改当前ThreadLocal(this)包含的值。

5843de7665ece7a47b6610de290910fe.png

remove方法就是获得当前线程的ThreadLocalMap对象,然后调用这个 map的remove(ThreadLocal)方法。查看ThreadLocalMap的remove(ThreadLocal)方法的实现:

f4a2aa690cd38ae09e35f7ef112757b7.png

逻辑就是先找到参数(ThreadLocal对象)对应的Entry,然后调用Entry的clear()方法,再调用expungeStaleEntry(i),i为该Entry在 map的Entry数组中的索引。

(1)首先来看看e.clear()做了什么。

查看ThreadLocalMap的源代码,我们可以发现这个 “Map” 的Entry的实现如下:

2b1d8858b19665fff1b810429db485b8.png

可以看到,该Entry类继承自WeakReference>,所以Entry是一个WeakReference(弱引用),而且该WeakReference包含的是一个ThreadLocal对象 —— 因而每个Entry 是一个弱引用的 ThreadLocal 对象(又因为Entry包括了一个 value变量,所以该Entry构成了一个ThreadLocal -> Object的键值对),而Entry的clear()方法,是继承自WeakReference,作用就是将WeakReference包含的对象的引用设置为null:

我们知道对于一个弱引用的对象,一旦该对象不再被其他对象引用(比如像clear()方法那样将对象引用直接设置为null),那么在 GC 发生的时候,该对象便会被 GC 回收。所以让Entry作为一个WeakReference,配合ThreadLocal的remove方法,可以及时清除某个Entry中的ThreadLocal(Entry的 key)。

(2)expungeStaleEntry(i)的作用

先来看expungeStaleEntry的前一半代码:

fe6bfe3de370891a1b29a682b02e02ec.png

expungeStaleEntry这部分代码的作用就是将 i位置上的Entry的 value设置为null,以及将Entry的引用设置为null。为什么要这做呢?因为前面调用e.clear(),只是将Entry的 key设置为null并且可以使其在 GC 是被快速回收,但是Entry的 value在调用e.clear()后并不会为null—— 所以如果不对 value也进行清除,那么就可能会导致内存泄漏了。因此expungeStaleEntry方法的一个作用在于可以把需要清除的Entry彻底的从ThreadLocalMap中清除(key,value,Entry全部设置为null)。但是expungeStaleEntry还有另外的功能:看expungeStaleEntry的后一半代码:

ea1bb365d25e07ee6b9a018c005244d6.png

作用就是扫描位置 staleSlot之后的Entry数组,清除每个 key(ThreadLocal) 为null的Entry,所以使用expungeStaleEntry可以降低内存泄漏的概率。但是如果某些ThreadLocal变量不需要使用但是却没有调用到expungeStaleEntry方法。

那么就会导致这些ThreadLocal变量长期的贮存在内存中,引起内存浪费或者泄露 —— 所以,如果确定某个ThreadLocal变量已经不需要使用,需要及时的使用ThreadLocal的remove()方法(ThreadLocal的get和set方法也会调用到expungeStaleEntry),将其从内存中清除。返回搜狐,查看更多

责任编辑:

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值