ThreadLocal
ThreadLocal
摘自《Java并发编程之美》再结合一些自己的理解
简介
多线程访问同一个共享变量时特别容易出现并发问题,特别时在多个线程需要对同一个共享变量进行写入时。为了保证线程安全,一般使用者在访问共享变量时需要进行适当的同步,如图。
同步的措施一般是加锁,这就需要使用者对锁有一定的了解,这显然加重了使用者的负担。那么有没有一种方式可以做到,当创建一个变量后,每个线程对其访问的时候访问的是自己线程的变量呢?其实 ThreadLocal
就可以做这件事,虽然 ThreadLocal
并不是为了解决这个问题而出现的。
ThreadLocal
是 JDK
包提供的,他提供了线程本地变量,也就是如果你创建了一个 ThreadLocal
变量时,实际操作的是自己本地内存里面的变量,从而避免了线程安全问题。创建一个 ThreadLocal
变量后,每个线程都会复制一个变量到自己的本地内存(也就是如果你创建了一个 ThreadLocal
变量,那么访问这个变量的每个线程都会有该变量的一个副本),如图。
使用示例
本例开启了两个线程,在每个线程内部都设置了本地变量的值,然后调用 print 函数打印当前本地变量的值。如果打印后调用了本 变量的remove 方法, 删除本地内存中的该变量,代码如下。
public class ThreadLocalDemo { |
执行结果
线程One中的代码通过 set 方法设置了 localVariable 的值,这其实设置的是线程One 本地内存中的一个副本,这个副本线程 Two 是访问不了的。然后调用了print函数,通过 get 函数取得了当前线程本地内存中 localVariable 的值。线程Two执行同理
实现原理
ThreadLocal
相关类的类图结构
由图可知,Thread类中有一个 threadLocals
变量和与一个 inheritableThreadLocals
变量,他们都是 ThreadLocalMap
类型的变量 。在默认情况下,每个线程中的的这两个变量都为null,只有当线程第一次调用 ThreadLocal
的 set 或者get 方法时才会创建它们。
ThreadLocalMap
是一个定制化的Hashmap
。
其实当我们调用 ThreadLocal
的 set 方法存储本地变量时,并不存放在 ThreadLocal
实例里面,而是存放在调用线程的 threadLocals
变量里面。也就是说,**ThreadLocal
类型实例的本地变量存放在具体的线程空间中**。
Thread 就是一个工具壳,他并不是正真存放变量的地方,而是通过 set 方法和 get 方法 把 value 值存放到调用线程的
threadLocals
里面存放和从中拿出来。如果调用的线程一直不终止,那么这个本地变量会一直存放在调用线程的
threadLocals
变量里面,所以当不需要使用本地变量时可以通过调用ThreadLocal
变量的remove方法,从当前线程的threadLocals
中删除该本地变量。
Thread 里面的 threadLocals
之所以被设计为 map 结构,是因为每个线程可以关联多个 ThreadLocal
变量
API分析
void set(T value)
public void set(T value) { |
可以发现上述代码中一个段是根据当前调用线程,获取map,调用了 getMap(t)
方法
ThreadLocalMap getMap(Thread t) { |
该方法的作用时获取传入线程的 threadLocals
变量,threadlocal 变量被绑定到了线程的成员变量上。
再回到 set 方法,如果 getMap(t)
返回值不为空,就把 value 值设置到 threadLocals
中。 threadLocals
是一个 HashMap
结构,其中 key 就是当前 ThreadLocal
的实例对象引用,value 是通过 set 方法传递的值
如果 getMap(t)
返回值为空,则说明是第一次调用 set 方法,这时会调用 createMap(t, value)
初始化当前线程的 threadLocals
变量。
void createMap(Thread t, T firstValue) { |
T get()
public T get() { |
同样也是获得当前调用的线程,通过该线程去获得线程内部的 threadLocals
变量,如果 getMap(t)
返回值不为空,再根据当前 ThreadLocal
实例作为key去获取 threadLocals
中对应的值。
如果 getMap(t)
返回值为空或者获取到的变量值为空,则执行 setInitialValue()
方法进行初始化。
private T setInitialValue() { |
如果当前线程的 threadLocals
变量不为空,则设置当前线程本地变量值为null,否则调用 createMap
方法创建当前线程的 threadLocals
变量。
void remove()
public void remove() { |
如果当前线程的 threadLocals
变量不为空,则删除当前线程中指定 ThreadLocal
实例对应的本地变量
总结
每个线程内部都有一个名为 threadLocals
的成员变量,该变量的类型是 HashMap
,其中 key 为我们定义的 ThreadLocal
变量的 this 引用, value 则为我们使用 set 方法设置的值。每个线程的本地变量存放在线程自己的内存变量 threadLocals
中。
如果当前线程一直不消亡,那么这些本地变量会一直存在,所以可能会造成内存溢出,因此使用完毕后要记得调用 ThreadLocal
的 remove 方法删除对应线程的 threadLocals
中的本地变量。
这个图很形象的介绍了内存关系。
TheadLocal 不支持继承性
首先看一个例子
public class ThreadLocalDemo02 { |
输出结果如下:
原因就是父子线程并不是同一个线程,所以获取不到父线程 threadLocals
中的本地变量。
那么有没有办法让子线程能访问到父线程中的值? 答案是有。**InheritableThreadLocal类
**
lnheritableThreadLocal类
为了解决子线程无法访问父线程中值的问题,InheritableThreadLocal
应运而生。InheritableThreadLocal
继承自 ThreadLocal
, 其提供一个特性,就是让子线程可以访问在父线程中设置的本地变量。下面是 InheritableThreadLocal
的代码。
public class InheritableThreadLocal<T> extends ThreadLocal<T> { |
InheritableThreadLocal
继承了 ThreadLocal
,并重写了三个方法。由上述代码可知,InheritableThreadLocal
**重写了 createMap
方法,现在当第一次调用 set 方法时,创建的是当前线程的 inheritableThreadLocals
变量的实例而不是 threadLocals
**。
当调用 get 方法时获取当前线程内部的 map 变量时,获取的是 inheritableThreadLocals
变量的实例而不再是 threadLocals
综上可知,在 InheritableThreadLocal
中,变量 inheritableThreadLocals
代替了 threadLocals
接下来,我要揭秘 InheritableThreadLocal
让子线程可以访问父线程的本地变量。这要从创建 Thread 的代码说起,打开 Thread 类的默认构造函数,代码如下。
public Thread(Runnable target) { |
如上代码在创建线程时,在构造函数里面会调用 init
方法。代码中获取了当前线程(父线程,这里是 main 函数所在的线程),然后判断 main 函数所在线程里面的 inheritableThreadLocals
属性是否为空以及 inheritableThreadLocals
变量是否为 true,如果都满足就会执行 createInheritedMap()
方法
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) { |
可以看到,在方法内部使用父线程的 inheritableThreadLocals
变量作为构造函数的变量创建了一个新的 ThreadLocalMap
变量,然后赋值给了子线程的 inheritableThreadLocals
变量。
下面我们看看 ThreadLocalMap
的构造函数内部都做了什么
private ThreadLocalMap(ThreadLocalMap parentMap) { |
在该构造函数内部把父线程的 inheritableThreadLocals
成员变量的值复制到了新的 ThreadLocalMap
对象中 , 其中调用了 InheritableThreadLocal
类重写的代码
总结
InheritableThreadLocal
类通过重写代码让本地变量保存到了具体线程的 inheritableThreadLocals
变量里面。
线程在通过 InheritableThreadLocal
类实例的 set 或者 get方法设置变量时,就会创建当前线程的 inheritableThreadLocals
变量。当父线程创建子线程时,构造函数会把父线程中 inheritableThreadLocals
变量里面的本地变量复制一份保存到子线程的 inheritableThreadLocals
变量里面。
注意:只会在初始化线程的时候才会把父线程中
inheritableThreadLocals
内的本地变量复制到子线程中。线程一旦初始化后,父线程再调用 set 或者 remove 方法修改inheritableThreadLocals
是不影响子线程的。
修改原来代码
//创建线程变量 |
输出结果
可见,现在可以从子线程正常获取到父线程变量的值了。
其实子线程使用父线程中的
threadlocal
方法有很多种,比如:
- 创建线程时,传入父线程中的变量,并将其复制到子线程中
- 在父线程中构造一个 map 作为参数传递给子线程
但这些方法都改变了我们的使用习惯,所以在这些情况下就显得
InheritableThreadLocal
比较有用。
使用场景
- 子线程需要使用存放在
threadLocal
变量中的用户登录信息 - 中间件需要把统一的 id 追踪的整个调用链路记录下来。
ThreadLocal内存泄漏
强引用与弱引用
实线代表强引用,虚线代表弱引用
每一个Thread维护一个
ThreadLocalMap
, key为使用弱引用的ThreadLocal
实例,value为线程变量的副本。强引用,使用最普遍的引用,一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,Java虚拟机宁愿抛出
OutOfMemoryError
错误,使程序异常终止,也不回收这种对象。如果想取消强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样可以使
JVM
在合适的时间就会回收该对象。弱引用,
JVM
进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java
中,用java.lang.ref.WeakReference
类来表示。
如果一个对象只具有弱引用,那就类似于可有可无的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。
弱引用可以和一个引用队列(
ReferenceQueue
)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。
ThreadLocal 内存泄露问题是怎么导致的?
ThreadLocalMap
中使用的 key 为 ThreadLocal
的弱引用,而 value 是强引用。所以,如果 ThreadLocal
没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。
这样一来,ThreadLocalMap
中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC
回收,这个时候就可能会产生内存泄露。ThreadLocalMap
实现中已经考虑了这种情况,在调用 set()
、get()
、remove()
方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal
方法后 最好手动调用remove()
方法
//弱引用 |