ThreadLocal
ThreadLocal是多线程并发质性过程中很重要的一个对象,它往往会保存一些在单个线程内不同函数之间共享的一些变量。
如,在网络请求中,我们可以把一次请求调用视作单线程,接着我们在线程内用一些函数获取用户的信息,将用户的信息贮存在ThreadLocal当中,之后其他函数需要,就直接从其中获取。
这就有一点容器的思想了。Spring中也会通过一个context保存各种Bean,如果你希望获取这个context,你只需要实现相应的aware接口,接着Spring在初始化的时候就会检测所有的Bean,拥有这个接口的Bean,Spring就会把context传给你,你可以使用它进行一些操作,也可以干脆持久化一点,把它当做自己的变量,持久化引用它。
ThreadLocal是如何为每一个线程保存一个独立的本地值呢?
我们可以把它理解成一个Map,每一个线程都拥有一个这样的Map,它的key是一个ThreadLocal实例。
换言之,我们可以创建多个ThreadLocal实例,每一个ThreadLocal实例都相当于一个key,
伪代码
local = new ThreadLocal() ---- map.put(local, null)
local.set(value) ---map.put(local, value)
local.get(value) ---map.get(local, value)
local.remove() ---map.remove(local)
早起的ThreadLocal则是另外一种情况,Map的拥有者是一个ThreadLocal对象,Map的键则是一个Thread实例。
新版本的优势在于:
多线程的情况下,新版本的Map的体积依旧比较小。
随着Thread的销毁,Map也会随之销毁,减少了内存的消耗。
所以ThreadLocal不需要传递key,因为它自己就是一个key。
源码分析:
public class ThreadLocal<T> {
public ThreadLocal() {
}
public void set(T value) {
set(Thread.currentThread(), value);
if (TRACE_VTHREAD_LOCALS && Thread.currentThread().isVirtual()) {
printStackTrace();
}
}
private void set(Thread t, T value) {
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
从set方法中就可以看出来,ThreadLocal类为当前Thread创建了一个Map。
我们深入这个Map的源码:
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
private Entry[] table;
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
// 这里我们重点关注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);`获得当前key的初始位置,接着从这个位置遍历map,也就是一个Entry数组。
//为什么`i`的求法会这样写,这里不了解的同学可以参考HashMap的求法,
//因为这里的len必然是2的幂,所以`&(len-1)`相当于取模运算,在底层使用位运算效率更高。
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
//接着,从这个位置遍历table,为什么使用`nextIndex(i, len)`,因为数组是从中间开始遍历的,可能需要回到开头。
e = tab[i = nextIndex(i, len)]) {
//找到了key,就可以篡位了。
if (e.refersTo(key)) {
e.value = value;
return;
}
//找到了空位,其实这里是一个异常的槽位,之后讲。
if (e.refersTo(null)) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
这里我们重点关注set方法。
这里通过int i = key.threadLocalHashCode & (len-1);获得当前key的初始位置,接着从这个位置遍历map,也就是一个Entry数组。
为什么i的求法会这样写,这里不了解的同学可以参考HashMap的求法,因为这里的len必然是2的幂,所以&(len-1)相当于取模运算,在底层使用位运算效率更高。
接着,从这个位置遍历table,为什么使用nextIndex(i, len),因为数组是从中间开始遍历的,可能需要回到开头。
为什么Entry需要extends WeakReference<ThreadLocal<?>>,换言之,为什么它的key需要一个弱引用?
public void funA(){
ThreadLocal local = new ThreadLocal();
local.set(110);
local.get();
local = null;
}
这里函数执行结束之后,我们的ThreadLocal依旧被内部类的Entry 的key引用着。
使用弱引用的时候,仅被弱引用引用的对象在GC发生的时候,无论内容是否够用,都会被回收。相应的,对应的key就会变成null,也就有了上面的那个情况,
Entry e != null && e.refersTo(null)
尽管如此,这只是治标不治本,Entry的Value依旧存在着。
所以,一般规范都要求,要记得自己手动在不需要的时候清除ThreadLocal。
推荐使用static final修饰ThreadLocal对象。因为ThreadLocal本来就是为了共享而存在的。不过,这样就出现了一个问题,Entry的弱引用形同宿舍,因为只要这个这个类存在,它就会始终指向这个ThreadLocal实例,换言之,除非线程结束,或者主动释放,否则这个ThreadLocal就会永远存在。
当然,也推荐使用private封装,需要的时候调用公开的方法就好了,这样更方便定制。
如果希望在ThreadLocal只是存在更复杂的数据,可以存入一个map,或者设置更多的ThreadLocal对象,其实都差不多,看自己的使用场景。
使用建议:
可以定制线程池的afterExecute方法,
综上,使用private static final,让一个类持有一个ThreadLocal对象,但是每一个Thread都可以在这个对象中拥有自己的一个访问空间。