ThreadLocal是JDK1.2提供的,做用是给单个线程内的共享变量提供载具。每一个线程之间的ThreadLocal里的数据是相互隔离的,并随着线程的消亡而消亡。java
ThreadLocal提供了get(),set(T value),remove()3个对外方法。数组
ThreadLocal常被用来作登入状态信息的存储。可是若是当前线程操做完不对状态信息作remove()可能会出现坑。咱们拿购买商品举个例子:jvm
能够看到B用户使用了A用户的信息去购买了商品,正确的作法应该是每一个线程使用结束后去remove()。源码分析
ThreadLocal的UML图以下this
调用set方法真正的数据是存在ThreadLocalMap里的,而ThreadLocalMap是线程Thread的成员变量,因此说线程Thread被jvm回收后ThreadLocalMap也会被回收。ThreadLocalMap的实现是采用顺序存储结构哈希表,它跟HashMap不一样,每一个hash地址只能存一个数据。它key存的是ThreadLocal自己并且它的Entry继承至WeakReference,因此它的key若是没被强引用会在GC的触发的时候回收掉。线程
public void set(T value) { //获取当前线程 Thread t = Thread.currentThread(); //根据当前线程获取ThreadLocalMap ThreadLocalMap map = getMap(t); //若是map不为空设置值 if (map != null) map.set(this, value); //若是map为空说明线程中成员变量ThreadLocalMap还没被建立,则建立map else createMap(t, value); }
这个方法主要根据当前线程获取ThreadLocalMap,若是还没初始化则调用createMap(t, value)初始化,反之调用map.set(this, value)设置值。设计
下面看下getMap(t)的实现code
ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
很简单就是获取Thread的成员变量threadLocalsblog
先来看下map为空调用createMap(t, value)去建立ThreadLocalMap的状况:继承
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
createMap(t, value)直接是new的ThreadLocalMap,ThreadLocalMap构造方法以下:
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { //1.建立和初始化table容量 table = new Entry[INITIAL_CAPACITY]; //2.快速hash获取下标地址 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); //3.建立Entry,存放第一个数据 table[i] = new Entry(firstKey, firstValue); //4.设置存储个数 size = 1; //5.设置扩容阀值 setThreshold(INITIAL_CAPACITY); }
private final int threadLocalHashCode = nextHashCode(); private static AtomicInteger nextHashCode = new AtomicInteger(); private static final int HASH_INCREMENT = 0x61c88647; private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); }
nextHashCode()的方法每次建立ThreadLocal都会加HASH_INCREMENT从新计算threadLocalHashCode的值,HASH_INCREMENT这个魔数的选取与斐波那契散列有关为了让哈希码能均匀的分布在2的N次方的数组里,这里指table数组。
static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
因为key是弱引用,因此它的key若是没被强引用会在GC的触发的时候回收掉。
private void setThreshold(int len) { threshold = len * 2 / 3; }
扩容阀值的计算是容量大小的2/3是,这里结果是10。
下面看下map.set(this, value)实现
private void set(ThreadLocal<?> key, Object value) { //调用set以前已经作过判断,因此table已经初始化了 Entry[] tab = table; //获取tab的长度 int len = tab.length; //1.快速hash获取下标地址 int i = key.threadLocalHashCode & (len-1); //2.用线性探测法解决冲突 for (Entry e = tab[i]; e != null; //取下个下标值 e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); //3.若是这个key已经存在,从新设置值 if (k == key) { e.value = value; return; } //4.若是key已通过期,则替换这个脏槽 if (k == null) { replaceStaleEntry(key, value, i); return; } } //5.建立Entry tab[i] = new Entry(key, value); //6.存储个数加1 int sz = ++size; //7.清理key已通过期清理的脏槽,若是没脏槽而且存储个数已经大于扩容阀值,则扩容 if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); }
遍历的实现其实设计了一个环,从i开始遍历到达len长度后又开始从0开始。实际上这里用线性探测法解决冲突不会到达len长度,由于在到达以前已经进行了扩容。
下面来看下replaceStaleEntry(key, value, i)的源码
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) { Entry[] tab = table; int len = tab.length; Entry e; int slotToExpunge = staleSlot; //1.向前查找,找到第一个key过时的脏槽 for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)) if (e.get() == null) slotToExpunge = i; //2.从staleSlot位置开始向后查找,若是找到key,交换至staleSlot位置的脏槽 for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); //找到key if (k == key) { //交换至staleSlot位置的脏槽 e.value = value; tab[i] = tab[staleSlot]; tab[staleSlot] = e; //若是slotToExpunge == staleSlot,说明前面没有脏槽,直接从i位置开始清理 if (slotToExpunge == staleSlot) slotToExpunge = i; //清理脏槽 cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); return; } //若是slotToExpunge == staleSlot,说明前面没有脏槽,直接从i位置开始清理 if (k == null && slotToExpunge == staleSlot) slotToExpunge = i; } //3.若是没有找到key,则建立一个新的Entry放至staleSlot位置的脏槽 tab[staleSlot].value = null; tab[staleSlot] = new Entry(key, value); //4.若是运行过程当中有找到脏槽,清理之 if (slotToExpunge != staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); }
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len)的做用主要是清理脏槽expungeStaleEntry(slotToExpunge)方法做用的从slotToExpunge位置(包括slotToExpunge)开始清理临近的脏槽。
下面来看下expungeStaleEntry(slotToExpunge)的源码
private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; // 1.清理槽 tab[staleSlot].value = null; tab[staleSlot] = null; //存储个数减一 size--; // 2.重hash或清理staleSlot以后的槽,直到空值 Entry e; int i; for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); //2.1若是轮询到的k为空,则清理之 if (k == null) { e.value = null; tab[i] = null; size--; } else { //2.2重hash,从新设置hash已经改变的Entry的位置 int h = k.threadLocalHashCode & (len - 1); if (h != i) { tab[i] = null; while (tab[h] != null) h = nextIndex(h, len); tab[h] = e; } } } //3.返回清理遍历的最后位置i return i; }
拿到清理遍历的最后位置i后会调用cleanSomeSlots(int i, int n)继续从i开始清理脏槽下面来看下的源码:
private boolean cleanSomeSlots(int i, int n) { boolean removed = false; Entry[] tab = table; int len = tab.length; do { //环形遍历 i = nextIndex(i, len); Entry e = tab[i]; //若是是脏槽,则清理之 if (e != null && e.get() == null) { n = len; removed = true; //最终调用expungeStaleEntry(i)去清理 i = expungeStaleEntry(i); } //log2(n)清理次数 } while ( (n >>>= 1) != 0); return removed; }
cleanSomeSlots(int i, int n)主要功能就是从i位置开始遍历log2(n)次去清理槽,为何是log2(n)次官方给的缘由是简单,快速。因此这个方法可能不是清理全部的脏槽,而是简单快速的清理几个脏槽。
下面来看下rehash()方法
private void rehash() { //1.清理全部的脏槽 expungeStaleEntries(); //2.若是清理事后存储个数仍是大于扩容阀值的3/4,则扩容 if (size >= threshold - threshold / 4) resize(); }
下面看下expungeStaleEntries()方法源码
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); } }
代码很简单就是遍历全部的table并清理脏槽。
下面看下resize()方法源码
private void resize() { Entry[] oldTab = table; //获取老table容量 int oldLen = oldTab.length; //新table容量扩大2倍 int newLen = oldLen * 2; Entry[] newTab = new Entry[newLen]; int count = 0; //遍历老的table,对全部Entry重hash定位 for (int j = 0; j < oldLen; ++j) { Entry e = oldTab[j]; if (e != null) { ThreadLocal<?> k = e.get(); //若是遇到脏槽,清理之帮助GC if (k == null) { e.value = null; // Help the GC } else { //重hash int h = k.threadLocalHashCode & (newLen - 1); //线性探测法解决冲突 while (newTab[h] != null) h = nextIndex(h, newLen); newTab[h] = e; count++; } } } //从新计算扩容阀值 setThreshold(newLen); //从新设置存储个数 size = count; //从新设置table table = newTab; }
resize()实现也比较简单,先建立比原来大2倍的Entry数组,并遍历老的table,对全部Entry重hash定位,若是冲突就是采用线性探测法解决冲突。
看下get()的源码
public T get() { //获取当前线程 Thread t = Thread.currentThread(); //根据当前线程获取ThreadLocalMap ThreadLocalMap map = getMap(t); //若是不为空获取Entry if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } //为空获取初始化的值 return setInitialValue(); }
这个方法主要根据当前线程获取ThreadLocalMap,若是还没初始化则调用setInitialValue()初始化并返回值,反之调用map.getEntry(this)获取值。
先来看下map不为空调用map.getEntry(this)的源码:
private Entry getEntry(ThreadLocal<?> key) { //1.快速hash获取hash地址 int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; //2.若是找到Entry,则返回 if (e != null && e.get() == key) return e; //3.若是未快速找到,则去遍历查找 else return getEntryAfterMiss(key, i, e); }
来看下getEntryAfterMiss(key, i, e)的源码:
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; //从i位置开始遍历table while (e != null) { ThreadLocal<?> k = e.get(); //若是找到直接返回 if (k == key) return e; //若是是脏槽清理之 if (k == null) expungeStaleEntry(i); //获取下个地址 else i = nextIndex(i, len); e = tab[i]; } return null; }
实现很简单就是从i位置开始遍历table,找到就返回Entry,遍历过程当中顺便清理脏槽。
再来看下setInitialValue()的源码:
private T setInitialValue() { //1.获取默认初始化值 T value = initialValue(); Thread t = Thread.currentThread(); //2.根据当前线程获取ThreadLocalMap ThreadLocalMap map = getMap(t); //3.不为空,设置值 if (map != null) map.set(this, value); //4.反之初始化map else createMap(t, value); return value; }
protected T initialValue() { return null; }
这个能够本身实现覆盖原来的方法。
直接看下remove()源码
public void remove() { //1.根据当前线程获取ThreadLocalMap ThreadLocalMap m = getMap(Thread.currentThread()); //2.若是map已经存在则调用m.remove(this)删除值 if (m != null) m.remove(this); }
下面来看下m.remove(this)的源码:
private void remove(ThreadLocal<?> key) { Entry[] tab = table; int len = tab.length; //快速hash到地址 int i = key.threadLocalHashCode & (len-1); //向后查找 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { //若是找到 if (e.get() == key) { //清理key e.clear(); //清理脏槽 expungeStaleEntry(i); return; } } }
实现很简单,先快速hash到地址i,而后从这个地址i日后查找key(包括地址i)直到槽为空,若是找到则清理之。