1.ThreadLocal
1.1ThreadLocalの概要
これは、スレッド関連のデータを格納、取得、および削除するために使用されるツールクラスです。実際のスレッドプライベートデータは、このオブジェクトには格納されません。クラスには、配列で実装されたテーブルがThreadLocal
1つあります。クラスにはタイプのメンバー変数があります。ThreadLocalMap
Hash
Thread
ThreadLocalMap
実際、データを格納するときに、このThreadLocalMap
オブジェクトが作成され、このプロパティに割り当てられます。呼び出し元のThreadLocal
メソッドによって保存されたデータは、実際にはここに保存されます。また、このオブジェクトは現在のスレッドの内部オブジェクトであるため、実際、スレッドのプライベートデータを格納する場合は、オブジェクトではなく、現在のスレッドのオブジェクトに実際に格納されThreadLocal
ます。
1.2 ThreadLocalMap
ThreadLocalMap
配列で実装されたハッシュテーブルであることは誰もが知っていますが、どのように実装されていますか?その基礎となるデータ構造の実装を見てみましょう。
1.2.1ThreadLocalMapデータ構造
ThreadLocalMap
クラスはで定義され、このEntry
クラスのカプセル化はHash
テーブルのkey
合計ですvalue
。特定の実装では、ThreadLocal
オブジェクトはと見なされkey
、このThreadLocal参照は依然として弱参照であることがわかります。
ThreadLocalMap
最下層は、Entry
1つを実装するための配列を構築することHash表
です。このハッシュテーブルの初期容量は16です。
拡張しきい値は、配列の長さです2/3
。
1)では、なぜThreadLocal
それを弱参照として設定するのですか?
因为如果设置为强引用,当我们不再使用这个ThreadLocal
对象时吗,即使我们把栈中的指向ThreadLocal
对象的引用设置为null,我们还是不能回收这个对象。因为在堆中的还有一个Entry
中的key
引用,作为一个强引用指向这个对象。我们会在ThreadLocalMap
中维护一个Entry
数组来实现Hash
表,而ThreadLocalMap
对象的引用又在当期线程对象中。所以,在当前线程被销毁前,这部分数据会一直存在,这样就导致了内存泄露。
2)为什么不把value
设置为弱引用呢?
ThreadLocal
可以设置为弱引用,因为在使用过程中,还是有一个栈中的强引用对象指向堆中的ThreadLocal
对象。但是如果把value也设置为弱引用,在栈中却没有一个指向value
的强引用。也就是说,value
只有弱引用指向,那么只要一发生GC,即使你还在使用ThreadLocal
,那么value
也会被回收。
1.2.2 ThreadLocalMap代码分析
了解了上面这些知识,那么我们是怎么从这个Hash表中存取数据的呢?来看看它的get、set方法。
1)set方法
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
//通过数组长度减1和ThreadLocal对象的hashCode进行与运算得到一个数组下标
int i = key.threadLocalHashCode & (len-1);
//从定位到的下标开始遍历数组,直到数组中的entry = null为止
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//如果找到key相同的,替换value
if (k == key) {
e.value = value;
return;
}
//如果k=null,那么说明这个entry已经是无用数据了
if (k == null) {
//将过期的entry替换掉
replaceStaleEntry(key, value, i);
return;
}
}
//如果没有在数组中遍历到,新建一个entry加入到entry = null的位置。
tab[i] = new Entry(key, value);
//数组entry个数加1
int sz = ++size;
/*如果在进行启发式清理的时候没有清理掉过期的entry,并且数组个数已经大于扩容阈值,
那么就进行rehash
*/
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
复制代码
(1)ThreadLocalMap的hashCode
我们看到,ThreadLocalMap
是通过获取ThreadLocal的属性threadLocalHashCode
再与数组长度减1来得到下标的。那么这个hashCode是怎么实现的?
如上图,一开始nextHashCode是一个原子类对象,值为0。每次获取threadLocalHashCode
时都要调用nextHashCode
方法,让这个对象的值加上一个固定的hash增量。这个HASH_INCREMENT
值是一个特殊的值,他可以让数据在长度为2n的hash表中均匀分布。这样就尽量减少hash冲突
(2)replaceStaleEntry
我们看到,在我们遍历到一个过期的Entry
(即k=null的Entry)时,那么我们就要调用replaceStaleEntry
方法来替换掉过期的Entry
。 `
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
/*
从过期entry的下标向前遍历,直到entry为null的时候停止,记录找的的最后一个过期的entry
的下标。
这个prevIndex方法在到达下标0时会跳到 len-1,所以可能找到的最后一个过期entry在staleSlot
的后面
*/
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
/*
从staleSlot开始往后面找,直到遇到一个空entry
*/
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == key) {
/*
如果遇到一个相同的key,将其替换,然后交换当前entry和过期的entry的位置
*/
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
/*
如果在上一个遍历时没有找到过期entry,那么就将当前的i作为要消除的插槽
因为当前entry在交换位置有已经是过期entry了。
*/
if (slotToExpunge == staleSlot)
slotToExpunge = i;
//从当前的过期entry开始探测式清理,再进行启发式清理
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
//在清理过期数据后,执行结束
return;
}
/*
如果在向后遍历的过程中遇到一个过期entry,但是前面还有过期的,就表示要从前面开始
清理,而不是从后面
*/
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
//如果没有找到相同的key,在过期位置新new一个entry
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
//如果找到了一个新的过期数据,那么就进行清理
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}`
复制代码
(3)为什么要进行交换?
可以看到,我们在碰到一个拥有相同的key
的entry
的时候,不仅仅进行了值的覆盖,还进行了和过期数据位置的交换,这是为什么?
因为ThreadLocalMap
使用的是线性探测法来解决hash碰撞的,如果我们不把他们的位置进行交换,那么就会把这个过期数据清除,这个过期entry就变成的空entry。那么如果下一次插入的entry的key和这次插入的entry的key重复时,那么它就先会通过线性探测法找到这个空的位置,直接插入,那么在hash表中就会存在重复的key了。所以,我们就要对它们的位置进行交换,这是为了保证hash表的key的不重复性的。
(4)为什么还要向前找slotToExpunge?
使得后面在进行探测式清理时,探测到尽可能大的范围。
(5)探测式清理
在上面的代码注释中,我们提到了探测性清理,实际上expungeStaleEntry
实现的就是探测性清理。 `
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 因为staleSlot位置的entry过期了,所以,把它数据清除
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
//向后遍历知道遇到entry为null
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
//如果key为null,清除
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
//如果不为null,进行重hash,直到找到一个null的位置插入
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}`
复制代码
其实探测式清除的步骤就是:从过期entry开始向后探测并清除过期entry,并对没有过期的entry进行重新hash,进行位置调整,直到探测到entry为null,停止探测。
1.为什么要进行对遍历的不过期的entry调整位置?
其实这也是为了保证Hash表key的不重复性。如果不重新进行位置调整,那么在插入的key重复时,那么就会直接占据前面的为null的位置,后面可能还会存在重复的key。
2.为什么都把entry = null
作为循环停止条件?
因为使用的是线性探测法,并且还会调整entry的位置来保证key的唯一性,如果entry为null,那就意味着后面没有存在hash碰撞的数据了,存在hash碰撞的entry之间,一定没有null。
(6)启发式清理
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
//i向后遍历
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
//n = n>>>1,无符号右移,相当与n = n/2。
} while ( (n >>>= 1) != 0);
return removed;
}
复制代码
通过源码我们可以看到,启发式清理执行的并不是O(n)时间复杂度的清理,而是O(logn)时间复杂度的清理,从传入的i开始,如果找到一个过期的entry,那么就会调用探测式清理进行清理,并且循环n重新赋值为数组长度。
(7)rehash
如果插入数据后启发式清理没有进行并且现在元素个数大于扩容阈值,那么就会进行rehash。
rehash首先会调用expungeStaleEntries
方法,这个方法会将遍历整个hash表,将所有过期的entry全部删除,并把没有过期的数据全部重新hash定位。
如果在清除所有的过期entry后entry个数还是大于扩容阈值的3/4,那么就需要扩容,扩容为原来的2倍,重新设置扩容阈值。
(8)set方法总结
通过上面的分析,我们可以总结出set方法的执行流程:
-
先通过ThreadLocal的属性
threadLocalHashCode
与数组长度减1得到数组下标 -
从这个数组下标开始探测
-
如果最先找到一个重复的key,那么就直接替换这个entry的value,返回执行结束。
-
如果先找到一个过期的entry,那么就替换这个过期的entry,返回执行结束
- 如果在找到这个过期的entry后,找到重复的key,那么就覆盖value,交换位置。
- 如果最后还是找到了entry为null的位置,那么就直接new一个entry,将这个entry放在过期entry的地方。
- 一般设值完成后都会进行探测式清理和启发式清理
-
如果先遇到一个entry为null的地方,那么就new一个entry,把数据放在这个地方。
- 如果插入数据后启发式清理没有进行并且现在元素个数大于扩容阈值,那么就会进行rehash
- 在rehash时会对数组的所有entry都进行扫描,清理所有的过期entry,所有元素重新定位。
- 如果发现此时entry个数还是大于扩容阈值的3/4,那么就扩容到原来的两倍。
-
2)get方法
①也是通过threadLocalHashCode
变量得到一个下标,看key是否相等。如果key相等,直接返回这个entry。
②如果key不相等,那么就从当前的下标开始向后找,直到entry为null。
- 如果找到key相等,返回entry
- 如果找到过期entry,进行探测式清理。
- 如果没有找到,返回null。
3)remove方法
也是通过threadLocalHashCode
定位,从这个下标开始遍历,直到entry为null。如果找到目标,将弱引用删除,进行探测式清理。
1.3 ThreadLocal源码
1.3.1get方法
①得到当前线程对象,通过当前线程得到ThreadLocalMap
对象。
②マップがnullでない場合はThreadLocal
、現在のオブジェクトを介してエントリを取得し、返す値を取得します。
③マップがnullの場合はsetInitialValue
、メソッドを呼び出して戻り値を返します。
①initialValue
メソッドを介してThreadLocal
初期値を保存します。
②現在のスレッドオブジェクトを取得し、マップを取得します。マップがnullの場合は、新しいThreadLocalMap
オブジェクトを作成し、それをスレッドオブジェクトの変数に割り当ててデータを追加します。
③nullでない場合は、デフォルト値を設定します。
④デフォルト値に戻す
1.3.2setメソッド
①現在のスレッドオブジェクトを取得して、マップが存在するかどうかを判断します
②存在しない、地図を作成、データを追加
③存在し、直接データを追加
1.3.3削除方法
マップが存在するかどうかを判断するには、ThreadLocalMap
removeを呼び出して、データが存在する場合はそれを削除します。