前面我們學習的線程并發時的同步控制,是為了保證多個線程對共享資料争用時的正确性的。那如果一個操作本身不涉及對共享資料的使用,相反,隻是希望變量隻能由建立它的線程使用(即線程隔離)就需要到線程本地存儲了。
Java 通過 ThreadLocal 提供了程式對線程本地存儲的使用。
通過建立 ThreadLocal 類的執行個體,讓我們能夠建立隻能由同一線程讀取和寫入的變量。是以,即使兩個線程正在執行相同的代碼,并且代碼引用了相同名稱的 ThreadLocal 變量,這兩個線程也無法看到彼此的存儲在 ThreadLocal 裡的值。否則也就不能叫線程本地存儲了。
本文大綱如下:
ThreadLocal
ThreadLocal 是 Java 内置的類,全稱 java.lang.ThreadLoal, java.lang 包裡定義的類和接口在程式裡都是可以直接使用,不需要導入的。
ThreadLocal 的類定義如下:
public class ThreadLocal<T> {
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
//......
return setInitialValue();
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
m.remove(this);
}
}
protected T initialValue() {
return null;
}
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
return new SuppliedThreadLocal<>(supplier);
}
// ...
}
複制代碼
上面隻是列出了 ThreadLocal類裡我們經常會用到的方法,這幾個方法他們的說明如下。
- T get()- 用于擷取 ThreadLocal 在目前線程中儲存的變量副本。
- void set(T value) - 用于向ThreadLocal中設定目前線程中變量的副本。
- void remove() - 用于删除目前線程儲存在ThreadLocal中的變量副本。
- initialValue() - 為 ThreadLocal 設定預設的 get方法擷取到的始值,預設是 null ,想修改的話需要用子類重寫 initialValue 方法,或者是用TheadLocal提供的withInitial方法 。
下面我們詳細看一下 ThreadLocal 的使用。
建立和讀寫 ThreadLocal
通過上面 ThreadLocal 類的定義我們能看出來, ThreadLocal 是支援泛型的,是以在建立 ThreadLocal 時沒有什麼特殊需求的情況下,我們都會為其提供類型參數,這樣在讀取使用 ThreadLocal 變量時就能免去類型轉換的操作。
private ThreadLocal threadLocal = new ThreadLocal();
threadLocal.set("A thread local value");
// 建立時沒有使用泛型指定類型,預設是 Object
// 使用時要先做類型轉換
String threadLocalValue = (String) threadLocal.get();
複制代碼
上面這個例子,在建立 ThreadLocal 時沒有使用泛型指定類型,是以存儲在其中的值預設是 Object 類型,這樣就需要在使用時先做類型轉換才行。
下面再看一個使用泛型的版本
private ThreadLocal<String> myThreadLocal = new ThreadLocal<String>();
myThreadLocal.set("Hello ThreadLocal");
String threadLocalValue = myThreadLocal.get();
複制代碼
現在我們隻能把 String 類型的值存到 ThreadLocal 中,并且從 ThreadLocal 讀取出值後也不再需要進行類型轉換。
關于泛型使用方面的詳細講解,可以看本系列中的泛型章節。
看了這篇Java 泛型通關指南,再也不怵滿屏尖括号了
想要删除一個 ThreadLocal 執行個體裡存儲的值,隻需要調用ThreadLocal執行個體中的 remove 方法即可。
myThreadLocal.remove();
複制代碼
當然,這個删除操作隻是删除的變量在本地線程中的副本,其他線程不會受到本線程中删除操作的影響。下面我們把 ThreadLocal 的建立、讀寫和删除攢一個簡單的例子,做下示範。
// 源碼: https://github.com/kevinyan815/JavaXPlay/blob/main/src/com/threadlocal/ThreadLocalExample.java
package com.threadlocal;
public class ThreadLocalExample {
private ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
private void setAndPrintThreadLocal() {
threadLocal.set((int) (Math.random() * 100D) );
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println( Thread.currentThread().getName() + ": " + threadLocal.get() );
if ( threadLocal.get() % 2 == 0) {
// 測試删除 ThreadLocal
System.out.println(Thread.currentThread().getName() + ": 删除ThreadLocal");
threadLocal.remove();
}
}
public static void main(String[] args) throws InterruptedException {
ThreadLocalExample tlExample = new ThreadLocalExample();
Thread thread1 = new Thread(() -> tlExample.setAndPrintThreadLocal(), "線程1");
Thread thread2 = new Thread(() -> tlExample.setAndPrintThreadLocal(), "線程2");
thread1.start();
thread2.start();
thread1.join();
thread2.join();
}
}
複制代碼
上面的例程會有如下輸出,當然如果恰好兩個線程裡 ThreadLocal 變量裡存儲的都是偶數的話,就不會有第三行輸出啦。
線程2: 97
線程1: 64
線程1: 删除ThreadLocal
複制代碼
本例子的源碼項目放在了GitHub上,需要的可自行取用進行參考:ThreadLocal變量操作示例--增删查
為 ThreadLocal 設定初始值
在程式裡,聲明ThreadLocal類型的變量時,我們可以同時為變量設定一個自定義的初始值,這樣做的好處是,即使沒有使用 set 方法給 ThreadLocal 變量設定值的情況下,調用ThreadLocal變量的 get() 時能傳回一個對業務邏輯來說更有意義的初始值,而不是預設的 Null 值。
在 Java 中有兩種方式可以指定 ThreadLocal 變量的自定義初始值:
- 建立一個 ThreadLocal 的子類,覆寫 initialValue() 方法,程式中則使用ThreadLocal子類建立執行個體變量。
- 使用 ThreadLocal 類提供的的靜态方法 withInitial(Supplier<? extends S> supplier) 來建立 ThreadLocal 執行個體變量,該方法接收一個函數式接口 Supplier 的實作作為參數,在 Supplier 實作中為 ThreadLocal 設定初始值。
關于函數式接口Supplier如果你還不太清楚的話,可以檢視系列中函數式程式設計接口章節中的詳細内容。下面我們看看分别用這兩種方式怎麼給 ThreadLocal 變量提供初始值。
使用子類覆寫 initialValue() 設定初始值
通過定義ThreadLocal 的子類,在子類中覆寫 initialValue() 方法的方式給 ThreadLocal 變量設定初始值的方式,可以使用匿名類,簡化建立子類的步驟。
下面我們在程式裡建立 ThreadLocal 執行個體時,直接使用匿名類來覆寫 initialValue() 方法的一個例子。
public class ThreadLocalExample {
private ThreadLocal threadLocal = new ThreadLocal<Integer>() {
@Override protected Integer initialValue() {
return (int) System.currentTimeMillis();
}
};
......
}
複制代碼
有同學可能會問,這塊能不能用 Lambda 而不是用匿名類,答案是不能,在這個專欄講 Lambda 的文章中我們說過,Lambda 隻能用于實作函數式接口(接口中有且隻有一個抽象方法,是以這裡隻能使用匿名了簡化建立子類的步驟,不過另外一種通過withInitial方法建立并自定義初始化ThreadLocal變量的時候,是可以使用Lambda 的,我們下面看看使用 withInital 靜态方法設定 ThreadLocal 變量初始值的示範。
通過 withInital 靜态方法設定初始值
為 ThreadLocal 執行個體變量指定初始值的第二種方式是使用 ThreadLocal 類提供的靜态工廠方法 withInitial 。withInitial 方法接收一個函數式接口 Supplier 的實作作為參數,在 Supplier 的實作中我們可以為要建立的 ThreadLocal 變量設定初始值。
Supplier 接口是一個函數式接口,表示提供某種值的函數。 Supplier 接口也可以被認為是工廠接口。
@FunctionalInterface public interface Supplier { T get(); }
下面的程式裡,我們用 ThreadLocal 的 withInitial 方法為 ThreadLocal 執行個體變量設定了初始值
public class ThreadLocalExample {
private ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(new Supplier<Integer>() {
@Override
public String get() {
return (int) System.currentTimeMillis();
}
});
......
}
複制代碼
對于函數式接口,理所當然會想到用 Lambda 來實作。上面這個 withInitial 的例子用 Lambda 實作的話能進一步簡化成:
public class ThreadLocalExample {
private ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> (int) System.currentTimeMillis());
......
}
複制代碼
關于 Lambda 和 函數式接口 Supplier 的詳細内容,可以通過本系列中與這兩個主題相關的文章進行學習。
Java Lambda 表達式的各種形态和使用場景,看這篇就夠了 Java 中那些繞不開的内置接口 -- 函數式程式設計和 Java 的内置函數式接口
ThreadLocal 在父子線程間的傳遞
ThreadLocal 提供的線程本地存儲,給資料提供了線程隔離,但是有的時候用一個線程開啟的子線程,往往是需要些相關性的,那麼父線程的ThreadLocal中存儲的資料能在子線程中使用嗎?答案是不行......那怎麼能讓父子線程上下文能關聯起來,Java 為這種情況專門提供了InheritableThreadLocal 給我們使用。
InheritableThreadLocal 是 ThreadLocal 的一個子類,其定義如下:
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
protected T childValue(T parentValue) {
return parentValue;
}
/**
* Get the map associated with a ThreadLocal.
*
* @param t the current thread
*/
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
/**
* Create the map associated with a ThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the table.
*/
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
複制代碼
與 ThreadLocal 讓線程擁有變量在本地存儲的副本這個形式不同的是,InheritableThreadLocal 允許讓建立它的線程和其子線程都能通路到在它裡面存儲的值。
下面是一個 InheritableThreadLocal 的使用示例
// 源碼: https://github.com/kevinyan815/JavaXPlay/blob/main/src/com/threadlocal/InheritableThreadLocalExample.java
package com.threadlocal;
public class InheritableThreadLocalExample {
public static void main(String[] args) {
ThreadLocal<String> threadLocal = new ThreadLocal<>();
InheritableThreadLocal<String> inheritableThreadLocal =
new InheritableThreadLocal<>();
Thread thread1 = new Thread(() -> {
System.out.println("===== Thread 1 =====");
threadLocal.set("Thread 1 - ThreadLocal");
inheritableThreadLocal.set("Thread 1 - InheritableThreadLocal");
System.out.println(threadLocal.get());
System.out.println(inheritableThreadLocal.get());
Thread childThread = new Thread( () -> {
System.out.println("===== ChildThread =====");
System.out.println(threadLocal.get());
System.out.println(inheritableThreadLocal.get());
});
childThread.start();
});
thread1.start();
Thread thread2 = new Thread(() -> {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("===== Thread2 =====");
System.out.println(threadLocal.get());
System.out.println(inheritableThreadLocal.get());
});
thread2.start();
}
}
複制代碼
運作程式後,會有如下輸出
===== Thread 1 =====
Thread 1 - ThreadLocal
Thread 1 - InheritableThreadLocal
===== ChildThread =====
null
Thread 1 - InheritableThreadLocal
===== Thread2 =====
null
null
複制代碼
這個例程中建立了分别建立了 ThreadLocal 和 InheritableThreadLocal的 執行個體,然後例程中建立的線程Thread1, 線上程 Thread1中向 ThreadLocal 和 InheritableThreadLocal 執行個體中都存儲了資料,并嘗試在開啟了的子線程 ChildThread 中通路這兩個資料。按照上面的解釋,ChildThread 應該隻能通路到父線程存儲在 InheritableThreadLocal 執行個體中的資料。
在例程的最後,程式又建立了一個與 Thread1 不相幹的線程 Thread2, 它在通路 ThreadLocal 和 InheritableThreadLocal 執行個體中存儲的資料時,因為它自己沒有設定過,是以最後得到的結果都是 null。
ThreadLocal 的實作原理
梳理完 ThreadLocal 相關的常用功能都怎麼使用後,我們再來簡單過一下 ThreadLocal 在 Java 中的實作原理。
在 Thread 類中維護着一個 ThreadLocal.ThreadLocalMap 類型的成員變量threadLocals。這個成員變量就是用來存儲目前線程獨占的變量副本的。
public class Thread implements Runnable {
// ...
ThreadLocal.ThreadLocalMap threadLocals = null;
// ...
}
複制代碼
ThreadLocalMap類 是 ThreadLocal 中的靜态内部類,其定義如下。
package java.lang;
public class ThreadLocal<T> {
// ...
static class ThreadLocalMap {
// ...
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
// ...
}
}
複制代碼
它維護着一個 Entry 數組,Entry 繼承了 WeakReference ,是以是弱引用。 Entry 用于儲存鍵值對,其中:
- key 是 ThreadLocal 對象;
- value 是傳遞進來的對象(變量副本)。
ThreadLocalMap 雖然是類似 HashMap 結構的資料結構,但它解決哈希碰撞的時候,使用的方案并非像 HashMap 那樣使用拉鍊法(用連結清單儲存沖突的元素)。
實際上,ThreadLocalMap 采用了線性探測的方式來解決哈希碰撞沖突。所謂線性探測,就是根據初始 key 的 hashcode 值确定元素在哈希表數組中的位置,如果發現這個位置上已經被其他的 key 值占用,則利用固定的算法尋找一定步長的下個位置,依次判斷,直至找到能夠存放的位置。
總結
關于 ThreadLocal 的内容就介紹到這了,這塊内容在一些基礎的面試中還是挺常被問到的,與它一起經常被問到的還有一個 volatile 關鍵字,這部分内容我們放到下一篇再講,喜歡本文的内容還請給點個贊,點個關注,這樣就能及時跟上後面的更新啦。
引用連結
- Java并發程式設計--多線程間的同步控制和通信
- 看了這篇Java 泛型通關指南,再也不怵滿屏尖括号了
- Java Lambda 表達式的各種形态和使用場景,看這篇就夠了
- Java 中那些繞不開的内置接口 -- 函數式程式設計和 Java 的内置函數式接口
- ThreadLocal變量操作示例--增删查源代碼
原文連結:https://juejin.cn/post/7170614613683175454
來源:稀土掘金