ThreadLocal源码分析
文章目录
一,基本使用
Threadlocal的作用就是,为一个线程保存一个线程本地变量,该变量对该线程全局可知,其他线程无法干扰到该变量
简单使用:
public static void main(String[] args) throws InterruptedException {
//threadLocal简单使用,只有三个API
ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
//set
threadLocal.set(20);
//get
int i = threadLocal.get();
System.out.println(i);
//remove
threadLocal.remove();
System.out.println(threadLocal.get());
}
我们使用**ThreadLocal解决线程局部变量统一定义问题,**多线程数据不能共享。(InheritableThreadLocal特例除外)不能解决并发问题。解决了:基于类级别的变量定义,每一个线程单独维护自己线程内的变量值(*存、取、删的功能*)
二,get / set / remove方法
2.1 get方法
public T get() {
Thread t = Thread.currentThread();
//通过当前线程找到threadlocalmap
ThreadLocalMap map = getMap(t);
if (map != null) {
//通过指定的key找到指定的entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
//找到entry中的value
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
2.2 set方法
public void set(T value) {
//获取当前线程,当前进行再操作threadlocal的线程
Thread t = Thread.currentThread();
//获取当前线程的threadlocalmap
ThreadLocalMap map = getMap(t);
if (map != null)
//map不为空则直接设置值,注意是当前this对象(threadlocal对象)
map.set(this, value);
else
//空的话先构造,再设置值
createMap(t, value);
}
2.3 remove方法
public void remove() {
//获取当前线程的threadlocalmap
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
//要移除entry的位置
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
//找到对应的key
if (e.get() == key) {
// 将引用设置null,方便GC
e.clear();
//从该位置开始进行一次连续段清理
expungeStaleEntry(i);
return;
}
}
}
三,深入ThreadLocalMap
每个线程中都有一个ThreadLocalMap
数据结构
ThreadLocal.ThreadLocalMap threadLocals = null;
在每个线程中的threadlocalmap其实是一个和hashmap很相似的数据结构,但是threadlocalmap只有数组构成,内部通过一个entry数组来保存键值对,键key是当前threadlocal对象,value则是我们希望保存的本地线程变量
ThreadLocalMap类是一个为ThreadLocal专门定制的实现。使用开放寻址法的散列表存储
基本属性
//默认容量是16个entry
private static final int INITIAL_CAPACITY = 16;
//threadlocalmap实际上就是一个entry类型的数组
private Entry[] table;
//阀值,超过了就需要rehash
private int threshold;
//表中entry的个数
private int size = 0;
entry
static class Entry extends WeakReference<ThreadLocal<?>> {
//entry继承WeakReference,弱引用指向threadlocal实例
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
ThreadLocal在没有外部强引用时,发生GC时会被回收。所以后文会出现entry中的threadlocal对象被gc了,但是位置还占用着这种无效占用
构造threadlocalmap
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//开辟容量为16entry数组
table = new Entry[INITIAL_CAPACITY];
//计算firstKey在entry数组的索引
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//在索引位置实例化entry对象存放<firstKey,firstValue>
table[i] = new Entry(firstKey, firstValue);
size = 1;
//设置阀值为默认容量的2/3
setThreshold(INITIAL_CAPACITY);
}
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
threadlocalmap.set
private void set(ThreadLocal<?> key, Object value) {
//entry数组 tab
Entry[] tab = table;
//数组长度 len
int len = tab.length;
//获取key的索引
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相同
if (k == key) {
//则直接覆盖旧的value
e.value = value;
return;
}
//该threadlocal对象已经被gc,但是位置还被占着
if (k == null) {
//则替换当前失效的k所在Entry节点
replaceStaleEntry(key, value, i);
return;
}
}
//如果原来entry[i]位置没有元素,则直接将新元素放那里
tab[i] = new Entry(key, value);
//更新entry的数量
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
//清除一些无效entry
private boolean cleanSomeSlots(int i, int n) {
//判断是否移出成功
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
//此时i执行传进来的索引的下一个位置
i = nextIndex(i, len);
Entry e = tab[i];
//如果该位置被占但是其引用为null(已经被gc回收了)
if (e != null && e.get() == null) {
//则清除该位置
n = len;
removed = true;
//核心清理方法
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
//获取指定索引的下一个位置,如果是最后一个元素则获取第一个元素
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
set流程:
replaceStaleEntry
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
//当前失效节点
int slotToExpunge = staleSlot;
//从当前失效节点的前一个节点开始向前遍历
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
//查找并记录最前位置value为null的下标
if (e.get() == null)
slotToExpunge = i;
//此时slotToExpunge是最前位置value为null的下标
//从当前失效节点的下一个节点开始向后遍历
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
//如果找到key相同的entry,则直接覆盖旧的value
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
if (slotToExpunge == staleSlot)
//i之前的节点里,没有value为null的情况
slotToExpunge = i;
//清除无效的位置
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
//如果当前下标所在已经失效,并且向后扫描过程当中没有找到失效的Entry节点
if (k == null && slotToExpunge == staleSlot)
//则slotToExpunge赋值为当前位置
slotToExpunge = i;
}
//如果并没有在table当中找到该key,则直接在当前位置new一个Entry取代原来的无效引用
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
清除指定位置及其之后的无效的key----连续段清理
因为entry是弱引用
Entry extends WeakReference<ThreadLocal<?>
弱引用对象在内存充足时不会被gc,但是内存不足会被gc,如果此时gc了内存还不足分配,就会报OOM
所以
- 很多entry节点被GC了但是table数组中还占用着位置,不及时清理就会浪费位置
- 在清理节点的同时,可以将后续非空的Entry节点重新计算下标进行排放,这样子在get的时候就能快速定位资源,加快效率。(rehash)
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
//将制定需要清理的entry的value设置为null
tab[staleSlot].value = null;
//gc该entry
tab[staleSlot] = null;
//entry数量-1
size--;
Entry e;
int i;
//遍历被删除索引位置后边的entry
for (i = nextIndex(staleSlot, len);
//循环条件:该位置被占用
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
//获取当前遍历entry当中的key
if (k == null) {
//如果ThreadLocal为null,则将value以及数组下标所在位置设置null,方便GC,并且entry数量-1
e.value = null;
tab[i] = null;
size--;
} else {
//如果不为null,则重新计算位置后放于新位置
int h = k.threadLocalHashCode & (len - 1);
//如果是当前位置则遍历下一个位置
if (h != i) {
//不是当前位置,则重新从i开始找到下一个为null的坐标进行赋值
tab[i] = null;
while (tab[h] != null)
h = nextIndex(h, len);
//放到新位置
tab[h] = e;
}
}
}
return i;
}
全表清理expungeStaleEntries
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
Entry e = tab[j];
// 全表遍历,找到一个无效key,则做一次连续段清理
if (e != null && e.get() == null)
expungeStaleEntry(j);
}
}
四,执行流程总结
五,内存泄漏问题
在java中,我对内存泄漏问题的理解就是,一个对象你永远不用了,但是这个对象却无法被gc
ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。(比如线程池)
如何避免?
既然已经发现有内存泄露的隐患,自然有应对的策略,在调用ThreadLocal的get()、set()可能会清除ThreadLocalMap中key为null的Entry对象,这样对应的value就没有GC Roots可达了,下次GC的时候就可以被回收,当然如果调用remove方法,肯定会删除对应的Entry对象。
如果使用ThreadLocal的set方法之后,没有显示的调用remove方法,就有可能发生内存泄露,所以养成良好的编程习惯十分重要,使用完ThreadLocal之后,记得调用remove方法。
ThreadLocal<String> localName = new ThreadLocal();
try {
localName.set("kevin");
// 其它业务逻辑
} finally {
//使用完threadlocal变量后显示remove
localName.remove();
}