Threadlocal
Threadlocal简介
Threadlocal是通过在每个线程维护自己的变量来实现线程隔离,所有它一般会用于不需要线程共享时解决线程安全问题
Threadlocal源码解读
Threadlocal的get()方法
源码
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 拿到当前线程的threadLocals实例
ThreadLocalMap map = getMap(t);
// 判断threadLocals是否初始化
if (map != null) {
// 通过this当前ThreadLocal对象实例调用ThreadLocalMap的getEntry()取出结果(后续文章详解)
ThreadLocalMap.Entry e = map.getEntry(this);
// 取得出值则返回
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// threadLocals未初始化或threadLocals通过this取出的值为null,则进行初始化
return setInitialValue();
}
// 初始化方法
private T setInitialValue() {
// 获取initialValue()的值(初始值),创建ThreadLocal实例时可以通过重写的方式赋上初始值
T value = initialValue();
// 获取当前线程
Thread t = Thread.currentThread();
// 获取线程的threadLocals
ThreadLocalMap map = getMap(t);
// 如果threadLocals尚未初始化,则执行createMap,否者直接设置值
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
if (this instanceof TerminatingThreadLocal) {
TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
}
// 最后返回从initialValue()获取的初始值
return value;
}
// 创建(初始化)ThreadLocalMap,并通过firstValue设置初始值
void createMap(Thread t, T firstValue) {
// 这里涉及到ThreadLocalMap后续文章详解
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
流程图
总结
- Threadlocal实际是通过ThreadlocalMap里的一个数组容器进行存储的,重点也都到里面
ThreadlocalMap部分源码解析
基本属性
源码
//继承WeakReference实现对k的弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//默认初始容器大小
private static final intINITIAL_CAPACITY= 16;
//数组容器
private Entry[] table;
//容器中数据的数量
private int size = 0;
//扩容的临界值,默认为容器大小的2/3
private int threshold;
ThreadLocal的内存泄露问题
- 每个Thread对象会引用一个ThreadlocalMap对象,ThreadLocal会引用Entry对象,而Entry对象中的key会引用Threadlocal对象,若是强引用,在栈中的Threadlocal对象引用销毁时,key还是会引用堆中Threadlocal对象,GC不会回收Threadlocal对象,容易造成内存泄漏。而如果是弱引用就会被回收。一定要记得手动使用
remove()
方法!!!
初始化
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
//利用hash值定位数组坐标
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
//获取hash值
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);
}
set()方法
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 计算key的下标
int i = key.threadLocalHashCode & (len-1);
// 开放寻址方式向后遍历
for (Entry e = tab[i];
e != null; // 直到找到Entry为null
e = tab[i = nextIndex(i, len)]) { // nextIndex环状向后遍历(不存在越界问题,到末尾为设置为开头)
// 获取当前Entry的key
ThreadLocal<?> k = e.get();
// 情况一:当前key和需要设置的key相同,直接更新值
if (k == key) {
e.value = value;
return;
}
// 情况二:遇到过期的key,进行替换过期数据操作
if (k == null) {
// 替换过期数据,底层调用了启发式清理和探测式清理
replaceStaleEntry(key, value, i);
return;
}
}
// 情况三:找到空的Entry,直接加入并更新size
tab[i] = new Entry(key, value);
int sz = ++size;
// 最后调用一次启发式清理方法,清理过期的Key。清理完成后如果size还是超过了扩容阈值则进行rehash操作
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
流程图
get() and remove()
- get()定位坐标与set()类似,特殊点在key==null时执行探测式清理
- remove()定位坐标与set()类似,在清除后执行探测式清理
探测式
源码
// 探测式清理 staleSlot开始清理下标
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 1. 清理当前位置的Entry同时更新size
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
Entry e;
int i;
// 2. 从开始位置向后遍历Entry,直到遇到Entry为null时停止
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
// 获取当前的key
ThreadLocal<?> k = e.get();
// 判断是否为过期数据
if (k == null) {
// 是过期数据则直接清理当前Entry,同时更新size
e.value = null;
tab[i] = null;
size--;
} else {
// 不是过期数据则再次获取Entry对于的下标
int h = k.threadLocalHashCode & (len - 1);
// 判断获得的下标是否是当前位置相同,不相同则重新定位
if (h != i) {
// 先置空当前位置
tab[i] = null;
// 从获取的下标处开始向后开放寻址找到能存放的下标
while (tab[h] != null)
h = nextIndex(h, len);
// 最后存储进空的Entry中
tab[h] = e;
}
}
}
return i;
}
流程图
总结
- 简单来说他的过程,就是从当前位置开,清除过期数据,对未过期的数据重新进行位置计算,直到遍历到空entry为止
启发式清理
源码
// 启发式清理 (i表示开始下标,n表示数组长度)
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
// 从开始下标位置向后循环遍历,只会循环 数组长度的对数(比如长度为16则循环4次)
do {
i = nextIndex(i, len);
Entry e = tab[i];
// 判断当Entry不为空且Entry又为过期数据时
if (e != null && e.get() == null) {
n = len;
removed = true;
// 从当前下标启动探测式清理
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0); // 每次将n >>> 1 (即 n / 2)
return removed;
}
流程图
总结
- 当添加新元素
set()
或删除另一个过时元素时,将调用此函数。它执行对数扫描次数作为不扫描(保留过期数据)
和与元素数量成比例的扫描次数
之间的平衡,使其能够清除过期数据。
扩容
源码
private void rehash() {
// 通过从头循环的方式判断是否有过期数据,有则执行探测式清理
expungeStaleEntries();
// 如果清理后的Entry数量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);
}
}
总结
rehash()
触发条件:set()
执行最后当启发式清理完毕后Entry的数量size
>=扩容阈值threshold(Entry数组的长度的2/3)
时触发resize()
触发条件:rehash()
执行并且expungeStaleEntries()
底层调用的探测式清理完毕后Entry的数量size
>=扩容阈值的3/4
时触发