ThreadLocal与ThreadLocalMap与Thread
ThreadLocal是一个线程本地变量的引用,它就是一个普通的对象,只是它的内部实现比较特别。为什么叫这个名字,这与其实现有关。
ThreadLocalMap是ThreadLocal的静态内部类,同时是一个对象池的角色,看其命名以关键词”Map“结尾也可以知道,其持有多个ThreadLocal对象的”映射引用“,具体后面再讲。
Thread代表的就是一个Java中的线程对象,在编码、运行时代表的就是一个线程。对线程的操作可以通过一个Thread对象进行,Thread持有一个ThreadLocal.ThreadLocalMap类型的变量,通过此操作让Thread持有了ThreadLocalMap对象的引用。
三者的关系如下图,Thread内部持有一个ThreadLocalMap,ThreadLocalMap中保存有当前线程所有的的ThreadLocal对象。
透过现象看本质
使用ThreadLocal的时候,最常用的就是三个方法set(),get(),remove(),而且使用的顺序一般也是这三步。 看上面的图,Thread持有ThreadLocalMap,ThreadLocalMap保存了ThreadLocal,怎么通过ThreadLocal#set()方法就将值保存到了线程私有的ThreadLocalMap中去了?而且使用的时候直接ThreadLocal#get()方法就能拿到值?
set()方法
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);
}
在set方法中直接通过Thread的静态方法currentThread()获得了当前的Thread对象引用。currentThread()方法被native修饰,具体实现不在此探究。
然后使用getMap()方法,传入了当前的Thread对象引用,获得了ThreadLocalMap对象。getMap()方法的源码如下,其实现更简单,直接返回了Thread对象所持有的threadLocalMap对象。
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
获得ThreadLocalMap对象后,先判断其是否为空,若为空则创建一个新的ThreadLocalMap对象出来,并将当前ThreadLocal的set()方法传入的对象,放入到新创建的ThreadLocalMap中;若已经存在,则直接将并将当前ThreadLocal的set()方法传入的对象,放入到ThreadLocalMap中。
这里为什么会有一个判空的操作?
在每一个Thread内部都维护有一个ThreadLocakMap对象,用于保存当前线程独占的变量(ThreadLocal)与其值。ThreadLocalMap在默认情况下是一个空对象,在第一次向ThreadLocal对象中set值的时候才会初始化,这样设计的原因是:ThreadLocal是一个低频功能,绝大部分情况下是不需要使用的,这样设计可以节省内存空间。
createMap(t, value)方法
createMap方法,从其方法的命名就能看出是用来创建Map映射对象的,这里创建的就是ThreadLocalMap。 继续探究createMap(t, value)方法,向下一路追踪代码而去,会找到ThreadLocalMap对象创建的代码如下
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);
}
ThreadLocalMap的构造函数有两个参数,一个是ThreadLocal>类型的firstKey,一个是Object类型的firstValue。从这两个变量的命名来看,一个Key、一个Value,符合Map映射的K-V形式。
从代码的逻辑实现上可以看出来,ThreadLocalMap实际持有的是一个Entry类型的数组,初始容量INITIAL_CAPACITY=16。ThreadLocalMap的本质就是一个Entry数组,Entry数组中的元素持有的就是ThreadLocal变量的值。这整个Entry数组就是一个线程独占的ThreadLocalMap。既然是一个数组,为什么命名的时候要叫一个XXXMap呢?
为什么会叫“ThreadLocakMap“这个以Map关键字结尾的变量名?
或者:ThreadLocalMap为什么被命名为一个Map,映射是谁映射的谁?
从宏观来看,ThreadLocalMap维护的就是一个以ThreadLocal变量为Key,以具体值为Value的映射关系对象。
ThreadLocakMap是对Entry数组的封装,封装后的表现就是以Entry数组的下标映射到的具体的值的行为,所以以关键词”Map“作为变量名的后缀。既然叫“Map”,则可以看出其设计初衷为实现一个“key-Value”形式的存储。在实际的实现中,Value好说,就是ThreadLocal中set进去的值对象。那么Key呢?ThreadLocalMap的本质是一个Entry数组,所以可以看出此处Key的本质就是Entry数组的下标,对应Java中就是一个int类型的值。但在实际的使用中不可能直接指定这个“下标”的值,所以在ThreadLocalMap中已经封装了这个“下标”的生成方法。该生成方法的启发因子就是ThreadLocal对象的hashCode,从宏观来看,ThreadLocalMap维护的就是一个以ThreadLocal变量为Key,以具体值为Value的映射关系对象。所以,在实现的编码使用中,直接使用ThreadLocal的get方法就能取到之前set进去的值。
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();
}
与set()方法一样,在set方法中直接通过Thread的静态方法currentThread()获得了当前的Thread对象引用。
与set()方法一样,使用getMap()方法,传入了当前的Thread对象引用,获得了ThreadLocalMap对象。
判断得到的ThreadLocalMap(即当前线程的ThreadLocalMap)是否为null,如果是null,则直接使用setInitialValue()返回给当前的ThreadLocal变量初始化一个默认的值并放入当前线程的ThreadLocalMap中,并将默认值返回给调用方。这个默认值就是一个“null”。
如果当前线程的ThreadLocalMap不是空的,则使用getEntry方法,通过当前的ThreadLocal变量(即ThreadLocalMap中的Key)获得对应的值(即ThreadLocalMap中的Value)。 若拿到的值不是null则进行一次类型转换,最后返回给调用方。如果拿到的值是null,则初始化一个默认的“null”返回给调用方。
一个ThreadLocal到底可以持有几个对象的引用
由此可以看出,在一个线程中可以持有多个ThreadLocal对象,但是每个ThreadLocal对象只能持有一个变量值。线程所持有的所有的ThreadLocal及其值都保存在Thread私有的ThreadLocalMap映射对象中。
现在再看这个问题,感觉自己挺傻的。😆😆😆😆😆这确实是当年我脑海中蹦出来的问题。也不知道当时脑子在想啥,就是没转过这个弯儿来~~~
为什么直接通过ThreadLocal就能操作Thread的线程私有对象映射迟ThreadLocalMap呢?
通过以上的分析,ThreadLocal、ThreadLocalMap、Thread三者之间的关系,可以更新为下图:
在ThreadLocal的set与get方法中通过Thread的静态本地方法currentThread获得当前的Thread对象引用,从而操作Thread持有的ThreadLocalMap对象池。