Thread和ThreadLocal的关系
初始化ThreadLocalMap和弱引用Entry
set方法与哈希冲突
清理槽
get方法也会清理槽
扩容
手动清理的重要性
Thread和ThreadLocal的关系
每个Thread中都持有一个ThreadLocalMap的实例,ThreadLocalMap是ThreadLocal的内部类。当Thread中没有ThreadLocalMap则需要先实例化ThreadLocalMap.
public class Thread implements Runnable { ThreadLocal.ThreadLocalMap threadLocals = null;//该对象是ThreadLocal中的内部类ThreadLocalMap } public class ThreadLocal<T> { //计算出来的hash值用它来确定Entry存放到哪个哈希槽 private final int threadLocalHashCode = nextHashCode(); //这是个固定值 private static final int HASH_INCREMENT = 0x61c88647; //这个默认值是0,但new ThreadLocal后断点看到的值不是0,这是因为这是一个静态成员,在我们自己创建ThreadLocal前,main方法会先加载ThreadLocal给这个赋值了。 private static AtomicInteger nextHashCode = new AtomicInteger(); //每次调用该方法都会在原有的nextHashCode值上加上0x61c88647 private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); } //设置值 public void set(T value) { Thread t = Thread.currentThread();//获取当前线程。 ThreadLocalMap map = getMap(t);//获取当前线程的成员变量ThreadLocal.ThreadLocalMap threadLocals if (map != null) map.set(this, value);//如果当前线程中的ThreadLocalMap已经实例化则set else createMap(t, value);//如果当前线程中的ThreadLocalMap没有实例化则实例化。 } //在这走实例化ThreadLocalMap void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); } }
初始化ThreadLocalMap和弱引用Entry
ThreadLocalMap里最重要的属性是Entry[],这个数组的初始长度是16,扩容阈值是size*2/3,Entry是ThreadLocalMap的内部类,Entry继承了弱引用。Entry里的key是ThreadLocal,value是设置的值。如果ThreadLocal栈引用结束了,在发生GC时虽然Entry还持有ThreadLocal的引用,这个ThreadLocal也会被垃圾回收,所以ThreadLocalMap常常伴随着扩容,清理操作。
static class ThreadLocalMap { //继承WeakReference很重要,WeakReferences是弱引用,在每次GC后都会回收弱引用对象里的引用值(若通过可达性分析查到引用值没有其他可达的Root,则会回收) //这个Entry就构成了唯一的key,也就是ThreadLocal。value是ThreadLocal.set(parameter)的参数 static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k);//最终传递给了Reference中的referent value = v; } } //ThreadLocalMap中的容器,一个线程持有一个ThreadLocalMap就相当于持有了一个Entry数组 private Entry[] table; //数组的初始容量 private static final int INITIAL_CAPACITY = 16; ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { table = new Entry[INITIAL_CAPACITY];//实例化数组 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);//确定数组的位置 //初始化ThreadLocalMap不会出现hash冲突。 table[i] = new Entry(firstKey, firstValue); //已有元素++ size = 1; //计算扩容阈值 setThreshold(INITIAL_CAPACITY); } private void setThreshold(int len) { //初始化的容量第一次扩容的阈值是10,也就是说在数组的size是10的情况下就会触发扩容。 threshold = len * 2 / 3; } }
}
set方法与哈希冲突
ThreadLocal的set方法是使用ThreadLocalMap的set方法。他分为四种情况。1 计算哈希后确定的槽内是null没有Entry表示没有哈希冲突,此时new一个Entry放入槽内。 2 计算哈希后确定的槽内有Entry但是槽内的Entry的key和当前的ThreadLocal相同则直接替换value。
3 计算哈希后确定的槽内有Entry但是key和当前ThreadLocal并不是同一个,则表示哈希冲突,此时顺着数组往右寻找,直到碰到有Entry但是没有key的槽,这表示这个槽内曾经有过ThreadLocal但是被GC掉了,此时这个槽是个废槽,可以替换掉Entry。 4 哈希冲突后向右
并没有找到被GC的槽,此时只能是找到距离最近的一个槽内没有Entry的,创建一个Entry存入。
static class ThreadLocalMap { //顺着当前下标往后查询。如果查询到了数组末尾则返回0号下标 private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); } //顺着当前下标往前查询。如果已经是0则返回数组末尾下标 private static int prevIndex(int i, int len) { return ((i - 1 >= 0) ? i - 1 : len - 1); } private void set(ThreadLocal<?> key, Object value) { //拿到数组 Entry[] tab = table; //数组长度 int len = tab.length; //hash&length-1 效果类似hash%length int i = key.threadLocalHashCode & (len-1); //在这就要处理hash冲突了。如果hash值不冲突,那么算出来的index位置的Entry肯定是null.那么不会进入循环。 //如果进入了循环,有没有可能两个if都不满足,有可能。这表示hash值冲突了,但是不是同一个ThreadLocal,并且hash值相同的槽内的ThreadLocal没有被GC。 //那么只能是一直找到Entry是null的位置,然后跳出循环。 for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); //如果是第一次循环到这里进去了,表示是同一个ThreadLocal多次设置值。则直接替换值。情况2 if (k == key) { e.value = value; return; } //如果ThreadLocal为null则表示发生了GC把弱引用ThreadLocal清理了。 //需要将当前set的key和value放入这个废掉的槽内,并且看看有没有需要清理的槽。情况3 if (k == null) { replaceStaleEntry(key, value, i); return; } } //没有进入循环,或者从循环跳出了。如果没有进入循环则i就是hash&length-1的位置表示当前算出来的hash值没有冲突,也是第一次使用。情况1 //如果是循环跳出来的,则这个i就是hash&length-1.算出来的位置向后移动循环次数的位置。表示hash冲突了,并且冲突后的槽往后也都没有被GC //只能是往后顺延找别的可用槽。总之会找到一个在数组内Entry为空的位置。创建Entry放进数组。情况4 tab[i] = new Entry(key, value); //已有元素++ int sz = ++size;
//如果没有清理槽,并且当前长度已经大于等于了阈值则扩容 if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); } //走到这个方法表示通过hash&length-1的位置上的Entry中的key是null或者是哈希冲突后,往数组后查询发现有Entry中的key是null private void replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot) { //数组 Entry[] tab = table; //数组长度 int len = tab.length; Entry e; //Entry为null的哈希槽 int slotToExpunge = staleSlot; //从Entry为null的哈希槽位置向前找,一直找到Entry为null停止 for (int i = prevIndex(staleSlot, len);(e = tab[i]) != null;i = prevIndex(i, len)){ //在向前寻找的过程中标记Entry中key为null的下标 if (e.get() == null) slotToExpunge = i; } //从Entry为null的哈希槽位置向后找,一直找到Entry为null停止 for (int i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) { //循环中的Entry中的key ThreadLocal<?> k = e.get(); if (k == key) { //如果key相同则替换value e.value = value; //将Entry中ThreadLocal为null的赋值给当前槽中 tab[i] = tab[staleSlot]; //在将Entry赋值给原来ThreadLocal为null的槽中。 //这两行操作相当于把槽里的内容互换了,达到的效果是前边的槽中的Entry有key,循环中的也就是后边的没有key tab[staleSlot] = e; //如果列表向左查询没有发现Entry中key有null的。则将当前循环中的槽的位置赋值。 //因为上两步操作已经把当前槽变成了key为null的槽,所以此处记录的位置就是key是null的位置 //如果向左查询有Entry里是null值那就表示这个区间内还有更左边有key是null的 if (slotToExpunge == staleSlot){ slotToExpunge = i; } //清理槽 cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); return; } //当前循环中的槽也是被GC过的。并且向左查询没有发现Entry为null的,就记录当前槽的位置。 if (k == null && slotToExpunge == staleSlot){ slotToExpunge = i; } } //出循环只有一种情况,key为null的Entry下标往后寻找没有发现与当前ThreadLocal相同的key。 //此时需要将原来Entry的value职位null。此操作用来释放内存。 tab[staleSlot].value = null; //创建一个新的Entry其中key是当前ThreadLocal,value是set的参数。将它放到被GC的位置。 tab[staleSlot] = new Entry(key, value); //如果向左查询有Entry中key是null的slotToExpunge就是在左边确定的 //如果向左查询没有Entry中key是null的,而向右查询有Entry中key是null的slotToExpunge就是右边确定的。 //如果两边都没有的情况表示当前区间内只有staleSlot一个为Entry是null的而这种情况下直接重新覆盖了Entry。不需要清理。条件不成立。 if (slotToExpunge != staleSlot){ cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); } } }
清理槽
expungeStaleEntry方法就是将废槽清空,然后将哈希冲突的槽重新分配位置,因为哈希冲突后是从哈希位向后移动寻找Entry是null的槽放入的,此后这些冲突的槽可能有被清理的,所以重新分配位置,方法的返回值是Entry为null的位置,cleanSomeSlots方法从这个位置
继续寻找有没有废槽,如果有就清理。
static class ThreadLocalMap { //接收的参数是槽里没有Entry的槽和当前数组的长度 private boolean cleanSomeSlots(int i, int n) { boolean removed = false; Entry[] tab = table; int len = tab.length; do { //找到下一个槽的位置 i = nextIndex(i, len); //获取槽内的Entry Entry e = tab[i]; //如果槽内有Entry,并且Entry的key是null,表示这是个废槽。 if (e != null && e.get() == null) { n = len; //有废槽肯定要清理的。 removed = true; //方法返回下一个槽内没有Entry的槽下标 i = expungeStaleEntry(i); } } while ( (n >>>= 1) != 0);//这个操作相当于折半除2的操作。10,5,2,0, return removed; } //接收的参数是槽下标内有Entry,但是Entry的key被GC了。 private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; //将槽清空 tab[staleSlot].value = null; tab[staleSlot] = null; //Entry[]-- size--; Entry e; int i; //循环的开始是废槽的下一个,终止条件是下一个槽有Entry for (i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) { //拿到槽内的ThreadLocal ThreadLocal<?> k = e.get(); //如果槽内的key也是null则表示这也是个废槽,则也需要做清空操作。 if (k == null) { e.value = null; tab[i] = null; size--; } else { //如果槽内有的Entry有key,则通过hash值算出槽的位置。 int h = k.threadLocalHashCode & (len - 1); //如果算出的槽位置不是当前的位置则表示这个key曾经哈希冲突了,所以位置并不是哈希位。 if (h != i) { //将这个槽清空 tab[i] = null; //从计算的哈希位开始循环,找到Entry为null的槽,将刚刚清空槽里的Entry重新安置。 while (tab[h] != null){ h = nextIndex(h, len); } //这一步的操作的意义在于,如果循环中有if条件满足的,这代表当前i这个位置之前有可用的槽,那就从哈希位开始往后找,找到空槽,重新安置这个Entry。 tab[h] = e; } } } return i;//入参staleSlot是一个废槽,返回的i则是一个Entry为null的槽。 } }
get方法也会清理槽
get方法通过当前ThreadLocal获取Entry[]中对应的Entry,如果ThreadLocalMap未实例化则实例化并返回null,通过哈希位找到了就返回,哈希位上的不是当前ThreadLocal则表示哈希冲突,继续在数组后寻找,如果途中发现有废槽则清理,如果最终没有找到则返回null。
public class ThreadLocal<T> { public T get() { //获取当前线程 Thread t = Thread.currentThread(); //获取线程内的ThreadLocalMap ThreadLocalMap map = getMap(t); //如果ThreadLocalMap已经实例化 if (map != null) { //通过ThreadLocal这个key到数组中找到Entry,是有可能找不到返回null的 ThreadLocalMap.Entry e = map.getEntry(this); //如果找到了,返回Entry中的value if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } //走到这里两种情况,1 ThreadLocalMap没有实例化,则实例化 2 从Entry[]没有找到对应ThreadLocal的Entry return setInitialValue(); } //这个方法和set差不多,但是它可以返回null。 private T setInitialValue() { //如果现在使用的就是ThreadLocal则一定返回null. T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); return value; } //这个方法只能子类重写,意味着可以给ThreadLocal赋默认值。 protected T initialValue() { return null; } } static class ThreadLocalMap { static class ThreadLocalMap { //通过ThreadLocal找Entry private Entry getEntry(ThreadLocal<?> key) { //计算哈希位 int i = key.threadLocalHashCode & (table.length - 1); //查看哈希位上的Entry Entry e = table[i]; //如果Entry不是null或者Entry的key就是当前的ThreadLocal则找到了返回Entry if (e != null && e.get() == key){ return e; } else{ //如果从哈希位没有找到Entry或者Entry中的key不是当前ThreadLocal return getEntryAfterMiss(key, i, e); } } //接收的参数是当前ThreadLocal,计算的哈希位,和这个哈希位上的Entry private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; //从哈希位上开始循环寻找 while (e != null) { ThreadLocal<?> k = e.get(); //如果找到了key相同的则返回 if (k == key){ return e; } //如果当前槽内的key是null则要被清理 if (k == null){ expungeStaleEntry(i); }else{ //如果槽内的key有值则继续寻找。直到Entry位null停止。 i = nextIndex(i, len); } //下一个位置继续找 e = tab[i]; } //如果循环结束了,表示哈希位往后寻找的key都不是当前的ThreadLocal,返回null。 return null; } }
扩容
当数组内的元素到达阈值后触发扩容,扩容操作进行前会遍历数组进行清理。如果清理后仍然达到阈值则二倍扩容,循环扩容前的数组,根据新数组的长度重新计算哈希值,如果哈希槽内没有元素则放入,如果有则线性查询可用槽放入。然后用新的数组替换老的数组。
static class ThreadLocalMap { //扩容 private void rehash() { //清理一遍槽 expungeStaleEntries(); //大于阈值扩容 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); } } } private void resize() { //扩容前的数组 Entry[] oldTab = table; //扩容前数组的长度 int oldLen = oldTab.length; //二倍扩容 int newLen = oldLen * 2; //创建新的数组 Entry[] newTab = new Entry[newLen]; int count = 0; //遍历扩容前的数组 for (int j = 0; j < oldLen; ++j) { Entry e = oldTab[j]; //如果Entry不是null if (e != null) { //获取key ThreadLocal<?> k = e.get(); if (k == null) { //key是null清理 e.value = null; } else { //根据哈希值算出来在新的数组中的位置。 int h = k.threadLocalHashCode & (newLen - 1); //新的位置上有Entry表示哈希冲突,则继续向后寻找。 while (newTab[h] != null){ h = nextIndex(h, newLen); } //找到一个Entry为null的位置存放Entry。 newTab[h] = e; count++; } } } //设置新的阈值 setThreshold(newLen); //新数组内元素的总个数 size = count; //替换数组 table = newTab; } }
手动清理的重要性
clear方法就是把ThreadLocal从Entry中删除,然后删除Entry。这样Entry就没有了引用会被GC。如果不使用clear,那么就算是ThreadLocal栈内存释放了,这个对象还是存在于Thread里的ThreadLocalMap里的Entry[]数组中,除非遇到GC否则永远存在。手动清理的作用就在于不用等待GC自己把Entry清理。
public class ThreadLocal<T> { //通过ThreadLocalMap的remove方法释放内存 public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); } ThreadLocalMap getMap(Thread t) { return t.threadLocals; } } static class ThreadLocalMap { //通过当前ThreadLocal删除 private void remove(ThreadLocal<?> key) { Entry[] tab = table; int len = tab.length; //计算哈希位 int i = key.threadLocalHashCode & (len-1); //循环找匹配的key for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) { if (e.get() == key) { //调用Refereence的clear把key清空 e.clear(); //再次清理槽。 expungeStaleEntry(i); return; } } } } public abstract class Reference<T> { private T referent;//这个就是ThreadLocal对象 public void clear() { this.referent = null; } }