正确理解ThreadLocal
首先,ThreadLocal 不是用来解决共享对象的多线程访问问题的!!!
JDK源码中这样描述:
This class provides thread-local variables. These variables differ from
their normal counterparts in that each thread that accesses one (via its
{@code get} or {@code set} method) has its own, independently initialized
copy of the variable. {@code ThreadLocal} instances are typically private
static fields in classes that wish to associate state with a thread (e.g.,
a user ID or Transaction ID).
翻译为:
ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get或set方法访问)时能保证各个线程里的变量相对独立于其他线程内的变量。ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程的上下文。
另外,说ThreadLocal使得各线程能够保持各自独立的一个对象,并不是通过ThreadLocal.set()来实现的,而是通过每个线程中的new 对象的操作来创建的对象,每个线程创建一个,不是什么对象的拷贝或副本。通过ThreadLocal.set()将这个新创建的对象的引用保存到各线程的自己的一个map中,每个线程都有这样一个map,执行ThreadLocal.get()时,各线程从自己的map中取出放进去的对象,因此取出来的是各自自己线程中的对象,ThreadLocal实例是作为map的key来使用的。
如果ThreadLocal.set()进去的东西本来就是多个线程共享的同一个对象,那么多个线程的ThreadLocal.get()取得的还是这个共享对象本身,还是有并发访问问题。
ThreadLocal基本操作
构造函数
ThreadLocal的构造函数签名是这样的:
1 | /** |
initialValue函数
initialValue函数用来设置ThreadLocal的初始值,函数签名如下:
1 | protected T initialValue() { |
该函数在调用get函数的时候会第一次调用,但是如果一开始就调用了set函数,则该函数不会被调用。通常该函数只会被调用一次,除非手动调用了remove函数之后又调用get函数,这种情况下,get函数中还是会调用initialValue函数。该函数是protected类型的,很显然是建议在子类重载该函数的,所以通常该函数都会以匿名内部类的形式被重载,以指定初始值,比如:
1 | public class TestThreadLocal { |
get函数
该函数用来获取与当前线程关联的ThreadLocal的值,函数签名如下:
1 | public T get() |
如果当前线程没有该ThreadLocal的值,则调用initialValue函数获取初始值返回。
set函数
set函数用来设置当前线程的该ThreadLocal的值,函数签名如下:
1 | public void set(T value) |
设置当前线程的ThreadLocal的值为value。
remove函数
remove函数用来将当前线程的ThreadLocal绑定的值删除,函数签名如下:
1 | public void remove() |
在某些情况下需要手动调用该函数,防止内存泄露。
ThreadLocal实现原理
相信大部分人会有这样的想法:
每个ThreadLocal类创建一个Map,然后用线程的ID作为Map的key,实例对象作为Map的value,这样就能达到各个线程的值隔离的效果。
这是最简单的设计方案,JDK最早期的ThreadLocal就是这样设计的。JDK1.3(不确定是否是1.3)之后ThreadLocal的设计换了一种方式。
先看看JDK8的ThreadLocal的get方法的源码:
1 | public T get() { |
其中getMap的源码:
1 | ThreadLocalMap getMap(Thread t) { |
setInitialValue函数的源码:
1 | private T setInitialValue() { |
createMap函数的源码:
1 | void createMap(Thread t, T firstValue) { |
简单解析一下,get方法的流程是这样的:
- 首先获取当前线程
- 根据当前线程获取一个Map
- 如果获取的Map不为空,则在Map中以ThreadLocal的引用作为key来在Map中获取对应的value e,否则转到5
- 如果e不为null,则返回e.value,否则转到5
- Map为空或者e为空,则通过initialValue函数获取初始值value,然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新的Map
然后需要注意的是Thread类中包含一个成员变量:
1 | ThreadLocal.ThreadLocalMap threadLocals = null; |
所以,可以总结一下ThreadLocal的设计思路:
每个Thread维护一个ThreadLocalMap映射表,这个映射表的key是ThreadLocal实例本身,value是真正需要存储的Object。
这个方案刚好与我们开始说的简单的设计方案相反。查阅了一下资料,这样设计的主要有以下几点优势:
- 每个线程中有一个map,而将ThreadLocal实例作为key,这样每个map中的项数很少,而且当线程销毁时相应的东西也一起销毁了
- 把map放到各自线程中带来的好处是 因为各线程访问的map是各自不同的map,所以不需要同步,速度会快些;而如果把所有线程要用的对象都放到一个静态map中的话 多线程并发访问需要进行同步。
更深入理解ThreadLocalMap
先看ThreadLocalMap源码
1 | static class ThreadLocalMap { |
ThreadLocalMap是使用ThreadLocal的弱引用作为Key
下图是本文介绍到的一些对象之间的引用关系图,实线表示强引用,虚线表示弱引用
大家考虑这种情况
ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:
Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
永远无法回收,造成内存泄露。
我们来看一下ThreadLocalMap是如何防止这种情况的
ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施,下面是ThreadLocalMap的getEntry方法的源码:
1 | private Entry getEntry(ThreadLocal<?> key) { |
getEntryAfterMiss函数的源码:
1 | private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { |
expungeStaleEntry函数的源码:
1 | private int expungeStaleEntry(int staleSlot) { |
整理一下ThreadLocalMap的getEntry函数的流程:
- 首先从ThreadLocal的直接索引位置(通过ThreadLocal.threadLocalHashCode & (len-1)运算得到)获取Entry e,如果e不为null并且key相同则返回e;
- 如果e为null或者key不一致则向下一个位置查询,如果下一个位置的key和当前需要查询的key相等,则返回对应的Entry,否则,如果key值为null,则擦除该位置的Entry,否则继续向下一个位置查询
在这个过程中遇到的key为null的Entry都会被擦除,那么Entry内的value也就没有强引用链,自然会被回收。仔细研究代码可以发现,set操作也有类似的思想,将key为null的这些Entry都删除,防止内存泄露。
但是光这样还是不够的,上面的设计思路依赖一个前提条件:
要调用ThreadLocalMap的getEntry函数或者set函数。这当然是不可能任何情况都成立的,所以很多情况下需要使用者手动调用ThreadLocal的remove函数,手动删除不再需要的ThreadLocal,防止内存泄露。所以JDK建议将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露。