天天看點

關于并發程式設計線程間資料隔離和安全,ThreadLocal給出高品質答案

作者:添甄
滿懷憂思,不如先幹再說!做幹淨純粹的技術分享!歡迎評論區或私信交流!

[酷][酷][酷]ThreadLocal在工作和面試中都非常常見,本篇文章收錄于《Java并發程式設計》合集,通過2-3篇文章介紹清楚ThreadLocal的應用,原理,案例,達到實戰會用,面試會說的效果。通過本篇你可以得到[奸笑]:

  • ThreadLocal的正确解釋和作用
  • ThreadLocal的API以及三種初始化方式的對比
  • 通過ThreadLocal實作春節紅包案例
  • 通過線程池引出ThreadLocal隐患和解決方案
  • 總結ThreadLocal特點和應用場景

喜歡的話記得動動手指點關注,升職加薪不迷路,長期穩定輸出幹貨技術文章

ThreadLocal

對于ThreadLocal的翻譯有很多,如:線程本地,本地線程,如果根據他的作用來說翻譯成【線程局部變量】我認為是比較合适的

ThreadLocal作用

ThreadLocal是java.lang包中的一個泛型類,可以實作為線程建立獨有變量,這個變量對于其他線程是隔離的,也就是線程本地的值,這也是ThreadLocal名字的來源

關于并發程式設計線程間資料隔離和安全,ThreadLocal給出高品質答案

每個使用該變量的線程都要初始化一個完全獨立的執行個體副本,不存在多線程間共享的問題

ThreadLocal方法

ThreadLocal用來存儲目前線程的獨有資料,相關API就是存值,取值,清空值的簡單操作

關于并發程式設計線程間資料隔離和安全,ThreadLocal給出高品質答案
  • 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

關于并發程式設計線程間資料隔離和安全,ThreadLocal給出高品質答案

如果想要設定值,則需要通過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

關于并發程式設計線程間資料隔離和安全,ThreadLocal給出高品質答案

重寫initialValue方法

該方法的主要作用是傳回目前線程ThreadLocal的初始值,但是在ThreadLocal中預設實作傳回為null

這個值和具體的泛型類型有關,通常需要根據實際需求重寫此方法,定義初始值

關于并發程式設計線程間資料隔離和安全,ThreadLocal給出高品質答案
@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

關于并發程式設計線程間資料隔離和安全,ThreadLocal給出高品質答案

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的初始化

關于并發程式設計線程間資料隔離和安全,ThreadLocal給出高品質答案

推薦通過withInitial靜态方法實作ThreadLocal的初始化

如果線程内需要修改值則可以使用set方法,如果需要擷取值則使用get方法

結合下圖搞懂ThreadLocal資料存儲:一個ThreadLocal執行個體,在每個線程中都有獨自的初始化副本,接下來每個線程對ThradLocal的操作都線上程内,對其他線程隔離

關于并發程式設計線程間資料隔離和安全,ThreadLocal給出高品質答案

​[吐舌]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個親戚家,收到的紅包金額實作獨自記錄

關于并發程式設計線程間資料隔離和安全,ThreadLocal給出高品質答案

線程池實作保管紅包

項目開發過程中對于多線程的場景都推薦使用線程池實作,可以避免線程頻繁建立和銷毀的資源浪費,使用線程池是一定要記得在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條任務,此時的結果沒有任何問題

關于并發程式設計線程間資料隔離和安全,ThreadLocal給出高品質答案

線程池中線程可複用引發的問題

如果此時又多出來一個孩子,或者核心線程數變為2,即任務數大于核心線程數,會複用線程處理其他任務,即一個線程需要處理多個任務,這裡減少核心線程數為例示範:

@Test
public void test6() {
    ......
    // 修改線程池核心線程數為2,其他不變
    ExecutorService threadPool = Executors.newFixedThreadPool(2);
    ......
}           

此時發現小明和小茗都是通過1号線程執行的任務,每個親戚最多發200紅包,5個親戚,最大值應該為1000,但是小茗收到了1172元的紅包,這肯定是不對的

關于并發程式設計線程間資料隔離和安全,ThreadLocal給出高品質答案

原因在于:1号線程處理完小明之後,發現小茗任務沒有執行,此時1号線程處理兩個任務,ThreadLocal也還是同一個,即處理小茗任務時,ThreadLocal的值為小明任務處理後的值,并不是初始值0

在阿裡Java開發規範中強制要求回收自定義的ThreadLocal變量

​​[機智]如果有需要《阿裡開發規範手冊》的評論區留言或者私信免費擷取

關于并發程式設計線程間資料隔離和安全,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給出高品質答案

ThreadLocal特點

  • 統一設定初始值,每個線程可以通過set方法設定值,也可以通過get方法擷取目前值
  • ThreadLocal被每一個線程單獨持有副本,互相獨立,隻能在該線程内部使用
  • 如果配合線程池使用,線程可複用,需要調用remove方法回收資料,即重新設定為初始值,避免對後續程式造成影響和記憶體洩漏
  • ThreadLocal變量因為線程獨立,是以不線上程安全問題

ThreadLocal應用場景

如上文所述,ThreadLocal 适用于如下兩種場景

  • 每個線程需要自己獨立的資料
  • 資料線上程内的共享,不需要在多線程之間共享

如:

  • 遊戲玩家個人的屬性,裝備,積分等
  • Spring中也通過ThreadLocal解決線程安全問題,在同一次請求響應的調用線程中,所有對象所通路的同一ThreadLocal變量都是目前線程所綁定的

​[666]本篇達成實戰會用,下一篇通過原理介紹,實作面試會說!

繼續閱讀