前言
本文將以一個例子開頭簡單看看
ThreadLocal
類的特性,進而分析該類的源代碼.
例子
啟動三個線程,每個線程的操作都是使用靜態變量
count
把原先的值加1
.
package com.com.example.threadlocal;
import java.util.concurrent.TimeUnit;
public class TestThreadLocal {
static ThreadLocal<Integer> count = new ThreadLocal<Integer>(){
protected Integer initialValue() {
return 100;
}
};
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 3; i++) {
new Thread(new Runner(), "thread-" + i).start();
TimeUnit.SECONDS.sleep(1);
}
}
static class Runner implements Runnable {
public void run() {
for (int i = 0; i < 3; i++) {
count.set(count.get() + 1);
System.out.println(Thread.currentThread().getName() + ":" +
count.get());
}
}
}
}
結果如下: 可以看到每個線程都有單獨的一個
count
實例一樣,這個就是threadlocal
的特性可以使得線程之間隔離,相當于每個線程自己保存了一份自己的數據副本,在本線程中操作只會改變當前線程的值并不會影響其他線程的值.
thread-0:101
thread-0:102
thread-0:103
thread-1:101
thread-1:102
thread-1:103
thread-2:101
thread-2:102
thread-2:103
類圖
下面的圖是整個
ThreadLocal
涉及到的所有類. 接下來通過該圖理解一下整體的操作.
threadlocal.png
實現思路: 每個線程實體類中都保存著一個
ThreadLocal.ThreadLocalMap
用于存放該線程中所有的映射關系, 這個映射關系是由threadlocal
類和初始化的value
對應并放在ThreadLocalMap.Entry
類中存放.
對上面的例子而言:thread-1.threadlocals
存放了[count, 100]
的映射關系,thread-2
和thread-3
各自的threadlocals
都存放著[count, 100]
的映射關系. 所以當thread-1
運行run
方法時循環了三次操作后thread-1.threadlocals
存放了[count,103]
,而hread-2
和thread-3
則保存不變.
1. 通過類ThreadLocal中的構造方法
threadlocal()
生成對象.
2. 通過initialValue
初始化value
值.
3.ThreadLocal
類的set
,get
,remove
操作都是調用的ThreadLocalMap
的方法進行操作, 因為ThreadLocalMap
定義了這些邏輯的核心實現, 那ThreadLocal
類的方法做了什么呢? 主要是為了獲取當前線程的ThreadLocalMap
對象, 即當前線程的成員變量threadlocals
, 通過該變量進行真正的邏輯操作.
所以接下來我們將簡單看看
ThreadLocal
的方法, 重點分析的是ThreadLocalMap
類.
ThreadLocal中的set, get, remove
set
方法的邏輯
// 獲取線程t的ThreadLocalMap對象
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
// 根據當前的ThreadLocal對象和firstValue為線程t的成員變量threadlocas生成一個ThreadLocalMap對象
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
/**
* 1. 獲取當前線程
* 2. 如果當前線程的ThreadLocalMap對象threadlocals已經存在,則直接調用ThreadLocalMap類的set方法
* 3. 如果不存在,則創建一個ThreadLocalMap對象
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
get
方法的邏輯
// 設置初始化值并返回初始值
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
// 取當前線程的ThreadLocalMap對象
// 如果不存在或者不存在當前ThreadLocal對象不在ThreadLocalMap中,調用setInitialValue()返回初始值
// 反之則返回當前線程的ThreadLocalMap對象中當前ThreadLocal對象對應的value值
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
remove
方法的邏輯
// 獲取當前線程的ThreadLocalMap對象,如果不為空,則當前線程的ThreadLocalMap對象
//中的當前ThreadLocal對象所對應的節點.
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
ThreaLocalMap中的方法
ThreadLocalMap是整個ThreadLocal的核心部分. 由于我把ThreadLocalMap的源碼單獨拿了出來(源碼下載),接下來先由一個小例子簡單測試一下.
在測試之前需要先看一下
ThreadLocalMap
中的Entry
類繼承了WeakReference
類,請注意Entry
的key
也就是ThreadLocal
對象是采用弱引用的方法,而value
還是一個強引用. 關于弱引用可以關注我的另一個博客通過例子理解java強引用,軟引用,弱引用,虛引用
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
下面的例子是生成了9(10的時候數組會擴展)個ThreadLocal對象并且全部都set到了ThreadLocalMap對象中,然后打印一下整個數組中的情況. 因為每個tls[i]對應的對象現在都有兩個引用,1個強引用tls[i]和一個弱引用在某個entry[h]里面.所以我做了個簡單測試把tls[4]這個強引用去除,然后主動調用
gc
后再次打印觀察數組情況.
public static void test_2() {
ThreadLocal<Integer> [] tls = new ThreadLocal[9];
for (int i = 0; i < tls.length; i++) {
tls[i] = new ThreadLocal<Integer>();
}
ThreadLocalMap map = new ThreadLocalMap(tls[0], 0);
for (int i = 1; i < 9; i++) {
System.out.print("i = " + i + ", hash = ");
map.set(tls[i], i);
}
map.printEntry();
tls[4] = null;
System.gc();
//map.set(tls[4], 4);
System.out.println("---------------------------------");
map.printEntry();
}
輸出: 被垃圾回收器回收了
i = 0, hash = 0 & (16 - 1) = 0
i = 1, hash = 1640531527 & (16 - 1) = 7
i = 2, hash = -1013904242 & (16 - 1) = 14
i = 3, hash = 626627285 & (16 - 1) = 5
i = 4, hash = -2027808484 & (16 - 1) = 12
i = 5, hash = -387276957 & (16 - 1) = 3
i = 6, hash = 1253254570 & (16 - 1) = 10
i = 7, hash = -1401181199 & (16 - 1) = 1
i = 8, hash = 239350328 & (16 - 1) = 8
table[0] = [com.sourcecode.threadlocal.ThreadLocal@6f94fa3e,0]
table[1] = [com.sourcecode.threadlocal.ThreadLocal@5e481248,7]
table[2] = null
table[3] = [com.sourcecode.threadlocal.ThreadLocal@66d3c617,5]
table[4] = null
table[5] = [com.sourcecode.threadlocal.ThreadLocal@63947c6b,3]
table[6] = null
table[7] = [com.sourcecode.threadlocal.ThreadLocal@2b193f2d,1]
table[8] = [com.sourcecode.threadlocal.ThreadLocal@355da254,8]
table[9] = null
table[10] = [com.sourcecode.threadlocal.ThreadLocal@4dc63996,6]
table[11] = null
table[12] = [com.sourcecode.threadlocal.ThreadLocal@d716361,4]
table[13] = null
table[14] = [com.sourcecode.threadlocal.ThreadLocal@6ff3c5b5,2]
table[15] = null
---------------------------------
table[0] = [com.sourcecode.threadlocal.ThreadLocal@6f94fa3e,0]
table[1] = [com.sourcecode.threadlocal.ThreadLocal@5e481248,7]
table[2] = null
table[3] = [com.sourcecode.threadlocal.ThreadLocal@66d3c617,5]
table[4] = null
table[5] = [com.sourcecode.threadlocal.ThreadLocal@63947c6b,3]
table[6] = null
table[7] = [com.sourcecode.threadlocal.ThreadLocal@2b193f2d,1]
table[8] = [com.sourcecode.threadlocal.ThreadLocal@355da254,8]
table[9] = null
table[10] = [com.sourcecode.threadlocal.ThreadLocal@4dc63996,6]
table[11] = null
table[12] = [null,4]
table[13] = null
table[14] = [com.sourcecode.threadlocal.ThreadLocal@6ff3c5b5,2]
table[15] = null
ThreadLocalMap 與 HashMap處理沖突不一樣, HashMap采用的是拉鏈法,而ThreadLocal采用的開放地址法, 每個ThreadLocal對象都有一個
threadLocalHashCode
通過nextHashCode()
每生成一個ThreadLocal
對象都在前面對象的threadLocalHashCode
基礎上加一個常量HASH_INCREMENT = 0x61c88647
, (從上面的例子中也可以看出來.)至于為什么?應該是hash沖突的比較少,具體為什么我也不太清楚.
插入或者更新操作 set(ThreadLocal<?> key, Object value)
/**
* 作用: 將key和value 插入(如果key不存在)或者更新(如果key存在)
* @param key 鍵
* @param value 值
*/
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
//System.out.format("%d & (%d - 1) = %d\n", key.threadLocalHashCode, len, i);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 如果key存在,則替換該值
if (k == key) {
e.value = value;
return;
}
/**
* 如果當前k過期,則調用replaceStaleEntry方法
* 無論key是否存在,都會保存在位置i,具體細節可以看replaceStaleEntry的注釋
*/
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
/**
* 有限次去查找過期節點并刪除過期節點,如果有刪除則返回
* 如果沒有刪除則判斷是否超過閥值
* 如果超過閥值則調用rehash函數.
*/
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
作用: 將key和value 插入(如果key不存在)或者更新(如果key存在)
對應流程圖如下
set.png
expungeStaleEntry(int staleSlot)
/**
*
* 作用: 從該索引staleSlot往下直到遇到null結束返回當前下標,遇到的過期元素tab[i]設置為null,遇到的正常節點做rehash.
* @param staleSlot 需要清理的位置, 一個已經確定過期的位置
* @return 返回從staleSlot位置開始第一個為entry值為null的位置
*/
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
/**
* 清除該staleSlot的值
*/
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
/**
* 從stateSlot 開始往下繼續搜索
* 1. 如果為null, 直接退出
* 2. 如果虛引用對應的key已經為null,也就是被垃圾回收器回收了,則清除該位置
* 3. 如果不是1或者2,表明該位置存著一個正常值,觀察是否需要rehash,因為取值的時候會方便
* 因為該類處理hash沖突使用的是:開放定址法
*/
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
/**
* 因為處理沖突使用的開放地址法, 現在已經刪除了一個位置,
* 并且該節點前面的節點有可能為null,因為k==null的時候會把tab[i]=null,
* 所以比如下次set操作對該key進行操作的時候就找不到該key,因為前面有null值,
* 會認為該key不存在,重新創建一個新的節點,因此會造成有兩個節點擁有同一個key.
*
* 所以需要進行rehash
*
* 因此之前有些位置因為沖突沒有存放到對應的hash值該有的位置,
* 所以下面的方法就是檢查并且把此對象存到對應的hash值的位置或者它的后面.
*/
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// 往下繼續尋找,值到找到為null的空位置,然后把只放進去
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
作用: 從該索引往下直到遇到null結束返回當前下標,遇到的過期元素tab[i]設置為null,遇到的正常節點做rehash.
為什么需要做rehash?
因為處理沖突使用的開放地址法, 現在已經刪除了一個位置, 并且該節點前面的節點有可能為null,因為k==null的時候(表明該節點已經過期)會把tab[i]=null, 所以比如下次set操作對該key進行操作的時候就找不到該key,因為前面有null值, 會認為該key不存在,然后重新創建一個新的節點,因此會造成有兩個節點擁有同一個key. 所以需要進行rehash.
對應流程圖如下
expungeStaleEntry.png
cleanSomeSlots
/**
*
* @param i 從該位置i的下一個位置開始
* @param n n >>>= 1決定嘗試的次數
* @return 返回是否有清除過陳舊的值
*/
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];
// 如果當前節點不為null,并且對應的key已經被垃圾回收器收集
if (e != null && e.get() == null) {
// 重新設置n 和 設置removed標志位為true
n = len;
removed = true;
// 清除陳舊的位置節點i, 并設置i為當前i下一個位置開始第一個為entry值為null的位置
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
作用:
1. 盡可能多的刪除過期的節點.
2. 檢查次數由n
決定. 為logn
或者n
.
3. 返回是否有刪除過期元素
對應流程圖如下
cleanSomeSlots.png
replaceStaleEntry
/**
*
* 將set操作期間遇到的過期節點替換為指定鍵的節點。
* 無論指定鍵的節點是否已存在,value參數中傳遞的值都存儲在節點中。
* 作為副作用,此方法將清除包含過期節點的“run”中的所有過期節點。 (run是兩個空槽之間的一系列節點。)
*
*
* @param key 節點的鍵
* @param value 節點的值
* @param staleSlot 在尋找key過程中遇到的第一個過期的節點
*/
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
/**
* 備份以檢查當前"run"中的先前失效節點。
* 我們一次清理整個"run",以避免由于垃圾收集器釋放串聯的refs(即,每當收集器運行時)不斷的增量重復。
* slotToExpung 始終代表著整個run里面的第一個過期節點.
*/
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
/**
* 尋找"run"中的key 或者第一個空節點(null)
* 1. 找到key的位置i,就交換tab[i]和tab[staleSlot],提高查找時候的命中率
* 2. 如果找到一個空節點,就表示該key之前沒有插入到該tab中過,跳出循環后創建一個新的節點(key,value)
*/
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
/**
* 如果我們找到鍵,那么我們需要將它與陳舊條目交換以維護哈希表順序。
* 新陳舊的插槽或任何其他過期的插槽,在它上面遇到,然后可以發送到expungeStaleEntry
* 刪除或重新運行run中的所有其他條目。
*
*/
if (k == key) {
e.value = value; // 替換value
tab[i] = tab[staleSlot]; // 交換
tab[staleSlot] = e;
/**
* 如果slotToExpunge == staleSlot
* 表明當前的i是整個run里面的第一個過期的元素節點,更新一下slotToExpunge即可.
*/
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
/**
* 如果在反向掃描中找不到過期的節點, 那么在掃描key是看到的
* 第一個過期節點就是整個run里面的過期節點
*/
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
/**
* 如果key沒有找到,表明該key是第一次存入到該table中,
* 則生成一個新的節點并放到staleSlot的位置.
*/
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
/**
* 如果staleSlot不是該run里面的唯一一個過期節點,
* 則都需要進行清除工作
*/
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
作用: 替代原有key的節點或者新生成節點后做清除過期節點操作.
1. 定義了slotToExpunge
為整個run
(run是兩個空槽之間的一系列節點.)里面的第一個過期節點.
2. 查找該key
是否之前有存入過. 如果存在則存入到位置staleSlot
并清除從slotToExpunge
開始該run
里面的過期元素.
3. 如果不存在該key
則創建一個新節點并放到位置staleSlot
,如果staleSlot
是整個run
里面的唯一一個過期節點,則不需要清除,否則需要清除從slotToExpunge
開始該run
里面的過期元素.
詳細操作可以看代碼注釋和下面的流程圖.
對應流程圖如下
replaceStaleEntry.png
rehash, resize, expungeStaleEntries
/**
* 作用:
* 1. 先對整個數組的過期節點進行清除
* 2. 判斷是否需要對數組進行擴展
*/
private void rehash() {
/**
* 先對整個數組的過期節點進行清除
*/
expungeStaleEntries();
/**
* size >= 0.75 * threshold 則擴大容量
*/
if (size >= threshold - threshold / 4)
resize();
}
/**
* 作用: 擴展數組
* size擴大兩倍, 每一個正常的元素做rehash映射到新的數組中
* 每一個過期的元素的value都設置為null方便gc
*/
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];
if (e != null) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null; // Help the GC
} else {
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
setThreshold(newLen);
size = count;
table = newTab;
}
/**
* 作用: 從頭到尾掃描整個數組對所有過期節點做清理工作
*/
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);
}
}
取值操作 getEntry, getEntryAfterMiss
/**
* 作用: 先用hash定位尋找key,如果找到key 返回該節點
* 如果沒有找到key 返回getEntryAfterMiss(key, i, e)的結果
*/
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1); //計算hash值
Entry e = table[i];
if (e != null && e.get() == key) // 如果命中
return e;
else
return getEntryAfterMiss(key, i, e);
}
/**
* 作用: 利用開發地址法尋找key,如果找到key 返回該節點
* 如果沒有找到key 返回null
*/
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key) //找到key存在的位置,直接返回節點
return e;
/**
* 如果當前節點的key過期,則調用expungeStaleEntry(i)進行清理當前位置
* 并且不接受返回值,i 沒有發生變化
*
* 如果不過期則取下一個節點
*/
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
作用: 根據
key
獲得對應的節點,如果不存在則返回null
.
對應流程圖如下
getEntry.png
刪除操作 remove
/**
*
* 作用: 刪除key,調用了expungeStaleEntry(i)做清除和rehash工作,
*
* @param key 要刪除的鍵值
*/
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1); //獲取hash值, 如果不在該位置則繼續往下找直到遇到null
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i); //做清除工作
return;
}
}
}
作用: 刪除該key所對應的節點.
為什么需要調用expungeStaleEntry(i)?
這是因為在清除了位置i后, 整個
run
(run是兩個空槽之間的一系列節點.)該位置會變為null,因此位置i后面的節點需要做rehash
, 這是因為該數組處理hash沖突采用的是開放地址法,因此后面的節點的hash值有可能不在它本身所處的位置, 如果后面的某一個節點K本身的hash值在i前面(比如i-1,還是在整個run里面), 那么后續操作在對節點K更新或者獲取操作時就會找不到節點K, 因為取hash值是i-1,檢查到i時發現已經為null,所以會認為該節點K不存在. 所以可以看到整個ThreadLocalMap
對過期元素做刪除操作都是調用expungeStaleEntry(i)方法.
關于為什么使用弱引用和內存泄露的問題?
可以參考該文章: 深入分析 ThreadLocal 內存泄漏問題
和 深入分析 ThreadLocal 內存泄漏問題