一,TL的基本使用与原理
1.基本使用
/**
* @author 二十
* @since 2021/8/28 11:19 下午
*/
public class TlTest {
private static AtomicInteger id = new AtomicInteger(0);
private static ThreadLocal<Integer> tl = ThreadLocal.withInitial(()->id.getAndIncrement());
private static CountDownLatch count = new CountDownLatch(3);
public static void main(String[] args)throws Exception {
new Thread(()->{
System.out.println(tl.get()+" "+Thread.currentThread().getName());
tl.remove();
count.countDown();
},"A").start();
new Thread(()->{
System.out.println(tl.get()+" "+Thread.currentThread().getName());
tl.remove();
count.countDown();
},"B").start();
new Thread(()->{
System.out.println(tl.get()+" "+Thread.currentThread().getName());
tl.remove();
count.countDown();
},"C").start();
count.await();
}
}
2.原理分析
里面维护一个ThreadLocalMap结构,每一个元素对应一个桶位。
使用ThreadLocal定义的变量,将指向当前线程本地的一个LocalMap空间。
ThreadLocal变量作为key,其内容作为value,保存在本地。
多线程对ThreadLocal对象进行操作,实际上是对各自的本地变量进行操作,不存在线程安全问题。
假设一个类里面定义了三个
threadlocal
,三个线程来访问这个类,每个线程本地会维护一个threadlocalmap
,每一个map里面会有三个entry
,key
是threadlocal
对象,value
是threadlocal
里面set
的值。
二,TL源码
1.属性
/**
* 线程获取Threadlocal.get()时,如果是第一次在某个threadlocal对象上get,会给当前线程分配一个value,
* 这个value和当前的threadlocal对象被包装成一个entry,其中key=threadlocal对象,
* value=threadlocal对象给当前线程生成的value。这个entry存放到哪个位置与这个value有关。
*/
private final int threadLocalHashCode = nextHashCode();
//创建threadlocal对象时会使用到,每创建一个threadlocal对象就会使用它分配一个hash值给对象。
private static AtomicInteger nextHashCode = new AtomicInteger();
//每创建一个threadlocal对象,这个nextHashCode就会增长0x61c88647。
private static final int HASH_INCREMENT = 0x61c88647;
//创建新的threadlocal对象的时候,给当前对象分配hash的时候用到。
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
//留给子类重写扩展的
protected T initialValue() {
return null;
}
//带初始化值得threadlocal
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
return new SuppliedThreadLocal<>(supplier);
}
public ThreadLocal() {
}
2.get()
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//根据当前线程获取对应的map
ThreadLocalMap map = getMap(t);
if (map != null) { //已经初始化
//根据当前threadlocal对象获取entry节点
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) { //节点初始化过
//获取entry的value并返回
T result = (T)e.value;
return result;
}
}
//走到这里说明map尚未初始化获取entry尚未初始化
return setInitialValue();
}
2.1 setInitialValue()
private T setInitialValue() {
//获取初始值,留给子类重写
T value = initialValue();
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程对应的map
ThreadLocalMap map = getMap(t);
//如果map初始化过
if (map != null)
//map里面放入当前对象和value
map.set(this, value);
else //map尚未初始化过
//初始化map--直接new一个并放入当前对象和value
createMap(t, value);
//返回value
return value;
}
2.2 getMap()
//返回当前线程的threadLocals
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
2.3 createMap()
//利用构造器初始化threadLocals并将当前线程和线程对应的value设置进去
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
3.set()
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) //当前线程对应的map已经初始化
map.set(this, value); //map放入值
else //map未初始化
createMap(t, value); //初始化map
}
4.remove()
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) //map已经初始化
m.remove(this); //调用map的remove移除掉当前对象对应的entry
}
5.内部类ThreadLocalMap
//threadlocalmap里面的key是弱引用 ,key=threadlocal对象
//value是强引用,value保存的是threadlocal对象与当前线程关联的value
//这样设计的好处是为了防止内存泄漏
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//map的初始化容量为16
private static final int INITIAL_CAPACITY = 16;
//map里面的entry桶位列表
private Entry[] table;
//列表容量
private int size = 0;
/**
* 扩容阈值 当前数组长度的三分之二
*/
private int threshold; // Default to 0
//将扩容阈值设置为当前数组长度的三分之二
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
//获取下一个位置
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
//获取下一个位置
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
//其实从上层的api可以发现这里其实是延迟初始化,只有线程第一次调用threadlocal的
//get或者set的时候才会初始化。
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//初始化散列表,长度为16
table = new Entry[INITIAL_CAPACITY];
//计算entry的存储位置
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//创建新的entry
table[i] = new Entry(firstKey, firstValue);
//占用量设置为1
size = 1;
//修改扩容阈值为初始化长度
setThreshold(INITIAL_CAPACITY);
}
5.1 getEntry()
private Entry getEntry(ThreadLocal<?> key) {
//根据当前线程的threadlocal对象获取entry的存储位置
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key) //校验entry是不是已经丢了,或者已经被覆盖
return e;
else //执行打这里说明entry已经丢了或者被发生了hash冲突,继续向后寻找
return getEntryAfterMiss(key, i, e);
}
5.2 getEntryAfterMiss()
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
//获取散列表
Entry[] tab = table;
//获取散列表的长度
int len = tab.length;
//如果entry不为空,那就说明entryhash冲突了
while (e != null) {
//获取entry对应的threadlocal对象
ThreadLocal<?> k = e.get();
//说明key对应的threadlocal对象已经被回收了,当前entry属于脏数据
if (k == key)
//直接返回
return e;
//如果key==null,说明key对应的threadlocal对象已经被回收了,当前entry属于脏数据
if (k == null)
//做一次探测式过期清理
expungeStaleEntry(i);
else //执行到这里说明发生了hash冲突,继续从当前位置往后寻找
i = nextIndex(i, len);
e = tab[i];
}
//说明entry过期了,直接返回null
return null;
}
5.3 expungeStaleEntry() 探测式过期清理
private int expungeStaleEntry(int staleSlot) {
//获取散列表
Entry[] tab = table;
//获取散列表的长度
int len = tab.length;
//因为此处threadlocal对象已经被回收,所以直接将value设置为null,help GC
tab[staleSlot].value = null;
//再讲当前桶位设置为空
tab[staleSlot] = null;
/**
* 为什么这里要分两次设置为null?
* 因为key本身是弱引用,但是value是强引用,如果直接回收桶位,value无法直接被回收
*/
//散列表的占用长度-1
size--;
Entry e;
int i;
//从当前节点所在位置的下一个位置直到最后循环,
for (i = nextIndex(staleSlot, len);
//停止条件是当前索引对应桶位=null
(e = tab[i]) != null;
//循环条件是每次索引+1
i = nextIndex(i, len)) {
//获取entry的threadlocal对象
ThreadLocal<?> k = e.get();
if (k == null) {//如果对象为空,说明已经过期了,entry是脏数据
//回收
e.value = null;
tab[i] = null;
size--;
} else {//此时说明entry不是脏数据
//计算threadlocal对象在散列表的新索引,为啥重新计算?
//因为当前get到了脏数据,刚刚从散列表移除,所以散列表的占用量已经发生了变化
int h = k.threadLocalHashCode & (len - 1);
//如果没有发生hash冲突
if (h != i) {
//将原来的桶位释放
tab[i] = null;
//寻找存放位置,直到所在桶位为空,因为可能计算出的位置发生了hash冲突,
//这个时候,就要索引下推到下一桶位
while (tab[h] != null)
h = nextIndex(h, len);
//将entry放到新的桶位
tab[h] = e;
}
}
}
//返回最后处理的索引处
return i;
}
5.4 set()
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];//当前threadlocal对象所对应的节点
e != null; //终止条件是entry为空,说明这个桶位能存放entry
e = tab[i = nextIndex(i, len)]) { //桶位下推
ThreadLocal<?> k = e.get();
//如果当前对象所对应的桶位有值,且当前桶位的key是当前对象,
//说明这是一次值重置,直接覆盖旧的值即可
if (k == key) {
e.value = value;
return;
}
//如果k==null,说明当前位置对应的entry是过期的,
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//来到这里的条件:这次操作不是一次对已经有的值得覆盖,或者已经找到了应该存放当前entry的桶位
tab[i] = new Entry(key, value);
int sz = ++size;
//如果达到了扩容的条件,进行扩容操作
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
5.5 replaceStaleEntry()替换过期entry
//替换过期entry
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); //i=当前索引的前一个索引
(e = tab[i]) != null; //终止条件是索引所在的桶位有数据
i = prevIndex(i, len)) //循环条件是每次往前一个桶位
//说明是过期的,那就继续往前清理
if (e.get() == null)
slotToExpunge = i;
//从当前位置向后清理
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
//如果当前位置的key和当前threadlocal对象一致
if (k == key) {
//进行值覆盖操作
e.value = value;
//将过期数据放到当前循环到的table[i]
tab[i] = tab[staleSlot];
//这里的逻辑其实就是进行一下位置优化
tab[staleSlot] = e;
//说明上面的循环并没有找到过期数据
if (slotToExpunge == staleSlot)
//吧探测的开始位置改成当前位置
slotToExpunge = i;
//进行探测式过期清理
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
//当前遍历entry是一个过期数据 && 往前找过期数据没找到
if (k == null && slotToExpunge == staleSlot)
//更新探测位置为当前位置
slotToExpunge = i;
}
//将新的值放入当前节点
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
//如果两个索引不相等,就继续清理
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
5.6 cleanSomeSlots()启发式清理工作
//启发式清理工作 i 开始清理位置 n 结束条件,数组长度
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
//获取当前i的下一个下标
i = nextIndex(i, len);
//获取当前下标为I的元素
Entry e = tab[i];
//断定为过期元素
if (e != null && e.get() == null) {
n = len;//更新数组长度
removed = true;
//从当前过期位置开始一次谈测试清理工作
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);//假设table.length=16
return removed;
}
5.7 rehash()
private void rehash() {
//遍历,探测式清理,干掉所有过期数据
expungeStaleEntries();
//仍然达到扩容条件
if (size >= threshold - threshold / 4)
//扩容
resize();
}
5.8 resize()
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]; //访问old表指定位置的data
if (e != null) { //data存在
ThreadLocal<?> k = e.get();
if (k == null) { //过期数据
e.value = null; // Help the GC
} else {
int h = k.threadLocalHashCode & (newLen - 1);//重新计算hash值
while (newTab[h] != null) //获取到一个最近的,可以使用的位置
h = nextIndex(h, newLen);
newTab[h] = e; //数据迁移
count++;
}
}
}
setThreshold(newLen);//设置下一次扩容的指标
size = count;
table = newTab;
}
5.9 remove()
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)]) {
//从当前位置开始,如果桶位是空,就去下一个
//如果不为空的桶位与当前线程的threadlocal对象一致
if (e.get() == key) {
e.clear(); //干掉key的引用
expungeStaleEntry(i); //探测式过期清理
return;
}
}
}