ThreadLocal基本使用及原理分析
阿新 • • 發佈:2022-03-03
# ThreadLocal基本使用及原理分析
學習ThreadLocal的基本使用以及瞭解其核心原理實現。jdk版本:1.8
@[toc]
## ThreadLocal介紹
> ### 執行緒程式介紹
>
> 早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal為解決多執行緒程式的併發問題提供了一種新的思路。使用這個工具類可以很簡潔地編寫出優美的多執行緒程式。
>
>
>
> ### 關於其變數
>
> ThreadLocal很容易讓人望文生義,想當然地認為是一個“本地執行緒”。其實,ThreadLocal並不是一個Thread,而是Thread的[區域性變數](https://baike.baidu.com/item/%E5%B1%80%E9%83%A8%E5%8F%98%E9%87%8F),也許把它命名為ThreadLocalVariable更容易讓人理解一些。
>
> 所以,在Java中編寫執行緒區域性變數的程式碼相對來說要笨拙一些,因此造成執行緒區域性變數沒有在Java開發者中得到很好的普及。
>
> **-- 摘要自百度百科**
## ThreadLocal使用
在ThreadLocal中,提供了三個核心方法,get、set和remove。通過get賦值、通過set獲取值、通過remove刪除,使用起來還是非常簡單的。
```java
public void contextLoads() throws IOException {
ThreadLocal threadLocal = new ThreadLocal<>();
threadLocal.set(1);
System.out.println(threadLocal.get());
threadLocal.remove();
System.out.println(threadLocal.get());
System.in.read();
}
```
接下來從原始碼的角度分析來了解ThreadLocal的核心原理。為什麼他是執行緒的區域性變數、怎麼做到執行緒獨佔的、會存在什麼問題。
### set
```java
public void set(T value) {
//通過currentThread獲取到當前執行執行緒
Thread t = Thread.currentThread();
//這裡的ThreadLocalMap當成普通的hashMap來理解
ThreadLocalMap map = getMap(t);
if (map != null)//不為null直接set值,為null則初始化
//key就是ThreadLocal物件本身
map.set(this, value);
else
createMap(t, value);
}
//初始化就是直接new了
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
```
set方法本身非常簡單,就是拿到當前執行緒的的ThreadLocalMap並賦值,因為key存的就是ThreadLocal物件本身,所以set方法不需要傳key。接下來在看一下get方法。
### get
```java
public T get() {
//通過currentThread獲取到當前執行執行緒
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {//如果map是null的情況下做了一下初始化,否則從map中獲取值,值本身存放在map.Entry中
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
//初始化操作
private T setInitialValue() {
//初始化值就是一個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;
}
```
get方法同樣簡單,未初始化的情況下線初始化返回null值,已經初始化的情況下從map中獲取值。這裡的獲取方式類似於1.8之前的hashMap,存放的是Entry陣列,通過ThreadLocal的hashcode & entry陣列的長度來拿到對應的下標並獲取值,後面在來分析這段。
### remove
```java
public void remove() {
//同樣拿到threadLocalMap執行刪除方法
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
```
三個方法分析完,可以得出,ThreadLocal就是拿到當前執行緒中的map來執行get、set和remove,key是ThreadLocal自身。可以發現貫穿流程的在於Thread的成員變數ThreadLocalMap,因此有必要了解一下關於ThreadLocalMap的實現。
### ThreadLocalMap
#### 定義
```java
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
//注意,這裡的entry繼承了弱引用
static class Entry extends WeakReference> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal k, Object v) {
//ThreadLocal key實際為entry的成員
super(k);
value = v;
}
}
/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
//資料儲存在entry陣列中
private Entry[] table;
/**
* The number of entries in the table.
*/
private int size = 0;
/**
* The next size value at which to resize.
*/
private int threshold; // Default to 0
}
```
ThreadLocalMap並沒有實現map的介面,自身是ThreadLocal的內部類,資料儲存在ThreadLocalMap的內部類Entry中,自身維護一個Entry陣列(型別1.8之前的hashMap)。在ThreadLocalMap中,是存在記憶體洩露的問題的,可以帶著這個問題來閱讀ThreadLocalMap的原始碼。
#### ThreadLocalMap.set
```java
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;
//類似hashMap獲取下標的方式,但是這裡是hashCode不是通過hashCode()方法獲取的
int i = key.threadLocalHashCode & (len-1);
//這裡是採用的開放定址法來解決的hash碰撞的問題。
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {//遍歷尋找到對應的entry並賦值
ThreadLocal k = e.get();
if (k == key) {//key相同(ThreadLocal相同)則直接賦值
e.value = value;
return;
}
if (k == null) {//如果key為null
//替換舊值
replaceStaleEntry(key, value, i);
return;
}
}
//如果tab[i]為null就直接建立一個entry並賦值了
tab[i] = new Entry(key, value);
int sz = ++size;
//cleanSomeSlots是為了清除為null的key,解決記憶體洩露的問題。
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
```
set的程式碼邏輯參考如上註釋,可以看到,這裡的資料結構也是採用的散列表的結構,而關於ThreadLocal的hashcode,採用的**Fibonacci Hashing**,具體可以去了解一下斐波那契雜湊的相關概念。採用了開放定址法來解決hash碰撞的問題,因為這裡的entry是弱引用的實現,因此為了優化可能出現的記憶體洩露問題,在set、replaceStaleEntry、cleanSomeSlots等方法處都會去清理這些**stale key**。找到對應的entry後,將value賦值給entry.value。
#### ThreadLocalMap.getEntry
```java
private Entry getEntry(ThreadLocal key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else //table[i]沒有找到或者不匹配的情況往後尋找
return getEntryAfterMiss(key, i, e);
}
```
get的程式碼邏輯其實同set方法的尋找類似,看懂了set方法,get方法其實沒有什麼難度。
#### ThreadLocalMap.remove
```java
/**
* Remove the entry for key.
*/
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)]) {
if (e.get() == key) {
//remove方法呼叫的時候,如果匹配到key的時候,清除stale的entry。
e.clear();
expungeStaleEntry(i);
return;
}
}
}
```
remove會主動清理stale的entry,因為entry是弱引用,所以在使用ThreadLocal的時候要主動去呼叫remove,這樣才能將對應的value移除,被GC回收。雖然在使用get、set方法時候也會cleanSomeSlots,但是需要觸發場景。
### 記憶體洩露問題
分析完ThreadLocal和ThreadLocalMap的程式碼後,可以發現如果在使用ThreadLocal的時候,不主動呼叫remove方法時,可能會出現ThreadLocalMap中entry的value無法被GC回收的問題。雖然ThreadLocalMap是thread的成員變數,會隨著thread的銷燬而被回收,但是在日常開發中,我們往往會用到執行緒池,對於核心執行緒並不會被回收而是重複使用,導致thread一直存活且thread的成員變數ThreadLocalMap一直存活。因此在不用ThreadLocal後,一定記得呼叫remove銷燬。