前面我們學習的線程並發時的同步控制,是為了保證多個線程對共享數據爭用時的正確性的。那如果一個操作本身不涉及對共享數據的使用,相反,只是希望變量只能由創建它的線程使用(即線程隔離)就需要到線程本地存儲了。
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
來源:稀土掘金