01.28 聊聊面試中的 ThreadLocal 原理和使用場景

相信大家不管是在網上做題還是在面試中都經常被問過 ThreadLocal 的原理和用法,雖然一直知道這個東西的存在但是一直沒有好好的研究一下原理,沒有自己的知識體系。今天花點時間好好學習了一下,分享給有需要的朋友。

ThreadLocal 是什麼

ThreadLocal 是 JDK java.lang 包中的一個用來實現相同線程數據共享不同的線程數據隔離的一個工具。 我們來看下 JDK 源碼中是如何解釋的:

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).

Each thread holds an implicit reference to its copy of a thread-local variable as long as the thread is alive and the ThreadLocal instance is accessible; after a thread goes away, all of its copies of thread-local instances are subject to garbage collection (unless other references to these copies exist).

大致的意思是

ThreadLocal 這個類提供線程局部變量,這些變量與其他正常的變量的不同之處在於,每一個訪問該變量的線程在其內部都有一個獨立的初始化的變量副本;ThreadLocal 實例變量通常採用private static 在類中修飾。

只要 ThreadLocal 的變量能被訪問,並且線程存活,那每個線程都會持有 ThreadLocal 變量的副本。當一個線程結束時,它所持有的所有 ThreadLocal 相對的實例副本都可被回收。

一句話說就是 ThreadLocal 適用於每個線程需要自己獨立的實例且該實例需要在多個方法中被使用(相同線程數據共享),也就是變量在線程間隔離(不同的線程數據隔離)而在方法或類間共享的場景。

ThreadLocal 使用

我們先通過兩個例子來看一下 ThreadLocal 的使用

例子 1 普通變量

<code>
import java.util.concurrent.CountDownLatch;


public class MyStringDemo {
private String string;

private String getString() {
return string;
}

private void setString(String string) {
this.string = string;
}

public static void main(String[] args) {
int threads = 9;
MyStringDemo demo = new MyStringDemo();
CountDownLatch countDownLatch = new CountDownLatch(threads);
for (int i = 0; i < threads; i++) {
Thread thread = new Thread(() -> {
demo.setString(Thread.currentThread().getName());
System.out.println(demo.getString());
countDownLatch.countDown();
}, "thread - " + i);
thread.start();
}

}

}


/<code>

程序的運行的隨機結果如下:

<code>
thread - 1
thread - 2
thread - 1
thread - 3
thread - 4
thread - 5
thread - 6
thread - 7
thread - 8


Process finished with exit code 0

/<code>

從結果我們可以看出多個線程在訪問同一個變量的時候出現的異常,線程間的數據沒有隔離。下面我們來看下采用 ThreadLocal 變量的方式來解決這個問題的例子。

例子 2 ThreadLocal 變量

<code>
import java.util.concurrent.CountDownLatch;


public class MyThreadLocalStringDemo {
private static ThreadLocal<string> threadLocal = new ThreadLocal<>();

private String getString() {
return threadLocal.get();
}

private void setString(String string) {
threadLocal.set(string);
}

public static void main(String[] args) {
int threads = 9;
MyThreadLocalStringDemo demo = new MyThreadLocalStringDemo();
CountDownLatch countDownLatch = new CountDownLatch(threads);
for (int i = 0; i < threads; i++) {
Thread thread = new Thread(() -> {
demo.setString(Thread.currentThread().getName());
System.out.println(demo.getString());
countDownLatch.countDown();
}, "thread - " + i);
thread.start();
}
}

}


/<string>/<code>

程序運行結果

<code>thread - 0
thread - 1
thread - 2
thread - 3
thread - 4
thread - 5
thread - 6
thread - 7
thread - 8

Process finished with exit code 0

/<code>

從結果來看,這次我們很好的解決了多線程之間數據隔離的問題,十分方便。

這裡可能有的朋友會覺得在例子 1 中我們完全可以通過加鎖來實現這個功能。是的沒錯,加鎖確實可以解決這個問題,但是在這裡我們強調的是線程數據隔離的問題,並不是多線程共享數據的問題。假如我們這裡除了getString() 之外還有很多其他方法也要用到這個 String,這個時候各個方法之間就沒有顯式的數據傳遞過程了,都可以直接中 ThreadLocal 變量中獲取,這才是 ThreadLocal 的核心,相同線程數據共享不同的線程數據隔離

由於ThreadLocal 是支持泛型的,這裡採用的是存放一個 String 來演示,其實可以存放任何類型,效果都是一樣的。

ThreadLocal 源碼分析

在分析源碼前我們明白一個事那就是對象實例與 ThreadLocal 變量的映射關係是由線程 Thread 來維護的對象實例與 ThreadLocal 變量的映射關係是由線程 Thread 來維護的對象實例與 ThreadLocal 變量的映射關係是由線程 Thread 來維護的。重要的事情說三遍。換句話說就是對象實例與 ThreadLocal 變量的映射關係是存放的一個 Map 裡面(這個 Map 是個抽象的 Map 並不是 java.util 中的 Map ),而這個 Map 是 Thread 類的一個字段!而真正存放映射關係的 Map 就是 ThreadLocalMap。下面我們通過源碼的中幾個方法來看一下具體的實現。

<code>
//set 方法
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

//獲取線程中的ThreadLocalMap 字段!!
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

//創建線程的變量
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}


/<code>

在 set 方法中首先獲取當前線程,然後通過 getMap 獲取到當前線程的 ThreadLocalMap 類型的變量 threadLocals,如果存在則直接賦值,如果不存在則給該線程創建 ThreadLocalMap 變量並賦值。賦值的時候這裡的 this 就是調用變量的對象實例本身。

<code>
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();
}


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;
}

/<code>

get 方法也比較簡單,同樣也是先獲取當前線程的 ThreadLocalMap 變量,如果存在則返回值,不存在則創建並返回初始值。

ThreadLocalMap 源碼分析

ThreadLocal 的底層實現都是通過 ThreadLocalMap 來實現的,我們先看下 ThreadLocalMap 的定義,然後再看下相應的 set 和 get 方法。

<code>
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.
*/
static class Entry extends WeakReference<threadlocal>> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal> k, Object v) {
super(k);
value = v;
}
}

/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
}

/<threadlocal>/<code>

ThreadLocalMap 中使用 Entry[] 數組來存放對象實例與變量的關係,並且實例對象作為 key,變量作為 value 實現對應關係。並且這裡的 key 採用的是對實例對象的弱引用,(因為我們這裡的 key 是對象實例,每個對象實例有自己的生命週期,這裡採用弱引用就可以在不影響對象實例生命週期的情況下對其引用)。

<code>private void set(ThreadLocal> key, Object value) {

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)]) {
ThreadLocal> k = e.get();

//k 相等則覆蓋舊值
if (k == key) {
e.value = value;
return;
}

//此時說明此處 Entry 的 k 中的對象實例已經被回收了,需要替換掉這個位置的 key 和 value
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}

//創建 Entry 對象
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}


//獲取 Entry
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
return getEntryAfterMiss(key, i, e);
}


/<code>

至此我們看完了 ThreadLocal 相關的 JDK 源碼,我自己也有了更深入的瞭解,也希望能幫助到大家。

小結

在平時忙碌的工作中我們經常解決的是一個業務的需求,往往很少會涉及到底層的源碼或者框架的具體實現代碼。 其實這是很不好的,其實很多的東西的原理都是一樣的,我們需要經常去看一下源碼,瞭解一些底層的實現,不能總是停留在表層,代碼看到多了,才能寫出好的代碼,並且還能學到很多東西。 隨著我們知道的越來越多,我們會發現我們不知道的也越來越多。加油,共勉!


分享到:


相關文章: