一,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;}//带初始化值得threadlocalpublic static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {return new SuppliedThreadLocal<>(supplier);}public ThreadLocal() {}
2.get()
public T get() {//获取当前线程Thread t = Thread.currentThread();//根据当前线程获取对应的mapThreadLocalMap 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();//获取当前线程对应的mapThreadLocalMap map = getMap(t);//如果map初始化过if (map != null)//map里面放入当前对象和valuemap.set(this, value);else //map尚未初始化过//初始化map--直接new一个并放入当前对象和valuecreateMap(t, value);//返回valuereturn value;}
2.2 getMap()
//返回当前线程的threadLocalsThreadLocalMap 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的初始化容量为16private 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) {//初始化散列表,长度为16table = new Entry[INITIAL_CAPACITY];//计算entry的存储位置int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);//创建新的entrytable[i] = new Entry(firstKey, firstValue);//占用量设置为1size = 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过期了,直接返回nullreturn null;}
5.3 expungeStaleEntry() 探测式过期清理
private int expungeStaleEntry(int staleSlot) {//获取散列表Entry[] tab = table;//获取散列表的长度int len = tab.length;//因为此处threadlocal对象已经被回收,所以直接将value设置为null,help GCtab[staleSlot].value = null;//再讲当前桶位设置为空tab[staleSlot] = null;/*** 为什么这里要分两次设置为null?* 因为key本身是弱引用,但是value是强引用,如果直接回收桶位,value无法直接被回收*///散列表的占用长度-1size--;Entry e;int i;//从当前节点所在位置的下一个位置直到最后循环,for (i = nextIndex(staleSlot, len);//停止条件是当前索引对应桶位=null(e = tab[i]) != null;//循环条件是每次索引+1i = 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为空,说明这个桶位能存放entrye = 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
//替换过期entryprivate 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=16return 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表指定位置的dataif (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;}}}
