从ThreadLocal这个类名可以理解为,ThreadLocal表示线程的“本地变量”。即每个线程都拥有该变量副本,达到人手一份的效果,各用各的,这样就可以避免共享资源的竞争。
基本使用public class Test {
static ThreadLocal threadLocal = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
//main线程中的本地变量
threadLocal.set("T-main");
//打印main线程中的本地变量
System.out.println(Thread.currentThread().getName()+":::"+threadLocal.get());
//清除main线程的本地变量
threadLocal.remove();
//再次打印
System.out.println(Thread.currentThread().getName()+":::"+threadLocal.get());
}
}
原理
首先我们得从Thread类说起,在Thread类中维护了两个ThreadLocal.ThreadLocalMap对象,
ThreadLocal.ThreadLocalMap threadLocals = null; ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
这两个对象的初始值为null,调用ThreadLocal的set或get时才会创建他们
ThreadLocal.ThreadLocalMap是ThreadLocal的内部类,其key为弱引用的ThreadLocal对象,value为对应设置的Object对象
static class ThreadLocalMap {
static class Entry extends WeakReference> {
Object value;
Entry(ThreadLocal> k, Object v) {
super(k);
value = v;
}
}
………………
}
当我们set或get或remove ThreadLocal值时,实际调用的都是ThreadLocal.ThreadLocalMap对应的方法,以set操作为例:
public void set(T value) {
//获取当前线程对象t
Thread t = Thread.currentThread();
//获取当前线程的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
if (map != null) {
//当前线程的ThreadLocalMap对象不为null
//设置该ThreadLocal对象的值
map.set(this, value);
} else {
//为null则创建threadlocalMap对象并设置value值
createMap(t, value);
}
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
由此可以得知:ThreadLocal的变量值实际是存放在ThreadLocalMap中的,而ThreadLocal只是ThreadLocalMap的封装
ThreadLocalMap 成员变量// 初始容量默认为16,必须是2的幂 private static final int INITIAL_CAPACITY = 16; // 数据真正存储在table中 private Entry[] table; // 当前table中元素的数量 private int size = 0; // 扩容阈值 private int threshold; // Default to 0Hash算法
ThreadLocalMap 的hash 算法就是使用斐波那契数的倍数 和(len -1) 按位与(这个结果其实就是斐波那契数的倍数 对capacity 取模)的结果作为当前 key 在散列表中的数组下标。
public class ThreadLocal{ private final int threadLocalHashCode = nextHashCode(); private static AtomicInteger nextHashCode = new AtomicInteger(); private static final int HASH_INCREMENT = 0x61c88647; private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); } static class ThreadLocalMap { ThreadLocalMap(ThreadLocal> firstKey, Object firstValue) { //初始化table数组,默认长度为16 table = new Entry[INITIAL_CAPACITY]; //计算位置 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); //创建节点 table[i] = new Entry(firstKey, firstValue); size = 1; //设置扩容阈值 setThreshold(INITIAL_CAPACITY); } } }
private void setThreshold(int len) {
// 扩容阈值为当前数组的2/3
threshold = len * 2 / 3;
}
set()
private void set(ThreadLocal> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 根据ThreadLocal的hashcode确定entry应该存放的位置
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal> k = e.get();
// key相同,则替换
if (k == key) {
e.value = value;
return;
}
// e!=null&&k==null说明key被gc回收了,因为key是弱引用,这里就存在内存泄漏的可能性
if (k == null) {
// replaceStaleEntry方法就是用来解决内存泄漏问题的
replaceStaleEntry(key, value, i);
return;
}
}
//新建Entry并插入
tab[i] = new Entry(key, value);
int sz = ++size;
//
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
replaceStaleEntry()
private void replaceStaleEntry(ThreadLocal> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
// 从staleSlot向前遍历,
// 直到遇到 Entry = null,
// 期间遇到 key = null 时,更新slotToExpunge
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
// 从staleSlot向后遍历
// Entry = null 时停止
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal> k = e.get();
// 遇到 key = key
if (k == key) {
// 更新该位置entry,并将该位置和staleSlot的Entry交换
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// 若此时slotToExpunge == staleSlot,
// 说明向前遍历时没有发现过期元素,向后遍历也没有发现过期元素
if (slotToExpunge == staleSlot)
slotToExpunge = i;
// cleanSomeSlots为启发式清理,expungeStaleEntry为探测式清理
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// 若遇到k == null && slotToExpunge == staleSlot
// 说明向前遍历未遇到过期元素,但向后遍历遇到了过期元素
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// 从staleSlot向后遍历过程中遇到了Entry = null,此时直接将数据更新到staleSlot位置
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// slotToExpunge != staleSlot 说明向前遍历或向后遍历中遇到了过期元素
// 此时slotToExpunge为向前遍历中最远或者向后遍历中遇到的最远的key为null的下标
// 启动清理
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
expungeStaleEntry()
从开始的位置向后遍历,
对key==null的Entry进行清理,
对key!=null的Entry进行rehash后重新在table中定位,
如果hash冲突则向后遍历找到第一个Entry=null的位置存入(开放寻址法)
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 传入的staleSlot位置上的数据一定是过期数据,将staleSlot位置置空
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// 向后遍历,直到Entry = null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal> k = e.get();
// 遇到key==null,则将该位置Entry置空
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
// 若当前位置的key不为null,将其rehash并将key原本的位置置空
// 如果两次hash位置不同
// 再将key的Entry放入rehash后的位置以及其后面位置第一个为null的位置
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
// 返回的i是向后遍历过程中遇到的第一个为null的位置
return i;
}
cleanSomeSlots()
向后遍历 log2N 个位置,下标 i 作为遍历的第一个位置。遍历中遇到位置上 key=null 时(假设该位置为 i ),同步调用 expungeStaleEntry(i) 方法。
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
getEntry()
private Entry getEntry(ThreadLocal> key) {
// 确定元素在数组中的位置
int i = key.threadLocalHashCode & (table.length - 1);
// 根据下标获取元素
Entry e = table[i];
// e==null则表示元素key未被回收,e.get() == key表示没有hash冲突
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;
// 循环查找,直到Entry为null
while (e != null) {
ThreadLocal> k = e.get();
// 找到了,返回
if (k == key)
return e;
// 遇到了过期的key,清除
if (k == null)
expungeStaleEntry(i);
else
// 下一个
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
remove()
private void remove(ThreadLocal> key) {
Entry[] tab = table;
int len = tab.length;
// 计算下标
int i = key.threadLocalHashCode & (len-1);
// 遍历,直到为null
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
// 找到后,将key置空,并清理
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
扩容机制
// rehash()的调用在set方法最后,当table中元素大于阈值threshold时,调用该方法
private void rehash() {
// 全局清理
expungeStaleEntries();
// 清理后size大于等于阈值的3/4,扩容
if (size >= threshold - threshold / 4)
resize();
}
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
Entry e = tab[j];
if (e != null && e.get() == null)
expungeStaleEntry(j);
}
}
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
// 新的数组长度为原来的2倍
Entry[] newTab = new Entry[newLen];
int count = 0;
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal> k = e.get();
if (k == null) {
e.value = null; // Help the GC
} else {
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
setThreshold(newLen);
size = count;
table = newTab;
}
// 阈值为数组长度的2/3
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
为什么会出现内存泄漏
ThreadLocalMap的key为弱引用,value为强引用
GC发生时,该key的ThreadLocal对象被回收
此时该Entry的key为null,value为强引用,也就是说此时value无法通过key访问到并且也不会被回收导致内存泄漏
因此我们使用完ThreadLocal后最好调用remove()方法,但其实在上面的源码中我们也可以看出ThreadLocalMap 的实现中以及考虑到了这种情况,在调用 set()、get()、remove() 方法时,会清理 key 为 null 的记录。
子线程数据共享异步场景下无法给子线程共享父线程的线程副本数据,可以通过 InheritableThreadLocal 类解决这个问题。
它的原理就是子线程是通过在父线程中调用 new Thread() 创建的,在 Thread 的构造方法中调用了 Thread的init 方法,在 init 方法中父线程数据会复制到子线程



