目录
ThreadLocal的数据结构
这张图可以看出Thread、ThreadLocal、ThreadLocalMap之间的关系。
ThreadLoacl的弱引用关系图
- 一个Thread仅有一个ThreadLocalMap对象
- 一个Entry对象的Key弱引用指向ThreadLocal对象
- 一个ThreadLocalMap对象存储多个Entry对象
- 一个ThreadLocal对象可以被多个线程共享
- ThreadLocal对象不持有Value,Value由Entry持有
弱引用
如果弱引用指向的对象只存在弱引用这一条线路,则在下一次 YGC 时会被回收。 由于 YGC 时间的不确定性,弱引用何时被回收也具有不确定性。弱引用主要用于指向某个易消失的对象,在强引用断开后,此引用不会劫持对象。调用 WeakReference.get()可能返回null ,要注意空指针异常。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
ThreadLocal重要的方法
get
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);//从当前线程中获取ThreadLocalMap
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);//查询当前ThreadLocal变量实例对应的Entry
if (e != null) {//如果不为null,获取value,返回
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}//如果map为null,即还没有初始化,走初始化方法
return setInitialValue();
}
private T setInitialValue() {
T value = initialValue();//该方法默认返回null,用户可自定义
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)//如果map不为null,把初始化value设置进去
map.set(this, value);
else//如果map为null,则new一个map,并把初始化value设置进去
createMap(t, value);
return value;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];//初始化容量16
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);//设置阈值
}
//阈值设置为容量的*2/3,即负载因子为2/3,超过就进行再哈希
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
总结get步骤:
1)从当前线程中获取ThreadLocalMap,查询当前ThreadLocal变量实例对应的Entry,如果不为null,获取value,返回
2)如果map为null或entry为null,即还没有初始化,走初始化方法
getEntry
getEntry时如果第一个元素不是要获取的元素,那么就采用线性探测法遍历当前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);
}
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;
}
set
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);// 根据哈希码和数组长度求元素放置的位置,即数组下标
//从i开始往后一直遍历到数组最后一个Entry
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//如果key相等,覆盖value
if (k == key) {
e.value = value;
return;
}
//如果key为null,用新key、value覆盖,同时清理历史key=null的陈旧数据
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
//如果超过阀值,就需要再哈希了
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
总结set步骤:
1)根据
哈希码和数组长度求元素放置的位置,即数组下标
2)从第一步得出的下标开始往后遍历,如果key相等,覆盖value,如果key为null,用新key、value覆盖,同时清理历史key=null的陈旧数据
3)如果超过阀值,就需要再哈希:
- 清理一遍陈旧数据
- >= 3/4阀值,就执行扩容,把table扩容2倍==》注意这里3/4阀值就执行扩容,避免迟滞
- 把老数据重新哈希散列进新table
rehash
private void rehash() {
expungeStaleEntries();// 清理一次陈旧数据,这里涉及到ThreadLocal的内存泄漏的解决,在下一篇文章中会写
// 清理完陈旧数据,如果>= 3/4阀值,就执行扩容,避免迟滞
if (size >= threshold - threshold / 4)
resize();
}
/**
* 把table扩容2倍,并把老数据重新哈希散列进新table
*/
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
// 遍历Entry[]数组
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
if (k == null) {// 如果key=null
e.value = null; // 把value也置null,有助于GC回收对象
} else {// 如果key!=null
int h = k.threadLocalHashCode & (newLen - 1);// 计算hash值
while (newTab[h] != null)// 如果这个位置已使用
h = nextIndex(h, newLen);// 线性往后查询,直到找到一个没有使用的位置,h递增
newTab[h] = e;//在第一个空节点上塞入Entry e
count++;// 计数++
}
}
}
setThreshold(newLen);// 设置新的阈值(实际set方法用了2/3的newLen作为阈值)
size = count;// 设置ThreadLocalMap的元素个数
table = newTab;// 把新table赋值给ThreadLocalMap的Entry[] table
}
remove
Entry继承了WeakReferance,所以它的key是弱引用类型,当外部的ThreadLocal被置为null后,key失去了
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);//调用ThreadLocalMap删除变量
}
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();//调用Entry的clear方法
expungeStaleEntry(i);//清除陈旧数据
return;
}
}
}
//看一下clear方法如下:把弱引用的对象置null。
public void clear() {
this.referent = null;
}
threadLocalHashCode:定制的哈希算法
只要是定位元素的地方,都需要调用key.threadLocalHashCode & (len-1)来获得元素所在的entry数组在table中的下标,这个hashcode是ThreadLocalMap定制的哈希算法计算获得的:
元素散列算法如下:
- 求hashCode = i*HASH_INCREMENT+HASH_INCREMENT每次新增一个元素(threadLocal)进Entry[],自增0x61c88647
- 元素散列位置(数组下标)= hashCode & (length-1),
HASH_INCREMENT = 0x61c88647
使用这个方法来检测该算法的优略
private static void hashCode(Integer length){
int hashCode = 0;
for(int i=0;i<length;i++){
hashCode = i*HASH_INCREMENT+HASH_INCREMENT;//每次递增HASH_INCREMENT
System.out.print(hashCode & (length-1));//求散列下标,算法公式
System.out.print(" ");
}
System.out.println();
}
public static void main(String[] args) {
hashCode(16);//初始化16
hashCode(32);//后续2倍扩容
hashCode(64);
}
7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 0 --》Entry[]初始化容量为16时,元素完美散列
7 14 21 28 3 10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25 0--》Entry[]容量扩容2倍=32时,元素完美散列
7 14 21 28 35 42 49 56 63 6 13 20 27 34 41 48 55 62 5 12 19 26 33 40 47 54 61 4 11 18 25 32 39 46 53 60 3 10 17 24 31 38 45 52 59 2 9 16 23 30 37 44 51 58 1 8 15 22 29 36 43 50 57 0 --》Entry[]容量扩容2倍=64时,元素完美散列
根据运行结果,代表此算法在长度为2的N次方的数组上,确实可以完美散列。
ThreadLocal的作用
复杂的线程方法可能需要调用很多方法来实现某个功能,这时候ThreadLocal用于一个线程内,跨类、跨方法传递数据。 如果没有ThreadLocal,那么相互之间的信息传递,势必要靠返回值和参数,这样会造成类之间的耦合。
- hreadLocal不是用来解决线程安全问题的,多线程不共享,不存在竞争,目的是线程本地变量且只能单个线程内维护使用。
- ThreadLocal可以做业务间的参数透传,但是这些业务必须发生在一个线程当中。
- 项目如果使用了线程池,那么小心线程回收后ThreadLocal变量要remove,否则线程池回收后,变量还在内存中,后果不堪设想!(例如Tomcat容器的线程池,可以在拦截器中处理:extends HandlerInterceptorAdapter,然后复写afterCompletion方法,remove掉变量)
ThreadLocal源码开头的注释里写道:ThreadLocal被static修饰时的作用
This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).
/**
* For example, the class below generates unique identifiers local to each thread.
* A thread's id is assigned the first time it invokes {@code ThreadId.get()}
* and remains unchanged on subsequent calls.
*/
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadId {
// Atomic integer containing the next thread ID to be assigned
private static final AtomicInteger nextId = new AtomicInteger(0);
// Thread local variable containing each thread's ID
private static final ThreadLocal<Integer> threadId =
new ThreadLocal<Integer>() {
@Override protected Integer initialValue() {
return nextId.getAndIncrement();
}
};
// Returns the current thread's unique ID, assigning it if necessary
public static int get() {
return threadId.get();
}
}
参考资料:
ThreadLocal终极源码剖析-一篇足矣!:https://www.cnblogs.com/dennyzhangdd/p/7978455.html#_label1
《码出高效》第七章节:ThreadLocal的价值