ThreadLocal的作用和原理
ThreadLocal可以称为线程本地变量或线程本地存储,跟方法内作用域的变量一样,都是本线程私有的。可以用来在一个线程调用多个方法的过程中、用来传递参数,省去通过方法入参传递的麻烦。slfj的MDC,多数据源,以及弱引用WeakReference等等场景中都可以看到ThreadLocal的应用。
线程本地存储:在线程生命周期内作为上下文存储共享对象
这里的上下文指的是线程存活期间内,调用多个方法,各个方法之间共享的“上下文空间”。
我们知道,每个线程对应着它的线程栈,线程栈由栈帧组成,用这套数据结构来跟踪线程的方法调用。
每个栈帧里边存放着一个方法内的局部变量,进入一个方法则压入一个栈帧,从一个方法返回则弹出一个栈帧。
考虑一个问题:如果想在一个thread生命周期内,在多个栈帧或者说多个方法之间共享对象呢?
用局部变量显然不行,其作用域只在方法里或者栈帧内,每个栈帧维护自己的局部变量表,另一个栈帧不认识。thread里边弄个静态变量当然可以,但是这是类级别的、就对别的thread实例可见,要考虑并发问题了。
想来想去,在Thread类的内部的成员变量中搞个Map来存放这些值是个不错的主意:作用域是每个thread实例,能够被线程生命周期内各个方法调用所共享。我想这就是ThradLocalMap和ThreadLocal的由来。
ThreadLocal的使用方法
先看例子程序:
使用ThreadLocal在线程的多个方法调用之间共享参数
public class WorkerThread implements Runnable{
public static ThreadLocal<Map> paramA = new ThreadLocal<>();
private CountDownLatch latch;
public WorkerThread(CountDownLatch latch) {
this.latch = latch;
}
@Override
public void run() {
ThreadLocalTest tlt = new ThreadLocalTest();
tlt.initParamA();
tlt.useParamA();
latch.countDown();
}
}
public class ThreadLocalTest {
private static Logger logger = LoggerFactory.getLogger(ThreadLocalTest.class);
private static int N = 100;
public static void main(String[] args) {
CountDownLatch latch = new CountDownLatch(N);
for(int i=0; i<N; i++) {
new Thread(new WorkerThread(latch)).start();
}
try {
latch.await(); //等所有WorkerThread线程都执行完
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void initParamA() {
HashMap<String,String> map = new HashMap<>();
map.put("name", Thread.currentThread().getName());
WorkerThread.paramA.set(map);
}
public void useParamA() {
String name = (String)WorkerThread.paramA.get().get("name");
String threadName = Thread.currentThread().getName();
logger.info("当前线程名{},通过 ThreadLocal传递的线程名{}", threadName, name);
if(!threadName.equals(name))
logger.error("出现并发问题");
}
}
这个程序的意图是这样的:worker线程会去调用initParamA和useParamA两个方法,使用ThreadLocal在两者之间传递一个参数,这里传递的是一个Map,里边放了当前worker线程的线程名。最后会通过比较Thread.currentThread().getName()与ThreadLocal里的线程名是否相等来证明ThreadLocal的线程私有性。
运行结果是不会打印出"出现并发问题"。
源代码分析
ThreadLocal
我们的ThreadLocal是找了一个类声明了一个静态成员变量
public static ThreadLocal<Map> paramA = new ThreadLocal<>();
然后分别是在不同的方法里调用了set()和get()方法来放和取我们的参数Map。
我们来看一下源码分析一下:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
由以上两个方法源码可以知道,实际上ThreadLocal的set和get是把自己ThreadLocal对象作为key,和我们的参数作为value,组成k-v对,放在当前Thread的ThreadLocalMap这个Map里的。每个线程都有自己的ThreadLocalMap,这是定义在Thread.java里的。
ThreadLocalMap
ThreadLocal<Map> paramA = new ThreadLocal<>();
实例化了一个ThreadLocal,paramA,当调用paramA.set(Map)时,这个Map最终存放在当前线程的ThreadLocalMap里。ThreadLocalMap是每个线程Thread实例内部都有的一个存储结构,里边实际上是个Entry数组,每个Entry由ThreadLocal和Map这样一个k-v对来实例化。
也就是说ThreadLocal和Map这样一个k-v在ThreadLocalMap中存放时,是封装成Entry存放的,而Entry是存放在ThreadLocalMap的private Entry[] table这个数组中的,存放时先根据key的hash找到对应的Entry数组下标,然后找到对应的Entry,如果Entry的key等于当前这个ThreadLocal,那么就用我们的Map替换Entry的value,否则直接new Entry然后放到Entry数组的下标位置。
ThreadLocalMap的set方法:
/**
* Set the value associated with key.
*
* @param key the thread local object
* @param value the value to be set
*/
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
如何保证通过ThreadLocal在线程生命周期内共享的对象的正确回收
通过ThreadLocal和ThreadLocalMap机制在线程生命周期内共享对象,引出了另一个问题:我们用来在两个栈帧之间共享的这个对象,在这两个栈帧弹出之后,从使用者角度来说理论上这个变量就没用了,应该被gc了。且这时候这两个栈帧弹出,那两个方法中的局部变量与这个对象之间的强引用关系也不存在了,这对象可以回收了吗?
仅仅是这样显然还不行,如果这个时候线程还没执行完,那这个线程的成员变量ThreadLocalMap也还在堆里边呢,共享的对象也就是我们的Map就以Entry的形式存放在ThreadLocalMap里,也就是说ThreadLocalMap的Entry与我们的共享对象存在着引用关系。那怎么才能正确的释放我们的Map对象呢?
看一下Entry的定义:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
-
首先Entry继承了WeakReference,其中key也就是ThreadLocal对象本身用WeakReference包装起来了,见代码中的super(k)。这样一来如果是在方法中定义了ThreadLocal local = new ThreadLocal(),那么当方法返回,栈帧弹出,ThreadLocal对象失去了引用,而其与Entry之间的引用又是弱引用,所以它是可以被gc顺利回收的。
-
其次,我们需要注意,Entry构造方法里边只是把key作为弱引用包装起来了,而value只是个一般的成员变量类型。这样的话,当发生gc的时候,由于Entry的key也就是ThreadLocal是被WeakReference包装起来的,所以它会被垃圾回收。但如果Entry没有失去gc root引用,那这个value不会主动释放的。也就是线程没有执行完,Entry不会失去引用,我们的共享对象也不会失去引用,这个引用还是个强引用,所以我们的对象不会被垃圾回收而自动释放!
这一点要特别注意,如果我们的线程生命周期要很长,比如是在一个线程池里被复用的线程,那么其对应的ThreadLocalMap还有里边Entry都会存活不会回收。需要当我们确认通过ThreadLocal存储的对象不再使用的时候,最好手工调用remove()方法来清理Entry。
-
最后,一旦线程生命周期结束,线程栈销毁,线程方法内局部变量和ThreadLocalMap里的Entry对ThreadLocal存储对象的引用就都消失了,这个对象将会被垃圾回收。