ThreadLocal的應用場景


在通常的業務開發中,ThreadLocal 有兩種典型的使用場景

場景1:

ThreadLocal 用作保存每個線程獨享的對象,為每個線程都創建一個副本,這樣每個線程都可以修改自己所擁有的副本, 而不會影響其他線程的副本,確保了線程安全。

場景2:

ThreadLocal 用作每個線程內需要獨立保存信息,以便供其他方法更方便地獲取該信息的場景。每個線程獲取到的信息可能都是不一樣的,前面執行的方法保存了信息后,后續方法可以通過ThreadLocal 直接獲取到,避免了傳參,類似於全局變量的概念。

典型場景1

這種場景通常用於保存線程不安全的工具類,典型的需要使用的類就是 SimpleDateFormat

在這種情況下,每個Thread內都有自己的實例副本,且該副本只能由當前Thread訪問到並使用,相當於每個線程內部的本地變量,這也是ThreadLocal命名的含義。因為每個線程獨享副本,而不是公用的,所以不存在多線程間共享的問題。

比如有10個線程都要用到SimpleDateFormat

public class ThreadLocalDemo01 {

    public static void main(String[] args) throws InterruptedException {

        for (int i = 0; i < 10; i++) {
            int finalI = i;
            new Thread(() -> {
                String data = new ThreadLocalDemo01().date(finalI);
                System.out.println(data);
            }).start();
            Thread.sleep(100);
        }

    }

    private String date(int seconds){
        Date date = new Date(1000 * seconds);
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
        return simpleDateFormat.format(date);
    }
}

我們給每個線程都創建了SimpleDateFormat對象,他們之間互不影響,代碼是可以正常執行的。輸出結果:

00:00
00:01
00:02
00:03
00:04
00:05
00:06
00:07
00:08
00:09

我們用圖來看一下當前的這種狀態:

 

 

如果有1000個線程都用到SimpleDateFormat對象呢?

我們一般不會直接去創建這么多線程,而是通過線程池,比如:

public class ThreadLocalDemo011 {
   public static ExecutorService threadPool = Executors.newFixedThreadPool(16);

    public static void main(String[] args) throws InterruptedException {

        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            threadPool.submit(() -> {
                String data = new ThreadLocalDemo011().date(finalI);
                System.out.println(data);
            });
        }
        threadPool.shutdown();
    }

    private String date(int seconds){
        Date date = new Date(1000 * seconds);
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
        return simpleDateFormat.format(date);
    }

}

可以看出,我們用了一個16線程的線程池,並且給這個線程池提交了1000次任務。每個任務中它做的事情和之前是一樣的,還是去執行date方法,並且在這個方法中創建一個

simpleDateFormat 對象。結果:

00:00
00:07
00:04
00:02
...
16:29
16:28
16:27
16:26
16:39

我們剛才所做的就是每個任務都創建了一個 simpleDateFormat 對象,也就是說,1000 個任務對應 1000 個 simpleDateFormat 對象,但是如果任務數巨多怎么辦?

這么多對象的創建是有開銷的,並且在使用完之后的銷毀同樣是有開銷的,同時存在在內存中也是一種內存的浪費。

我們可能會想到,要不所有的線程共用一個 simpleDateFormat 對象?但是simpleDateFormat 又不是線程安全的,我們必須做同步,比如使用synchronized加鎖。到這里也許就是我們最終的一個解決方法。但是使用synchronized加鎖會陷入一種排隊的狀態,多個線程不能同時工作,這樣一來,整體的效率就被大大降低了。有沒有更好的解決方案呢?

使用ThreadLocal

對這種場景,ThreadLocal再合適不過了,ThreadLocal給每個線程維護一個自己的simpleDateFormat對象,這個對象在線程之間是獨立的,互相沒有關系的。這也就避免了線程安全問題。與此同時,simpleDateFormat對象還不會創造過多,線程池一共只有 16 個線程,所以需要16個對象即可。

public class ThreadLocalDemo04 {

    public static ExecutorService threadPool = Executors.newFixedThreadPool(16);

    public static void main(String[] args) throws InterruptedException {

        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            threadPool.submit(() -> {
                String data = new ThreadLocalDemo04().date(finalI);
                System.out.println(data);
            });
        }
        threadPool.shutdown();
    }

    private String date(int seconds){
        Date date = new Date(1000 * seconds);
        SimpleDateFormat dateFormat = ThreadSafeFormater.dateFormatThreadLocal.get();
        return dateFormat.format(date);
    }
}

class ThreadSafeFormater{
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("mm:ss"));
}

我們用圖來看一下當前的這種狀態:

 

 

典型場景2

每個線程內需要保存類似於全局變量的信息(例如在攔截器中獲取的用戶信息),可以讓不同方法直接使用,避免參數傳遞的麻煩卻不想被多線程共享(因為不同線程獲取到的用戶信息不一樣)。

例如,用 ThreadLocal 保存一些業務內容(用戶權限信息、從用戶系統獲取到的用戶名、用戶ID 等),這些信息在同一個線程內相同,但是不同的線程使用的業務內容是不相同的。

在線程生命周期內,都通過這個靜態 ThreadLocal 實例的 get() 方法取得自己 set 過的那個對象,避免了將這個對象(如 user 對象)作為參數傳遞的麻煩。

比如說我們是一個用戶系統,那么當一個請求進來的時候,一個線程會負責執行這個請求,然后這個請求就會依次調用service-1()、service-2()、service-3()、service-4(),這4個方法可能是分布在不同的類中的。

 

我們用圖畫的形式舉一個實例:

 

 

 

代碼:

package com.kong.threadlocal;


public class ThreadLocalDemo05 {
    public static void main(String[] args) {
        User user = new User("jack");
        new Service1().service1(user);
    }

}

class Service1 {
    public void service1(User user){
        //給ThreadLocal賦值,后續的服務直接通過ThreadLocal獲取就行了。
        UserContextHolder.holder.set(user);
        new Service2().service2();
    }
}

class Service2 {
    public void service2(){
        User user = UserContextHolder.holder.get();
        System.out.println("service2拿到的用戶:"+user.name);
        new Service3().service3();
    }
}

class Service3 {
    public void service3(){
        User user = UserContextHolder.holder.get();
        System.out.println("service3拿到的用戶:"+user.name);
        //在整個流程執行完畢后,一定要執行remove
        UserContextHolder.holder.remove();
    }
}

class UserContextHolder {
    //創建ThreadLocal保存User對象
    public static ThreadLocal<User> holder = new ThreadLocal<>();
}

class User {
    String name;
    public User(String name){
        this.name = name;
    }
}

 

執行的結果:

service2拿到的用戶:jack
service3拿到的用戶:jack

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM