滿懷憂思,不如先幹再說!做幹淨純粹的技術分享!歡迎評論區或私信交流!
[酷][酷][酷]ThreadLocal在工作和面試中都非常常見,本篇文章收錄于《Java并發程式設計》合集,通過2-3篇文章介紹清楚ThreadLocal的應用,原理,案例,達到實戰會用,面試會說的效果。通過本篇你可以得到[奸笑]:
- ThreadLocal的正确解釋和作用
- ThreadLocal的API以及三種初始化方式的對比
- 通過ThreadLocal實作春節紅包案例
- 通過線程池引出ThreadLocal隐患和解決方案
- 總結ThreadLocal特點和應用場景
喜歡的話記得動動手指點關注,升職加薪不迷路,長期穩定輸出幹貨技術文章
ThreadLocal
對于ThreadLocal的翻譯有很多,如:線程本地,本地線程,如果根據他的作用來說翻譯成【線程局部變量】我認為是比較合适的
ThreadLocal作用
ThreadLocal是java.lang包中的一個泛型類,可以實作為線程建立獨有變量,這個變量對于其他線程是隔離的,也就是線程本地的值,這也是ThreadLocal名字的來源
每個使用該變量的線程都要初始化一個完全獨立的執行個體副本,不存在多線程間共享的問題
ThreadLocal方法
ThreadLocal用來存儲目前線程的獨有資料,相關API就是存值,取值,清空值的簡單操作
- withInitial:建立一個ThreadLocal執行個體,并給定初始值【JDK8推出的新方法,一般都是用該方法初始化ThreadLocal】
- get:傳回目前線程ThreadLocal的值,如果沒有設定值傳回null
- set:設定目前線程ThreadLocal的值
- remove:删除目前線程ThreadLocal的值
- initialValue:此方法預設傳回null, 通過ThreadLocal構造方法初始化時一般重寫此方法,來設定初始值,在JDK8之後通過withInitial方法初始化
初始化ThreadLocal
初始化ThreadLocal有三種方式:
- 直接通過構造方法建立,此時初始值為null
- 通過構造方法同時重寫initialValue方法給定初始值
- 通過JDK8的withInitial()靜态方法建立,可以通過Lambda直接給初始值【推薦使用】
通過構造方法
@Test
public void test1() {
// 通過構造方法,初始化ThreadLocal
ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
// 未給定初始值,通過get方法擷取值為null
System.out.println(threadLocal.get());
}
通過get方法擷取結果為null
如果想要設定值,則需要通過set方法
@Test
public void test1() {
// 初始化ThreadLocal
ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
System.out.println("設定前===》" + threadLocal.get());
// 設定初始值
threadLocal.set(1);
System.out.println("設定後===》" + threadLocal.get());
}
此時擷取的值就是1
重寫initialValue方法
該方法的主要作用是傳回目前線程ThreadLocal的初始值,但是在ThreadLocal中預設實作傳回為null
這個值和具體的泛型類型有關,通常需要根據實際需求重寫此方法,定義初始值
@Test
public void test2() {
// 初始化ThreadLocal,重寫initialValue方法設定預設值
ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
// 設定,初始值為1024
return 1024;
}
};
// 啟動5個線程
for (int i = 0; i < 5; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + ":threadLocal初始值--->" + threadLocal.get());
},"線程:" + i).start();
}
}
啟動5個線程每個線程的初始值都為1024
withInitial靜态方法
上方兩種方案對建立ThreadLocal時給定初始值都稍顯繁瑣,在JDK8中新增了withInitial靜态方法接收Supplier供給型函數接口設定初始值
@Test
public void test3() {
// 通過withInitial方法設定初始值
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> {
return 100;
});
// 啟動5個線程
for (int i = 0; i < 5; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + ":threadLocal初始值--->" + threadLocal.get());
},"線程:" + i).start();
}
}
啟動5個線程之後初始值為100,強烈推薦此種方式來實作ThreadLocal的初始化
推薦通過withInitial靜态方法實作ThreadLocal的初始化
如果線程内需要修改值則可以使用set方法,如果需要擷取值則使用get方法
結合下圖搞懂ThreadLocal資料存儲:一個ThreadLocal執行個體,在每個線程中都有獨自的初始化副本,接下來每個線程對ThradLocal的操作都線上程内,對其他線程隔離
[吐舌]ThreadLocal的資料結構下一篇介紹,知道的小夥伴不妨寫在評論區
父母保管孩子春節紅包案例[白眼]
春節期間孩子收到的紅包都會被父母暫時保管,答應長大後歸還,比如,小翠有三個孩子小明,曉明和小茗,為了将來分賬,需要單獨記錄孩子們收到的紅包金額
@Test
public void test5() {
// 通過withInitial方法設定初始紅包為0
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
// 設定随機數
Random random = new Random();
// 通過map集合映射線程名字
Map<Integer, String> map = new HashMap<>();
map.put(0,"小明");
map.put(1,"曉明");
map.put(2,"小茗");
// 啟動3個線程,分别為3個孩子
for (int i = 0; i < 3; i++) {
new Thread(() -> {
// 去七大姑八大姨家拜年,比如5家親戚吧
for (int j = 0; j < 5; j++) {
// 每家親戚給随機的200以内的紅包
int yasuiqian = random.nextInt(200);
// 紅包金額加1
threadLocal.set(yasuiqian + threadLocal.get());
}
System.out.println(Thread.currentThread().getName() + "共收到:" + threadLocal.get() + "元紅包");
},map.get(i)).start();
}
}
三個孩子去5個親戚家,收到的紅包金額實作獨自記錄
線程池實作保管紅包
項目開發過程中對于多線程的場景都推薦使用線程池實作,可以避免線程頻繁建立和銷毀的資源浪費,使用線程池是一定要記得在finally代碼塊中關閉線程池
如下:開啟一個有三個核心線程的線程池來處理3個孩子的拜年領紅包任務
@Test
public void test6() {
// 通過withInitial方法設定初始值
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
// 設定随機數
Random random = new Random();
Map<Integer, String> map = new HashMap<>();
map.put(0,"小明");
map.put(1,"曉明");
map.put(2,"小茗");
// 開啟有3個核心線程的線程池
ExecutorService threadPool = Executors.newFixedThreadPool(3);
try {
// 3個孩子
for (int i = 0; i < 3; i++) {
// 記錄目前循環值,映射對應的孩子名字
int index = i;
threadPool.submit(() -> {
// 設定線程名:Thread.currentThread().getName()為預設的線程池給的名字,友善檢視是哪一個線程執行的此任務
Thread.currentThread().setName(Thread.currentThread().getName() + map.get(index));
// 去七大姑八大姨家拜年,比如5家親戚吧
for (int j = 0; j < 5; j++) {
// 每家親戚給随機的200以内的紅包
int yasuiqian = random.nextInt(200);
// 紅包金額加1
threadLocal.set(yasuiqian + threadLocal.get());
}
System.out.println(Thread.currentThread().getName() + "共收到:" + threadLocal.get() + "元紅包");
});
}
}catch (Exception e) {
e.printStackTrace();
}finally {
// 關閉線程池
threadPool.shutdown();
}
}
運作程式的電腦為8核,可以通過線程池中的3個核心線程,同時執行3條任務,此時的結果沒有任何問題
線程池中線程可複用引發的問題
如果此時又多出來一個孩子,或者核心線程數變為2,即任務數大于核心線程數,會複用線程處理其他任務,即一個線程需要處理多個任務,這裡減少核心線程數為例示範:
@Test
public void test6() {
......
// 修改線程池核心線程數為2,其他不變
ExecutorService threadPool = Executors.newFixedThreadPool(2);
......
}
此時發現小明和小茗都是通過1号線程執行的任務,每個親戚最多發200紅包,5個親戚,最大值應該為1000,但是小茗收到了1172元的紅包,這肯定是不對的
原因在于:1号線程處理完小明之後,發現小茗任務沒有執行,此時1号線程處理兩個任務,ThreadLocal也還是同一個,即處理小茗任務時,ThreadLocal的值為小明任務處理後的值,并不是初始值0
在阿裡Java開發規範中強制要求回收自定義的ThreadLocal變量
[機智]如果有需要《阿裡開發規範手冊》的評論區留言或者私信免費擷取
通過try塊将任務邏輯包裹,在finally中通過remove方法回收該任務執行後的ThreadLocal值
@Test
public void test7() {
......
try {
......
try {
for (int j = 0; j < 5; j++) {
// 每家親戚給随機的200以内的紅包
int yasuiqian = random.nextInt(200);
// 紅包金額加1
threadLocal.set(yasuiqian + threadLocal.get());
}
System.out.println(Thread.currentThread().getName() + "共收到:" + threadLocal.get() + "元紅包");
} catch (Exception e) {
e.printStackTrace();
} finally {
// 回收資料
threadLocal.remove();
}
}catch (Exception e) {
e.printStackTrace();
}finally {
// 關閉線程池
threadPool.shutdown();
}
}
回收ThreadLocal變量後,複用該線程也不會對後續程式造成影響
ThreadLocal特點
- 統一設定初始值,每個線程可以通過set方法設定值,也可以通過get方法擷取目前值
- ThreadLocal被每一個線程單獨持有副本,互相獨立,隻能在該線程内部使用
- 如果配合線程池使用,線程可複用,需要調用remove方法回收資料,即重新設定為初始值,避免對後續程式造成影響和記憶體洩漏
- ThreadLocal變量因為線程獨立,是以不線上程安全問題
ThreadLocal應用場景
如上文所述,ThreadLocal 适用于如下兩種場景
- 每個線程需要自己獨立的資料
- 資料線上程内的共享,不需要在多線程之間共享
如:
- 遊戲玩家個人的屬性,裝備,積分等
- Spring中也通過ThreadLocal解決線程安全問題,在同一次請求響應的調用線程中,所有對象所通路的同一ThreadLocal變量都是目前線程所綁定的
[666]本篇達成實戰會用,下一篇通過原理介紹,實作面試會說!