什么是ThreadLocal?
ThreadLocal是线程本地副本,就是每个线程都会有自己私有的副本值,互不干扰。典型的一种空间换时间的做法。
基本流程如下:
第一步:首先通过threadLocal.set("xxxx")获取当前线程副本值
-- 大体流程:获取当前线程->拿到当前线程的threadLocals变量->根据map是否有来设置值还是创建值
第二步:通过threadLocal.get()直接获取当前线程的本地副本值
-- 大体流程:获取当前线程->获取当前线程的threadLocals->获取当前threadLocals的hashcode,得到下标,然后去threadLocals的table数组获取值返回
其他,了解ThreadLocal与ThreadLocalMap与table[]数组关系
ThreadLocal是最外层,threadLocal可以根据对应线程获取当前线程的ThreadLocalMap,然后ThreadLocalMap里面包含一个Entry table[]数组(Entry<ThreadLocal<?>, Object>, 其中key的ThreadLocal类型是一个弱引用,容易被回收),数组的下标使用当前ThreadLocal的hashCode(key.threadLocalHashCode & (len-1))来定位出table[]数组下标,进而进行获取和更新
怎么避免hash冲突?
ThreadLocalMap解决hash冲突采用的是线性探测方式,采用步长+1的方式往下找
-- 获取下一个步长代码为:private static int nextIndex(int i, int len) {return ((i + 1 < len) ? i + 1 : 0);}
为什么使用开放地址法-线性探测 来处理ThreadLocalMap的hash冲突?
-- 第一点:ThreadLocal存放的数量往往不会很大,同时会被回收,所以使用开放地址法数组会更快,更省空间。
-- 第二点:ThreadLocal中用了一个HASH_INCREMENT = 0x61c88647,0x61c88647是一个很神奇的数字,能让hash均匀分配在2的N次方数组里
---- 并且那次获取hashcode的时候都会通过如下方式获取,由于nextHashCode是AtomiInteger,所以每次获取都不一样
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
/**
* 一个很神奇的数字,具体怎么神奇要自己探究了
*/
private static final int HASH_INCREMENT = 0x61c88647;
/**
* 返回下一个hashcode
*/
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
static和非static有啥区别?
一般建议ThreadLocal使用static的方式,因为如果不用static,由于Entry的键是ThreadLocal,那就每次都要创建ThreadLocal实例。造成资源浪费。
怎么进行回收?避免内存泄漏?
1 设置ThreadLocalMap中table[]数组Entty<ThreadLocal<?>, Object>的键为弱引用
-- 实现弱引用的代码:static class Entry extends WeakReference<ThreadLocalCode<?>> {.........}
2 每次使用完threadLocal需要手动调用remove()进行释放(其实就是赋值为null)
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());// 获取对应线程
if (m != null)
// 将this.referent = null、table[i].value=null、table[i]=null
m.remove(this);
}
}
3 如果不调用remove方法,每次调用get()、set(),都会顺便回收一下key为空的Entry
-- set()核心调用的回收方法逻辑为:
// k==null&&e!=null 说明key被垃圾回收了,这里涉及到弱引用,接下来讲
if (k == null) {
// 被回收的话就需要替换掉过期的值,把新的值放在这里返回
replaceStaleEntry(key, value, i);
return;
}
举个ThreadLocal set()/get()代码如下:
ThreadLocal<String> sThreadLocalThread = new ThreadLocal<>();
sThreadLocalThread.set("设置个值");// 设置线程本地副本值
sThreadLocalThread.get();// 获取线程本地副本值
第一步:首先通过threadLocal.set("xxxx")获取当前线程副本值
public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// 拿到当前线程的ThreadLocalMap对象,实际就是返回当前线程的threadLocals变量t.threadLocals
ThreadLocalMap map = getMap(t);
// this作key传入存储value
if (map != null)
map.set(this, value);
else
createMap(t, value);// t.threadLocals = new ThreadLocalMap(this, firstValue)
}
// 上面map.set(this, value)调用如下
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
//计算数组的下标
int i = key.threadLocalHashCode & (len-1);
//注意这里结束循环的条件是e != //null,这个很重要,还记得上面讲的开放地址法吗?忘记的回到上面看下,一定看懂才往下走,不然白白浪费时间
//这里遍历的逻辑是,先通过hash 找到数组下标,然后寻找相等的ThreadLocal对象
//找不到就往下一个index找,有两种可能会退出这个循环
// 1.找到了相同ThreadLocal对象
// 2.一直往数组下一个下标查询,直到下一个下标对应的是null 跳出
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//如果找到直接设置value 值返回,这个很简单没什么好讲的
if (k == key) {
e.value = value;
return;
}
// k==null&&e!=null 说明key被垃圾回收了,这里涉及到弱引用,接下来讲
if (k == null) {
//被回收的话就需要替换掉过期的值,把新的值放在这里返回
replaceStaleEntry(key, value, i);
return;
}
}
//来到这里,说明没找到
tab[i] = new Entry(key, value);
int sz = ++size;
// 查找已被gc回收的对象,并清除entry
// 如果没有清除任何entry,并且当前使用量达到了负载因子所定义(长度的2/3),那么进行rehash()
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
第二步:通过threadLocal.get()直接获取当前线程的本地副本值
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 拿到当前线程的ThreadLocalMap对象:t.threadLocals
ThreadLocalMap map = getMap(t);
if (map != null) {
//因为ThreadLocalMap存的时候就是拿ThreadLocal的hashcode作key.
//因此这里传入this获取entry,再拿到value
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}