开篇摘要
最近被问及threadlocal的数据结构,一时不知如何描述。虽然平时也用过threadlocal来做变量的线程隔离,然而却没有实际去研究过threadlocal的数据结构。实在惭愧,今天好好看了一下threadlocal的源码,然后写一点心得。
开篇先说明一点,网上很多说法说threadlocal维护的map里面,key用的是线程id,这种说法是不正确的,下面会说明为什么不正确。希望看到帖子的小伙伴也能一起思考下,我的见解是否正确。
源码分析
先上一个示例代码:
public class threadLocalTest {
static class MyThread extends Thread {
// 声明两个threadlocal变量
private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
private static ThreadLocal<String> threadLocal2 = new ThreadLocal<>();
@Override
public void run() {
super.run();
// thread local赋值及取值
threadLocal.set(1);
threadLocal2.set("this is a threadLocal param");
System.out.println(getName() + " threadLocal.get() = " + threadLocal.get());
System.out.println(getName() + " threadLocal2.get() = " + threadLocal2.get());
}
}
public static void main(String[] args) {
// 创建线程
MyThread myThread = new MyThread();
myThread.setName("ThreadA");
// 启动线程
myThread.start();
}
}
结果显而易见,两个变量被打印出来。那么threadlocal的数据结构是怎样的,set和get方法又做了哪些事情呢?
thread 数据接口
public class Thread implements Runnable {
...
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
/* 与此线程有关的ThreadLocal值。 该map由ThreadLocal类维护。 */
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
/* 与此线程有关的InheritableThreadLocal值。 该map由ThreadLocal类维护。 */
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
...
}
在thread类中,声明类两个threadlocal相关的变量,这两个变量就是用来保存和当前线程相关的thread local变量的。thread线程本身不维护这两个变量,而是交由thread local类来维护。
threadlocal数据结构
public class ThreadLocal<T> {
/**
* 根据每个线程生成的一个线性hash值
* 这个hash值会被记录到ThreadLocals(即Thread.threadLocals 和
* inheritableThreadLocals)
* ThreadLocal作为keys,实际是通过threadLocalHashCode来搜索。
* 这是一个自定义的hashcode,只在ThreadLocalMaps中有效。
* 这个hash值能很好的保证同一个线程中的不同threadlocal变量间不产生冲突
*/
private final int threadLocalHashCode = nextHashCode();
/**
* 产生下一个给定的hash值。
* 采用AtomicInteger数据结构,保证每次更新的原子性
* 从0开始
*/
private static AtomicInteger nextHashCode =
new AtomicInteger();
/**
* 生成hash值之间的步长
* 通过这个步长,可以将thread-local IDs转换成可以均匀散列在一个2的指数级大小
* 的table中的hash值
*/
private static final int HASH_INCREMENT = 0x61c88647;
/**
* 返回下一过hash值
*/
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
/**
* 构造一个thread local 变量
* 初始化的值是通过supplier上的get方法来决定的
*
* @param <S> thread local变量的类型
* @param supplier 初始化变量的提供者
* @return 返回一个新的thread local变量
* @throws NullPointerException 如果supplier为null
* @since 1.8
*/
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
return new ThreadLocal.SuppliedThreadLocal<>(supplier);
}
/**
* 无参构造方法
*/
public ThreadLocal() {
}
/**
* 获取当前线程的当前thread local变量的值的拷贝
*
* 如果当前线程没有当前thread local变量的值,那么会调用initialValue方法来
* 初始化变量,并返回初始化后的值
*
* @return 当前线程的当前thread-local变量的值
*/
public T get() {
Thread t = Thread.currentThread();
java.lang.ThreadLocal.ThreadLocalMap map = getMap(t);
if (map != null) {
java.lang.ThreadLocal.ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
/**
* set方法的变体,用来设置初始值
*
* 当用户重写来set方法时,使用该方法
* @return 初始值
*/
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
java.lang.ThreadLocal.ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
/**
* 设置当前线程的当前thread local变量的值
* 大部分的子类没有必要重写这个方法,仅依靠initialValue方法来这是thread locals的值
*
* @param value 需要保存到threadlocal变量中到值
*/
public void set(T value) {
Thread t = Thread.currentThread();
java.lang.ThreadLocal.ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
/**
* 删除当前线程到当前threadlocal变量
*/
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
/**
* 获取指定线程的ThreadLocalMap
* 这个方法在InheritableThreadLocal中被重写
*
* @param t 当前线程
* @return 返回map
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
/**
*
* 创建一个和指定线程相关联的ThreadLocalMap,并给定第一个值
*
* @param t 当前线程
* @param firstValue 初始化entry的值
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
/**
* 创建inherited thread locals map的工厂方法
* 被设计为只能在线程的构造方法中被调用
*
* @param parentMap 关联的父线程
* @return 一个包含父map中变量的map
*/
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}
}
上述代码介绍了ThreadLocal类中的主要数据结构。相关的说明已经在注释中给出,可以参照源码中的英文注释。除了以上的数据结构外,在ThreadLocal中构建了上面一直在用到的ThreadLocalMap。这一块比较复杂,所以分开来描述。
参见代码:
/**
* ThreadLocalMap是一个自定义的hash map,只用来维护thread local值
* 任何操作都不能在ThreadLocal类之外被引用。这个类是包内私有都,以允许在Thread类中
* 声明字段。该hash map使用WeakReferences作为key值,以帮助处理那些大并且生命周期
* 较长的引用。然而,由于不是使用参考队列(reference queues),因此尽在table空间
* 不足的时候才会淘汰果实的entry。
*/
static class ThreadLocalMap {
/**
* 这个map的内容条目扩展了WeakReference,并使用其主要的字段作为key
* 这个key其实对应着threadlocal对象
*
* 值得注意的是,如果key值为null(例如entry.get() == null)意味着这个key
* 已经不再被引用,且这个条目可以从table中清除。这些条目在以下代码中称为过时
* 条目(stale entries)
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/**
* 和这个ThreadLocal关联的value值
*/
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/**
* 初始容量,必须是2的指数倍
*/
private static final int INITIAL_CAPACITY = 16;
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
/**
* 用来存储数据的数组。是一个可扩展的数据
* 数组的长度必须为2的指数倍
*/
private Entry[] table;
/**
* table中的条目数量
*/
private int size = 0;
/**
* 触发扩展(resize)的最大容量,默认为0
*/
private int threshold; // Default to 0
/**
* 设置threshold,默认为长度的2/3,向下取整
* 即负载系数为2/3
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
/**
* 递增对len取模后的值i
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
/**
* 递减对len取模后的值i
*/
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
/**
* 带参数对构造方法
*
* @param firstKey
* @param firstValue
*/
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,通过传入一个父map
* 新map中包含父map中的所有条目
*
* @param parentMap 父线程关联的map
*/
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
/**
* 获取key关联的entry,这个方法本身只能处理快速路径:直接命中存在的key
* (未命中的时候,调用getEntryAfterMiss方法,查找没有命中key的条目)
* 使用快速路径,可以提高查询性能,且易于操作
* @param key
* @return
*/
private Entry getEntry(ThreadLocal<?> key) {
// 通过hashcode和table长度计算存储位置
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);
}
/**
* 当直接计算的位置上没有找到对应的key值,则使用这个方法继续查询
*
* @param key the thread local object
* @param i the table index for key's hash code
* @param e the entry at table[i]
* @return the entry associated with key, or null if no such
*/
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
/**
* 设置和key关联的value
*
* @param key the thread local object
* @param value the value to be set
*/
private void set(ThreadLocal<?> key, Object value) {
// 在这里没有项get方法一样使用快速路径。原因是通过set方法新建一个条目
// 和通过set方法替换一个条目一样普遍。在这种情况下,快速路径失败的可能
// 性更高
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();
}
/**
* 按key删除条目entry
*/
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;
}
}
}
}
上述代码说明了ThreadLocalMap的主要的数据结构和方法。如注释所说,这个map是一个自定义的hashMap,其key并不是直接维护在map中,而是维护在Entry扩展的WeakReference中。在map中维护了一个tables数组。与其说,ThreadLocalMap是一个map,倒不如说,是一个map组成的数组。ThreadLocalMap是线程里threadLoclas变量的数据结构,一个线程维护一个ThreadLocalMap供线程共享,ThreadLocalMap中维护了一个数组tables,这个tables中存储的是Entry,简单的说,Entry就是一个保存了以ThreadLocal变量的hashCode作为key,ThreadLocal的实例作为value的健值对。
这基本上就是ThreadLocal的数据结构了。下面通过对上述的示例进行断点调试,一起来看看运行的过程中,数据怎么保存在ThreadLocal中的。
断点调试
首先,创建TreadLocal变量,创建时,会调用初始化方法,初始化hashCode。
然后调用set方法为ThreadLocal设置一个初始值(value为null)。
设置完两个变量后,在get方法中,我们可以看到在当前线程中被设置好的ThreadLocal变量threadLocals
threadLocals的tables中,包含一个位于第1位上的entry,其hashCode和value值就是上述设置的值。还有一个位于第10位上的entry。在get函数的getMap方法中,获取的就是t.threadLocals。
在getEntry方法中,就是通过this的hashCode去匹配tables中的hashCode,然后获取到对应的Entry。
然后返回entry中的value值。
回顾开篇
回到开篇说的问题,ThreadLocalMap中的key,是线程ID吗?答案是否定的。理由有以下几点:
1,ThreadLocal是存放在线程对象里的,是一个线程隔离的变量,而对于在同一个线程内的ThreadLocal使用线程ID作为key,区分不了变量。因为一个线程内,线程id是相同的。
2,通过查看源码,我们可以知道,ThreadLocalMap的key,其实是ThreadLocal变量的hash值。在同一个线程中,每一个ThreadLocal变量的hashCode是不同的,可以起到区分ThreadLocal的目的。
3,ThreadLocalHashCode还有一个用途,就是计算ThreadLocal变量在tables中的位置。方法是
i = this.threadLocalHashCode & (tables.len - 1)
计算出来的i就是这个threadLocal在tables中的位置。需要注意的是,如果这个位置已经有Entry占用里,set方法会继续寻找下一个位置
重新看一下set方法
private void set(ThreadLocal<?> key, Object value) {
// 获取相关变量
Entry[] tab = table;
int len = tab.length;
// 计算当前threadLocal变量的位置
int i = key.threadLocalHashCode & (len-1);
// 如果当前位置不为null
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
// 获取当前位置的threadLocal值
ThreadLocal<?> k = e.get();
// 如果存放的就是我要设置的ThreadLocal,则更新当前值
if (k == key) {
e.value = value;
return;
}
// 如果当前的值已经变为null,说明已经过时了
if (k == null) {
// 用要设置的ThreadLocal去替换当前位置上
// 已经过时的ThreadLocal
replaceStaleEntry(key, value, i);
return;
}
}
// for循环中都不满足,且通过nextIndex方法找到下一个为null的位置
// 然后插入Entry
tab[i] = new Entry(key, value);
int sz = ++size;
// 标记过时的entry,并且判断tables大小是否已经达到了负载因子。
if (!cleanSomeSlots(i, sz) && sz >= threshold)
// 如果达到了,则进行rehash
rehash();
}
下面是rehash的代码
private void rehash() {
// 先清除过时的entries
expungeStaleEntries();
// size是否大于负载因子的3/4
if (size >= threshold - threshold / 4)
// 如果大于,则扩展tablse
resize();
}
这里没有直接使用负载因子,而是使用负载因子的3/4来判定,是为了降低程序出错的几率。在resize之前,如果大量的 threadLocal插入,导致tables满载。通过set方法的代码可以看到,tables满载的情况下,代码会进入不断寻找为null的Entry的死循环。所以采用3/4来提前触发resize
这一部分的设计,和GC收集器中的CMS收集器的负载因子设计类似。CMS收集器如果负载因子设置不合理,也会抛出异常。
这是我通过读代码得出的结论。如果有我看漏的地方来控制这块逻辑,还请大佬们指正。