ThreadLocal不好用?那是你沒用對!

在 Java 中,如果要問哪個類使用簡單,但用好最不簡單?我想你的腦海中一定會浮現出一次詞——“ThreadLocal”。

確實如此,ThreadLocal 原本設計是爲了解決併發時,線程共享變量的問題,但由於過度設計,如弱引用和哈希碰撞,從而導致它的理解難度大和使用成本高等問題。當然,如果稍有不慎還是導致髒數據、內存溢出、共享變量更新等問題,但即便如此,ThreadLocal 依舊有適合自己的使用場景,以及無可取代的價值,比如本文要介紹了這兩種使用場景,除了 ThreadLocal 之外,還真沒有合適的替代方案。

使用場景1:本地變量

我們以多線程格式化時間爲例,來演示 ThreadLocal 的價值和作用,當我們在多個線程中格式化時間時,通常會這樣操作。

① 2個線程格式化

當有 2 個線程進行時間格式化時,我們可以這樣寫:

import java.text.SimpleDateFormat;
import java.util.Date;

public class Test {
    public static void main(String[] args) throws InterruptedException {
        // 創建並啓動線程1
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                // 得到時間對象
                Date date = new Date(1 * 1000);
                // 執行時間格式化
                formatAndPrint(date);
            }
        });
        t1.start();
        // 創建並啓動線程2
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                // 得到時間對象
                Date date = new Date(2 * 1000);
                // 執行時間格式化
                formatAndPrint(date);
            }
        });
        t2.start();
    }

    /**
     * 格式化並打印結果
     * @param date 時間對象
     */
    private static void formatAndPrint(Date date) {
        // 格式化時間對象
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
        // 執行格式化
        String result = simpleDateFormat.format(date);
        // 打印最終結果
        System.out.println("時間:" + result);
    }
}

以上程序的執行結果爲:
image.png
上面的代碼因爲創建的線程數量並不多,所以我們可以給每個線程創建一個私有對象 SimpleDateFormat 來進行時間格式化。

② 10個線程格式化

當線程的數量從 2 個升級爲 10 個時,我們可以使用 for 循環來創建多個線程執行時間格式化,具體實現代碼如下:

import java.text.SimpleDateFormat;
import java.util.Date;

public class Test {
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            // 創建線程
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    // 得到時間對象
                    Date date = new Date(finalI * 1000);
                    // 執行時間格式化
                    formatAndPrint(date);
                }
            });
            // 啓動線程
            thread.start();
        }
    }
    /**
     * 格式化並打印時間
     * @param date 時間對象
     */
    private static void formatAndPrint(Date date) {
        // 格式化時間對象
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
        // 執行格式化
        String result = simpleDateFormat.format(date);
        // 打印最終結果
        System.out.println("時間:" + result);
    }
}

以上程序的執行結果爲:
image.png
從上述結果可以看出,雖然此時創建的線程數和 SimpleDateFormat 的數量不算少,但程序還是可以正常運行的。

③ 1000個線程格式化

然而當我們將線程的數量從 10 個變成 1000 個的時候,我們就不能單純的使用 for 循環來創建 1000 個線程的方式來解決問題了,因爲這樣頻繁的新建和銷燬線程會造成大量的系統開銷和線程過度爭搶 CPU 資源的問題。

所以經過一番思考後,我們決定使用線程池來執行這 1000 次的任務,因爲線程池可以複用線程資源,無需頻繁的新建和銷燬線程,也可以通過控制線程池中線程的數量來避免過多線程所導致的 **CPU** 資源過度爭搶和線程頻繁切換所造成的性能問題,而且我們可以將 SimpleDateFormat 提升爲全局變量,從而避免每次執行都要新建 SimpleDateFormat 的問題,於是我們寫下了這樣的代碼:

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class App {
    // 時間格式化對象
    private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");

    public static void main(String[] args) throws InterruptedException {
        // 創建線程池執行任務
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 10, 60,
                TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            // 執行任務
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    // 得到時間對象
                    Date date = new Date(finalI * 1000);
                    // 執行時間格式化
                    formatAndPrint(date);
                }
            });
        }
        // 線程池執行完任務之後關閉
        threadPool.shutdown();
    }

    /**
     * 格式化並打印時間
     * @param date 時間對象
     */
    private static void formatAndPrint(Date date) {
        // 執行格式化
        String result = simpleDateFormat.format(date);
        // 打印最終結果
        System.out.println("時間:" + result);
    }
}

以上程序的執行結果爲:
image.png
當我們懷着無比喜悅的心情去運行程序的時候,卻發現意外發生了,這樣寫代碼竟然會出現線程安全的問題。從上述結果可以看出,程序的打印結果竟然有重複內容的,正確的情況應該是沒有重複的時間纔對。

PS:所謂的線程安全問題是指:在多線程的執行中,程序的執行結果與預期結果不相符的情況

a) 線程安全問題分析

爲了找到問題所在,我們嘗試查看 SimpleDateFormatformat 方法的源碼來排查一下問題,format 源碼如下:

private StringBuffer format(Date date, StringBuffer toAppendTo,
                                FieldDelegate delegate) {
    // 注意此行代碼
    calendar.setTime(date);

    boolean useDateFormatSymbols = useDateFormatSymbols();

    for (int i = 0; i < compiledPattern.length; ) {
        int tag = compiledPattern[i] >>> 8;
        int count = compiledPattern[i++] & 0xff;
        if (count == 255) {
            count = compiledPattern[i++] << 16;
            count |= compiledPattern[i++];
        }

        switch (tag) {
            case TAG_QUOTE_ASCII_CHAR:
                toAppendTo.append((char)count);
                break;

            case TAG_QUOTE_CHARS:
                toAppendTo.append(compiledPattern, i, count);
                i += count;
                break;

            default:
                subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
                break;
        }
    }
    return toAppendTo;
}

從上述源碼可以看出,在執行 SimpleDateFormat.format 方法時,會使用 calendar.setTime 方法將輸入的時間進行轉換,那麼我們想象一下這樣的場景:

  1. 線程 1 執行了 calendar.setTime(date) 方法,將用戶輸入的時間轉換成了後面格式化時所需要的時間;
  2. 線程 1 暫停執行,線程 2 得到 CPU 時間片開始執行;
  3. 線程 2 執行了 calendar.setTime(date) 方法,對時間進行了修改;
  4. 線程 2 暫停執行,線程 1 得出 CPU 時間片繼續執行,因爲線程 1 和線程 2 使用的是同一對象,而時間已經被線程 2 修改了,所以此時當線程 1 繼續執行的時候就會出現線程安全的問題了。

正常的情況下,程序的執行是這樣的:
image.png

非線程安全的執行流程是這樣的:
image.png

b) 解決線程安全問題:加鎖

當出現線程安全問題時,我們想到的第一解決方案就是加鎖,具體的實現代碼如下:

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class App {
    // 時間格式化對象
    private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");

    public static void main(String[] args) throws InterruptedException {
        // 創建線程池執行任務
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 10, 60,
                TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            // 執行任務
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    // 得到時間對象
                    Date date = new Date(finalI * 1000);
                    // 執行時間格式化
                    formatAndPrint(date);
                }
            });
        }
        // 線程池執行完任務之後關閉
        threadPool.shutdown();
    }

    /**
     * 格式化並打印時間
     * @param date 時間對象
     */
    private static void formatAndPrint(Date date) {
        // 執行格式化
        String result = null;
        // 加鎖
        synchronized (App.class) {
            result = simpleDateFormat.format(date);
        }
        // 打印最終結果
        System.out.println("時間:" + result);
    }
}

以上程序的執行結果爲:
image.png
從上述結果可以看出,使用了 synchronized 加鎖之後程序就可以正常的執行了。

加鎖的缺點

加鎖的方式雖然可以解決線程安全的問題,但同時也帶來了新的問題,當程序加鎖之後,所有的線程必須排隊執行某些業務纔行,這樣無形中就降低了程序的運行效率了

有沒有既能解決線程安全問題,又能提高程序的執行速度的解決方案呢?

答案是:有的,這個時候 ThreadLocal 就要上場了。

c) 解決線程安全問題:ThreadLocal

1.ThreadLocal 介紹

ThreadLocal 從字面的意思來理解是線程本地變量的意思,也就是說它是線程中的私有變量,每個線程只能使用自己的變量。

以上面線程池格式化時間爲例,當線程池中有 10 個線程時,SimpleDateFormat 會存入 ThreadLocal 中,它也只會創建 10 個對象,即使要執行 1000 次時間格式化任務,依然只會新建 10 個 SimpleDateFormat 對象,每個線程調用自己的 ThreadLocal 變量。

2.ThreadLocal 基礎使用

ThreadLocal 常用的核心方法有三個:

  1. set 方法:用於設置線程獨立變量副本。 沒有 set 操作的 ThreadLocal 容易引起髒數據。
  2. get 方法:用於獲取線程獨立變量副本。 沒有 get 操作的 ThreadLocal 對象沒有意義。
  3. remove 方法:用於移除線程獨立變量副本。 沒有 remove 操作容易引起內存泄漏。

ThreadLocal 所有方法如下圖所示:
image.png
官方說明文檔:https://docs.oracle.com/javase/8/docs/api/

ThreadLocal 基礎用法如下:

/**
 * @公衆號:Java中文社羣
 */
public class ThreadLocalExample {
    // 創建一個 ThreadLocal 對象
    private static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        // 線程執行任務
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                String threadName = Thread.currentThread().getName();
                System.out.println(threadName + " 存入值:" + threadName);
                // 在 ThreadLocal 中設置值
                threadLocal.set(threadName);
                // 執行方法,打印線程中設置的值
                print(threadName);
            }
        };
        // 創建並啓動線程 1
        new Thread(runnable, "MyThread-1").start();
        // 創建並啓動線程 2
        new Thread(runnable, "MyThread-2").start();
    }

    /**
     * 打印線程中的 ThreadLocal 值
     * @param threadName 線程名稱
     */
    private static void print(String threadName) {
        try {
            // 得到 ThreadLocal 中的值
            String result = threadLocal.get();
            // 打印結果
            System.out.println(threadName + " 取出值:" + result);
        } finally {
            // 移除 ThreadLocal 中的值(防止內存溢出)
            threadLocal.remove();
        }
    }
}

以上程序的執行結果爲:
image.png
從上述結果可以看出,每個線程只會讀取到屬於自己的 ThreadLocal 值。

3.ThreadLocal 高級用法

① 初始化:initialValue
public class ThreadLocalByInitExample {
    // 定義 ThreadLocal
    private static ThreadLocal<String> threadLocal = new ThreadLocal(){
        @Override
        protected String initialValue() {
            System.out.println("執行 initialValue() 方法");
            return "默認值";
        }
    };

    public static void main(String[] args) {
        // 線程執行任務
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                // 執行方法,打印線程中數據(未設置值打印)
                print(threadName);
            }
        };
        // 創建並啓動線程 1
        new Thread(runnable, "MyThread-1").start();
        // 創建並啓動線程 2
        new Thread(runnable, "MyThread-2").start();
    }

    /**
     * 打印線程中的 ThreadLocal 值
     * @param threadName 線程名稱
     */
    private static void print(String threadName) {
        // 得到 ThreadLocal 中的值
        String result = threadLocal.get();
        // 打印結果
        System.out.println(threadName + " 得到值:" + result);
    }
}

以上程序的執行結果爲:
image.png
當使用了 #threadLocal.set 方法之後,initialValue 方法就不會被執行了,如下代碼所示:

public class ThreadLocalByInitExample {
    // 定義 ThreadLocal
    private static ThreadLocal<String> threadLocal = new ThreadLocal() {
        @Override
        protected String initialValue() {
            System.out.println("執行 initialValue() 方法");
            return "默認值";
        }
    };

    public static void main(String[] args) {
        // 線程執行任務
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                String threadName = Thread.currentThread().getName();
                System.out.println(threadName + " 存入值:" + threadName);
                // 在 ThreadLocal 中設置值
                threadLocal.set(threadName);
                // 執行方法,打印線程中設置的值
                print(threadName);
            }
        };
        // 創建並啓動線程 1
        new Thread(runnable, "MyThread-1").start();
        // 創建並啓動線程 2
        new Thread(runnable, "MyThread-2").start();
    }

    /**
     * 打印線程中的 ThreadLocal 值
     * @param threadName 線程名稱
     */
    private static void print(String threadName) {
        try {
            // 得到 ThreadLocal 中的值
            String result = threadLocal.get();
            // 打印結果
            System.out.println(threadName + "取出值:" + result);
        } finally {
            // 移除 ThreadLocal 中的值(防止內存溢出)
            threadLocal.remove();
        }
    }
}

以上程序的執行結果爲:
image.png

爲什麼 set 方法之後,初始化代碼就不執行了?

要理解這個問題,需要從 ThreadLocal.get() 方法的源碼中得到答案,因爲初始化方法 initialValueThreadLocal 創建時並不會立即執行,而是在調用了 get 方法只會纔會執行,測試代碼如下:

import java.util.Date;

public class ThreadLocalByInitExample {
    // 定義 ThreadLocal
    private static ThreadLocal<String> threadLocal = new ThreadLocal() {
        @Override
        protected String initialValue() {
            System.out.println("執行 initialValue() 方法 " + new Date());
            return "默認值";
        }
    };
    public static void main(String[] args) {
        // 線程執行任務
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                // 得到當前線程名稱
                String threadName = Thread.currentThread().getName();
                // 執行方法,打印線程中設置的值
                print(threadName);
            }
        };
        // 創建並啓動線程 1
        new Thread(runnable, "MyThread-1").start();
        // 創建並啓動線程 2
        new Thread(runnable, "MyThread-2").start();
    }

    /**
     * 打印線程中的 ThreadLocal 值
     * @param threadName 線程名稱
     */
    private static void print(String threadName) {
        System.out.println("進入 print() 方法 " + new Date());
        try {
            // 休眠 1s
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 得到 ThreadLocal 中的值
        String result = threadLocal.get();
        // 打印結果
        System.out.println(String.format("%s 取得值:%s %s",
                threadName, result, new Date()));
    }
}

以上程序的執行結果爲:
image.png
從上述打印的時間可以看出:initialValue 方法並不是在 ThreadLocal 創建時執行的,而是在調用 Thread.get 方法時才執行的。

接下來來看 Threadlocal.get 源碼的實現:

public T get() {
    // 得到當前的線程
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    // 判斷 ThreadLocal 中是否有數據
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            // 有 set 值,直接返回數據
            return result;
        }
    }
    // 執行初始化方法【重點關注】
    return setInitialValue();
}
private T setInitialValue() {
    // 執行初始化方法【重點關注】
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

從上述源碼可以看出,當 ThreadLocal 中有值時會直接返回值 e.value,只有 Threadlocal 中沒有任何值時纔會執行初始化方法 initialValue

注意事項—類型必須保持一致

注意在使用 initialValue 時,返回值的類型要和 ThreadLoca 定義的數據類型保持一致,如下圖所示:
image.png
如果數據不一致就會造成 ClassCaseException 類型轉換異常,如下圖所示:
image.png

② 初始化2:withInitial
import java.util.function.Supplier;

public class ThreadLocalByInitExample {
    // 定義 ThreadLocal
    private static ThreadLocal<String> threadLocal =
            ThreadLocal.withInitial(new Supplier<String>() {
                @Override
                public String get() {
                    System.out.println("執行 withInitial() 方法");
                    return "默認值";
                }
            });
    public static void main(String[] args) {
        // 線程執行任務
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                String threadName = Thread.currentThread().getName();
                // 執行方法,打印線程中設置的值
                print(threadName);
            }
        };
        // 創建並啓動線程 1
        new Thread(runnable, "MyThread-1").start();
        // 創建並啓動線程 2
        new Thread(runnable, "MyThread-2").start();
    }

    /**
     * 打印線程中的 ThreadLocal 值
     * @param threadName 線程名稱
     */
    private static void print(String threadName) {
        // 得到 ThreadLocal 中的值
        String result = threadLocal.get();
        // 打印結果
        System.out.println(threadName + " 得到值:" + result);
    }
}

以上程序的執行結果爲:
image.png
通過上述的代碼發現,withInitial 方法的使用好和 initialValue 好像沒啥區別,那爲啥還要造出兩個類似的方法呢?客官莫着急,繼續往下看。

③ 更簡潔的 withInitial 使用

withInitial 方法的優勢在於可以更簡單的實現變量初始化,如下代碼所示:

public class ThreadLocalByInitExample {
    // 定義 ThreadLocal
    private static ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "默認值");
    public static void main(String[] args) {
        // 線程執行任務
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                String threadName = Thread.currentThread().getName();
                // 執行方法,打印線程中設置的值
                print(threadName);
            }
        };
        // 創建並啓動線程 1
        new Thread(runnable, "MyThread-1").start();
        // 創建並啓動線程 2
        new Thread(runnable, "MyThread-2").start();
    }

    /**
     * 打印線程中的 ThreadLocal 值
     * @param threadName 線程名稱
     */
    private static void print(String threadName) {
        // 得到 ThreadLocal 中的值
        String result = threadLocal.get();
        // 打印結果
        System.out.println(threadName + " 得到值:" + result);
    }
}

以上程序的執行結果爲:
image.png

4.ThreadLocal 版時間格式化

瞭解了 ThreadLocal 的使用之後,我們回到本文的主題,接下來我們將使用 ThreadLocal 來實現 1000 個時間的格式化,具體實現代碼如下:

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class MyThreadLocalByDateFormat {
    // 創建 ThreadLocal 並設置默認值
    private static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =
            ThreadLocal.withInitial(() -> new SimpleDateFormat("mm:ss"));

    public static void main(String[] args) {
        // 創建線程池執行任務
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 10, 60,
                TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));
        // 執行任務
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            // 執行任務
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    // 得到時間對象
                    Date date = new Date(finalI * 1000);
                    // 執行時間格式化
                    formatAndPrint(date);
                }
            });
        }
        // 線程池執行完任務之後關閉
        threadPool.shutdown();
        // 線程池執行完任務之後關閉
        threadPool.shutdown();
    }
    /**
     * 格式化並打印時間
     * @param date 時間對象
     */
    private static void formatAndPrint(Date date) {
        // 執行格式化
        String result = dateFormatThreadLocal.get().format(date);
        // 打印最終結果
        System.out.println("時間:" + result);
    }
}

以上程序的執行結果爲:
image.png
從上述結果可以看出,使用 ThreadLocal 也可以解決線程併發問題,並且避免了代碼加鎖排隊執行的問題。

使用場景2:跨類傳遞數據

除了上面的使用場景之外,我們還可以使用 **ThreadLocal** 來實現線程中跨類、跨方法的數據傳遞。比如登錄用戶的 User 對象信息,我們需要在不同的子系統中多次使用,如果使用傳統的方式,我們需要使用方法傳參和返回值的方式來傳遞 User 對象,然而這樣就無形中造成了類和類之間,甚至是系統和系統之間的相互耦合了,所以此時我們可以使用 ThreadLocal 來實現 User 對象的傳遞。

確定了方案之後,接下來我們來實現具體的業務代碼。我們可以先在主線程中構造並初始化一個 User 對象,並將此 User 對象存儲在 ThreadLocal 中,存儲完成之後,我們就可以在同一個線程的其他類中,如倉儲類或訂單類中直接獲取並使用 User 對象了,具體實現代碼如下。

主線程中的業務代碼:

public class ThreadLocalByUser {
    public static void main(String[] args) {
        // 初始化用戶信息
        User user = new User("Java");
        // 將 User 對象存儲在 ThreadLocal 中
        UserStorage.setUser(user);
        // 調用訂單系統
        OrderSystem orderSystem = new OrderSystem();
        // 添加訂單(方法內獲取用戶信息)
        orderSystem.add();
        // 調用倉儲系統
        RepertorySystem repertory = new RepertorySystem();
        // 減庫存(方法內獲取用戶信息)
        repertory.decrement();
    }
}

User 實體類:

/**
 * 用戶實體類
 */
class User {
    public User(String name) {
        this.name = name;
    }
    private String name;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

ThreadLocal 操作類:

/**
 * 用戶信息存儲類
 */
class UserStorage {
    // 用戶信息
    public static ThreadLocal<User> USER = new ThreadLocal();

    /**
     * 存儲用戶信息
     * @param user 用戶數據
     */
    public static void setUser(User user) {
        USER.set(user);
    }
}

訂單類:

/**
 * 訂單類
 */
class OrderSystem {
    /**
     * 訂單添加方法
     */
    public void add() {
        // 得到用戶信息
        User user = UserStorage.USER.get();
        // 業務處理代碼(忽略)...
        System.out.println(String.format("訂單系統收到用戶:%s 的請求。",
                user.getName()));
    }
}

倉儲類:

/**
 * 倉儲類
 */
class RepertorySystem {
    /**
     * 減庫存方法
     */
    public void decrement() {
        // 得到用戶信息
        User user = UserStorage.USER.get();
        // 業務處理代碼(忽略)...
        System.out.println(String.format("倉儲系統收到用戶:%s 的請求。",
                user.getName()));
    }
}

以上程序的最終執行結果:
image.png
從上述結果可以看出,當我們在主線程中先初始化了 User 對象之後,訂單類和倉儲類無需進行任何的參數傳遞也可以正常獲得 User 對象了,從而實現了一個線程中,跨類和跨方法的數據傳遞

總結

使用 ThreadLocal 可以創建線程私有變量,所以不會導致線程安全問題,同時使用 ThreadLocal 還可以避免因爲引入鎖而造成線程排隊執行所帶來的性能消耗;再者使用 ThreadLocal 還可以實現一個線程內跨類、跨方法的數據傳遞。

參考 & 鳴謝

《碼出高效:Java開發手冊》

《Java 併發編程 78 講》

關注公號「Java中文社羣」查看更多有意思、漲知識的併發編程文章。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章