JUC之ThreadLocal

若有不對之處歡迎大家指出,這個也是在學習工作中的一些總結,侵刪!
得之在俄頃,積之在平日。

1、使用場景

每個線程需要獨享的對象(通常是工具類,典型需要使用的類有SimpleDateFormart和Random),每個Thread內有自己的實例副本,不共享。
每個線程內需要保存全局變量(例如在攔截器中獲取用戶信息),可以在不同的地方直接使用,避免參數傳遞的麻煩,例如:當前用戶信息需要被線程內的所有方法共享,一個比較繁瑣的解決方案是把user作爲參數層層傳遞,從一個service傳遞到另一個service,以此類推,但是這樣會導致代碼冗餘且不易維護;解決方法:如果用ThreadLocal保存一些業務內容(用戶權限、信息,從用戶系統獲取到的用戶名、UserID等),這些信息在同一個線程內相同,但是不同的線程使用的業務內容是不同的,在線程的生命週期內,都通過這個靜態的ThreaLocal實例的get()方法取得自己set過的那個對象,避免了將這個對象(例如:User對象)作爲參數傳遞的麻煩。強調的是同一個請求內(同一個線程內)不同方法的共享。

2、ThreadLocal的兩個作用:

讓某個需要用到的對象在線程間隔離(每個線程都有自己獨立的對象)
在任何方法中都可以輕鬆的獲取到該對象

3、示例


注:採用java8新日期不會出現線程安全問題

//打印1000個日期  用10個線程來執行
//創建線程池
public static ExecutorService executorService = Executors.newFixedThreadPool(10);
//工具類,進行日期轉換
public static String DateTransition(long seconds) {
    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-DD HH:mm:ss");
    Date date = new Date(1000*seconds);
    return dateFormat.format(date);
}
public static void main(String[] args) {
    for (int i = 0; i < 1000; i++) {
        //i的十倍作爲毫秒數
        long scond = i*10;
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                // TODO Auto-generated method stub
                String date = new ThreadLocalTest01().DateTransition(scond);
                System.out.println("--->"+date);
            }
        });
    }
    executorService.shutdown();
}

這樣做看似沒什麼問題,實際上當執行1000次的時候則會創建1000個SimpleDateFormat,如下圖:
在這裏插入圖片描述
然後進行再次升級,將SimpleDateFormat提取出來作爲靜態常量,所有線程共享這一個SimpleDateFormat,就會發生線程安全問題
在這裏插入圖片描述

static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-DD HH:mm:ss");
//工具類,進行日期轉換
public  String DateTransition(long seconds) {
    Date date = new Date(1000*seconds);
    return dateFormat.format(date);
}
 

在這裏我們可以選擇加鎖來解決線程安全問題。

//工具類,進行日期轉換
public  String DateTransition(long seconds) {
    Date date = new Date(1000*seconds);
    String endDate = null;
    synchronized (ThreadLocalTest01.class){
        endDate = dateFormat.format(date);
    }
    return endDate;
}

加鎖能解決問題,但是會有性能開銷,如果使用ThreadLocal就不會存在這個問題

//打印1000個日期  用10個線程來執行
//創建線程池
public static ExecutorService executorService = Executors.newFixedThreadPool(10);
//static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-DD HH:mm:ss");
static ThreadLocal<SimpleDateFormat> threadLocaldateFormat = new ThreadLocal<SimpleDateFormat>(){
    @Override
    protected SimpleDateFormat initialValue() {
        return new SimpleDateFormat("yyyy-MM-DD HH:mm:ss");
    }
};
//工具類,進行日期轉換
public  String DateTransition(long seconds) {
    Date date = new Date(1000*seconds);
    SimpleDateFormat simpleDateFormat = threadLocaldateFormat.get();
    return simpleDateFormat.format(date);
}
public static void main(String[] args) {
    for (int i = 0; i < 1000; i++) {
        //i的十倍作爲毫秒數
        long scond = i*10;
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                // TODO Auto-generated method stub
                String date = new ThreadLocalTest01().DateTransition(scond);
                System.out.println("--->"+date);
            }
        });
    }
    executorService.shutdown();
}
 


一個請求,調用多個業務,這樣就會將這個請求所帶的數據進行一層一層的傳遞,會導致代碼冗餘且不易維護

解決方案:採用ThreadLocal將參數放在一個map裏面,然後各層都能取到map裏面的數據,這樣每個線程裏面的map都不一樣

public class ThreadLocalTest02 {//一個請求,調用多個業務
    public static void main(String[] args) {
        UserRequest UserRequest = new UserRequest();
        UserRequest.request("123");
    }
  }
    class CreateThreadLocal{
        public static ThreadLocal<String> threadLocal = new ThreadLocal<>();
    }
    class UserRequest{
        void request(String id){//請求
            CreateThreadLocal.threadLocal.set(id);
            UserService01 userService01 = new UserService01();
            userService01.Service01();
        }
    }
    class UserService01{
        void Service01(){//業務一
            System.out.println("Service01拿到數據:"+CreateThreadLocal.threadLocal.get());
            UserService02 userService02 = new UserService02();
            userService02.Service02();
        }
    }
    class UserService02{
        void Service02(){//業務二
            System.out.println("Service02拿到數據:"+CreateThreadLocal.threadLocal.get());
            UserService03 userService03 = new UserService03();
            userService03.Service03();
        }
    }
    class UserService03{
        void Service03(){//業務三
            System.out.println("Service03拿到數據:"+CreateThreadLocal.threadLocal.get());
            CreateThreadLocal.threadLocal.remove();
        }
    }

4、initialValue使用場景:

在ThreadLocal第一次get的時候把對象初始化出來,對象的初始化時機可以由我們控制(常用於工具類)

5、set的使用場景

如果需要保存到ThreadLocal裏面的對象的生成時機不由我們隨意控制,例如攔截器生成的用戶信息,用ThreadLocal.set直接放到我們的ThreadLocal中去,以便後續使用。

6、使用ThreadLocal帶來的好處

達到線程安全。
不需要加鎖,提高執行效率。
更高的利用內存,節省開銷:相比於每個任務都新建一個SimpleDateFormat,顯然用ThreadLocal可以節省內存和開銷。
免去傳參的繁瑣,ThreadLocal使得代碼耦合度低,更優雅。

7、解析

ThreadLocalMap類:

也就是ThreadLocals, ThreadLocalMap類是每個線程Thread裏面的變量,裏面最重要的是一個鍵值對數組Entry[] table,可以認爲是一個map,鍵值對:
鍵:這個ThreadLocal
值:實際需要的成員變量,比如我們放進去的id
ThreadLocalMap採用的是線性探測法,也就是如果發生衝突,就繼續找下一個位置,而不是使用鏈表拉鍊,不想map那樣採用鏈表和紅黑樹

在這裏插入圖片描述
在這裏插入圖片描述

8、主要方法介紹:

T initialValue():初始化 。
【該方法會返回當前線程對應的“初始值”,這是一個延遲加載的方法,只有在調用get的時候纔會觸發,當線程第一次使用get方法訪問變量時調用此方法,除非線程先前調用了set方法,這種情況下,不會爲線程調用本initialValue方法;通常,每個線程最多調用一次此方法,但如果調用了remove()後再調用get(),則可再次調用此方法;如果不重寫本方法,這個方法會返回null,一般使用匿名內部類的方法來重寫initialValue()方法,以便在後續使用過程中可以初始化副本對象。】
在這裏插入圖片描述
void set(T t):爲這個線程設置新值。
在這裏插入圖片描述
T get():得到這個線程對應的value,如果是首次調用get,則會調用initialize來得到這個值。
在這裏插入圖片描述
void remove():刪除對應這個線程的值。
在這裏插入圖片描述

9、ThreadLocal注意點:

內存泄漏
內存泄漏是指某個對象不再有用,但是佔有的內存卻不能被回收

		key的泄漏:ThreadLocalMap中的Entry繼承自	weakReference,是弱引用(弱應用的特點是,如果這個對象只被弱應用關	聯,沒有任何強引用關聯,那麼這個對象就可以被回收,所以弱引用不會	阻止GC)
 ![在這裏插入圖片描述](https://img-blog.csdnimg.cn/20200223135951481.png)
		Value泄漏:ThreadLocalMap的每個Entry都是一個對key的	弱引用,同時每個Entry都包含了一個對Value的強引用,正常情況下,	當線程終止,保存在ThreadLocal裏面的Value會被垃圾回收,因爲沒有	任何強引用了,但是如果線程不終止或者持續很長時間,那麼key對應的	Value就不能被回收,因爲有以下調用鏈:
Thread->ThreadLocalMap->Entry(key爲null)->value
因爲Value和Thread之間還存在這個強引用鏈路,所以會導致Value無法	被回收,就可能出現OOM。JDK已經考慮到了這個問題,所以在	set,remove,rehash方法中會掃描key爲null的Entry,並把對應的value	值設置爲null,這樣對象就可以被回收,但是如果不調用這些方法,那麼	就會導致了Value的內存泄漏。

如何避免內存泄漏

	調用remove()方法就會刪除對應的Entry對象,可以避免內存泄漏,所以在使用完ThreadLocal之後應該調用remove()方法

空指針異常

基本數據類型《==》包裝類

共享對象

不能將static放入ThreadLocal中

優先使用框架支持

例如:在Spring中,如果可以使用RequestContextHolder,那麼就不需要自己維護ThreadLocal,因爲自己可能會忘記調用remove等方法造成內存泄漏。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章