原标题:Java 多线程 — ThreadLocal 的应用及原理
在涉及到多线程需要共享变量的时候,一般有两种方法:其一就是使用互斥锁,使得在每个时刻只能有一个线程访问该变量,好处就是便于编码(直接使用synchronized关键字进行同步访问),缺点在于这增加了线程间的竞争,降低了效率;其二就是使用本文要讲的ThreadLocal。
如果说synchronized是以“时间换空间”,那么ThreadLocal就是 “以空间换时间” —— 因为ThreadLocal的原理就是为每个线程都提供一个这样的变量,使得这些变量是线程级别的变量,不同线程之间互不影响,从而达到可以并发访问而不出现并发问题的目的
首先我们来看一个客观的事实:当一个可变对象被多个线程访问时,可能会得到非预期的结果 —— 所以先让我们来看一个例子。在讲到并发访问的问题的时候,SimpleDateFormat总是会被拿来当成一个绝好的例子(从这点看感谢 JDK 提供了这么一个有设计缺陷的类方便我们当成反面教材 :) )。
因为SimpleDateFormat的format和parse方法共享从父类DateFormat继承而来的Calendar对象:
并且在format和parse方法中都会改变这个Calendar对象:
format方法片段:
parse方法片段:
就拿format方法来说,考虑如下的并发情景:
线程A 此时调用calendar.setTime(date1),然后 线程A 被中断;
接着 线程B 执行,然后调用calendar.setTime(date2),然后 线程B 被中断;
接着又是 线程A 执行,但是此时的calendar已经和之前的不一致了,所以便导致了并发问题。
所以因为这个共享的calendar对象,SimpleDateFormat并不是一个线程安全的类,我们写一段代码来测试下。
(1)定义DateFormatWrapper类,来包装对SimpleDateFormat的调用:
(2)然后写一个DateFormatTest,开启多个线程来使用DateFormatWrapper:
某次运行的结果:
可以发现,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:
如果使用 Java8,则初始化ThreadLocal对象的代码可以改为:
然后再运行DateFormatTest,便始终是预期的结果:
我们已经看到了ThreadLocal的功能,那ThreadLocal是如何实现为每个线程提供一份共享变量的拷贝呢?
在使用ThreadLocal时,当前线程访问ThreadLocal中包含的变量是通过get()方法,所以首先来看这个方法的实现:
通过代码可以猜测:
在某个地方(其实就是在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)方法:
直接返回的就是t.threadLocals,原来在Thread类中有一个就叫 threadLocals的ThreadLocalMap的变量:
所以每个Thread都会拥有一个ThreadLocalMap变量,来存放属于该Thread的所有ThreadLocal变量。这样来看的话,ThreadLocal就相当于一个调度器,每次调用get方法的时候,都会先找到当前线程的ThreadLocalMap,然后再在这个ThreadLocalMap中找到对应的线程本地变量。
然后我们来看看当 map为null(即第一次调用get())时调用的setInitialValue()方法:
该方法首先会调用initialValue()方法来获得该ThreadLocal对象中需要包含的变量 —— 所以这就是为什么使用ThreadLocal是需要继承ThreadLocal时并覆写initialValue()方法,因为这样才能让setInitialValue()调用initialValue()从而得到ThreadLocal包含的初始变量;然后就是当 map不为null的时候,将该变量(value)与当前ThreadLocal对象(this)在 map中进行关联;如果 map为null,则调用createMap方法:
createMap会调用ThreadLocalMap的构造方法来创建一个ThreadLocalMap对象:
可以看到该方法通过一个ThreadLocal对象(firstKey)和该ThreadLocal包含的对象(firstValue)构造了一个ThreadLocalMap对象,使得该 map在构造完毕时候就包含了这样一个键值对(firstKey-> firstValue)
为啥需要使用 Map呢?因为一个线程可能有多个ThreadLocal对象,可能是包含SimpleDateFormat,也可能是包含一个数据库连接Connection,所以不同的变量需要通过对应的ThreadLocal对象来快速查找 —— 那么 Map当然是最好的方式
ThreadLocal还提供了修改和删除当前包含对象的方法,修改的方法为set,删除的方法为remove:
很好理解,如果当前ThredLocal还没有包含值,那么就调用createMap来初始化当前线程的ThreadLocalMap对象,否则直接在 map中修改当前ThreadLocal(this)包含的值。
remove方法就是获得当前线程的ThreadLocalMap对象,然后调用这个 map的remove(ThreadLocal)方法。查看ThreadLocalMap的remove(ThreadLocal)方法的实现:
逻辑就是先找到参数(ThreadLocal对象)对应的Entry,然后调用Entry的clear()方法,再调用expungeStaleEntry(i),i为该Entry在 map的Entry数组中的索引。
(1)首先来看看e.clear()做了什么。
查看ThreadLocalMap的源代码,我们可以发现这个 “Map” 的Entry的实现如下:
可以看到,该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的前一半代码:
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的后一半代码:
作用就是扫描位置 staleSlot之后的Entry数组,清除每个 key(ThreadLocal) 为null的Entry,所以使用expungeStaleEntry可以降低内存泄漏的概率。但是如果某些ThreadLocal变量不需要使用但是却没有调用到expungeStaleEntry方法。
那么就会导致这些ThreadLocal变量长期的贮存在内存中,引起内存浪费或者泄露 —— 所以,如果确定某个ThreadLocal变量已经不需要使用,需要及时的使用ThreadLocal的remove()方法(ThreadLocal的get和set方法也会调用到expungeStaleEntry),将其从内存中清除。返回搜狐,查看更多
责任编辑: