引言
一个例子
在讲解原理之前,我们先来看一个ThreadLocal的使用示例:
public class ThreadLocalTest {
private static final ThreadLocal<String> STRING_THREAD_LOCAL = new ThreadLocal<>();
private static final ThreadLocal<Integer> INTEGER_THREAD_LOCAL = new ThreadLocal<>();
private static void run(){
Thread thread = Thread.currentThread();
STRING_THREAD_LOCAL.set(thread.getName());
INTEGER_THREAD_LOCAL.set(Integer.valueOf(thread.getName().replace("thread","")));
System.out.println(thread.getName()+" string is " + STRING_THREAD_LOCAL.get());
System.out.println(thread.getName()+" integer is " + INTEGER_THREAD_LOCAL.get());
}
public static void main(String[] args) {
Thread thread1 = new Thread(ThreadLocalTest::run,"thread1");
Thread thread2 = new Thread(ThreadLocalTest::run,"thread2");
thread1.start();
thread2.start();
}
}
这个示例中,有两个ThreadLocal类的变量,一个存储的是String类型,一个存储的是Integer类型,然后两个线程thread1和thread2分别在这两个ThreadLocal中存储了对应的值,最后两个线程分别将各自的String和Integer值输出:
thread1 string is thread1
thread2 string is thread2
thread2 integer is 2
thread1 integer is 1
可以看到ThreadLocal确实起到了线程本地变量的作用。
原理分析
从set方法看ThreadLocalMap
我们需要重点理解ThreadLocal是如何实现为每个线程保存不同的变量这一关键点。我们先看它的set方法:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
首先,它拿到了当前线程,然后它调用了getMap方法,该方法的参数是当前线程,返回值是一个ThreadLocalMap对象,这个对象是ThreadLocal的内部类,也是理解ThreadLocal的重要一环。我们看getMap方法:
Thread与ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
逻辑很简单,它直接返回了当前线程的threadLocals字段。我们到Thread类中,能找到这个ThreadLocalMap类的变量的声明:
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
也就是说,每个线程对象是有一个对应的ThreadLocalMap的。那这个ThreadLocalMap对线程来说,有什么意义呢?它跟ThreadLocal的关系是什么?我们回到set方法中继续看:
在else的逻辑中,如果当前线程的ThreadLocalMap不存在,它就会调用createMap方法:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
这个方法也很简单,它就是实例化了一个新的ThreadLocalMap对象,然后赋值给了当前线程的threadLocals变量。我们重点看ThreadLocalMap初始化时的参数,第一个是当前ThreadLocal对象,第二个是我们调用set方法的参数,也就是要设置的值。到这里我们就能大胆地猜测一下,ThreadLocal本身并没有存储每个线程的值,它是将每个线程的值存储在该线程的ThreadLocalMap中,除了存储值,还存储了自身(ThreadLocal)的引用。试想一下,两个线程thread1和thread2同时调用set方法,createMap会将这两个线程要设置的值分别保存在它俩的ThreadLocalMap中。
ThreadLocalMap与ThreadLocal
那么ThreadLocalMap是怎样将ThreadLocal和值对应起来的呢?我们来看它的构造方法:
/**
* Construct a new map initially containing (firstKey, firstValue).
* ThreadLocalMaps are constructed lazily, so we only create
* one when we have at least one entry to put in it.
*/
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);
}
这里面用到了内部类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实际上就是ThreadLocal和值的映射,它的构造方法中的第一个参数就是ThreadLocal,第二个参数是该ThreadLocal对应的值,也就是我们上面说的set方法的参数。
然后在ThreadLocalMap的构造方法中,首先创建了一个Entry的数组,为什么需要Entry的数组呢?因为一个线程对应一个ThreadLocalMap,但是一个ThreadLocalMap中需要保存多个ThreadLocal和对应的值,所以需要一个数组。有了数组之后,就需要知道当前的这个ThreadLocal放在Entry数组的第几个。这里用的是ThreadLocal的HashCode,每个ThreadLocal对象的threadLocalHashCode值不同并且是有规律的,这里不去详细介绍了。找到对应位置i之后,就会设置该位置上的Entry。之后还会有扩容等操作,这里也不详细介绍了。
经过上面的步骤之后,一个Thread对应的ThreadLocalMap就初始化成功了。
get方法
我们接着来看get方法:
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();
}
首先拿到当前线程,然后拿到当前线程的ThreadLocalMap,如果该ThreadLocalMap已经存在(被初始化过),就调用ThreadLocalMap的getEntry方法获取当前ThreadLocal对应的Entry:
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
确定Entry的方式还是通过ThreadLocal的threadLocalHashCode值。
然后Entry的value就是我们要查找的值。
删除线程本地变量
某个线程要想删除它在某个ThreadLocal中对应的值,可以通过调用remove方法来实现:
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
它首先拿到当前线程的ThreadLocalMap,然后执行ThreadLocalMap的remove方法来删除对应的ThreadLocal:
private void remove(ThreadLocal<?> key) {
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)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
小结
ThreadLocal实现了线程本地变量,通过将变量封闭在线程调用栈内,它也能避免很多并发的数据安全问题。ThreadLocal的实现依赖于ThreadLocalMap,而ThreadLocalMap与Thread是一一对应关系。当然,ThreadLocalMap里面很多逻辑还涉及到扩容和rehash。这里没有重点讲述。