Java - ThreadLocal 類的使用

By sunwc 2023-03-15 Java

有關在實際工作上使用到 ThreadLocal 的例子,可以參考我的另一篇文章 Java - ThreadLocal 實際應用

ThreadLocal 實現原理

  • ThreadLocal 從字面義直翻,就是執行緒 (Thread) 的局部變數,是每一個執行緒所單獨持有,其他執行緒不能對其進行存取

    • ThreadLocal 支持泛型,也就是支持 value 是可以設置的,像是 ThreadLocal<Integer> 就是設置 value 為 Integer 類型

    • 每個執行緒會有自己一份 ThreadLocalMap 副本,去儲存這個執行緒自己想存放的 ThreadLocal<T> 變數們,ThreadLocalMap 副本內部儲存的是一個 key-value 對,其中 key 是某個 ThreadLocal<T> 物件實例 , value 就是這個執行緒、該 ThreadLocal<T> 物件實例 set 的值,所以對一個執行緒來說,一個 ThreadLocal<T> 只能存一個值,而一個執行緒可以存放多個 ThrealLocal<T>

JDK Souce code

public class Thread implements Runnable {
  // Thread 類裡的threadLocals 存放此執行緒的專有 ThreadLocalMap 副本
    ThreadLocal.ThreadLocalMap threadLocals = null;
}
public class ThreadLocal<T> {
    // 根據執行緒,取得那個執行緒自己的 ThreadLocalMap
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    static class ThreadLocalMap {

        // ThreadLocalMap 的 key 是使用 "弱引用" 的 ThreadLocal<T>
        static class Entry extends WeakReference<ThreadLocal<?>> {
            
            Object value;

            // ThreadLocalMap 中的 key 就是 ThreadLocal<T>,value 就是設置的值
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
    }

    public T get() {

        // 取得當前執行緒
        Thread t = Thread.currentThread();

        // 每個執行緒 都有一個自己的 ThreadLocalMap
        // ThreadLocalMap 裡就保存著所有的ThreadLocal<T>變數
        ThreadLocalMap map = getMap(t);

        if (map != null) {
            // ThreadLocalMap 的 key 就是當前 ThreadLocal<T> 物件實例
            // 多個 ThreadLocal<T> 變數都是放在這個 map 中的
            ThreadLocalMap.Entry e = map.getEntry(this);

            if (e != null) {
                @SuppressWarnings("unchecked")
                // 從 map 裡取出來的值就是我們需要的這個 ThreadLocal<T> 變數
                T result = (T)e.value;
                return result;
            }
        }
        // 如果 map 沒有初始化,那麼在這裡初始化一下
        return setInitialValue();
    }


    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            map.set(this, value);
        } else {
            createMap(t, value);  
        }     
    }

    public void remove() {
        ThreadLocalMap map = getMap(Thread.currentThread());
        if (map != null) {
            map.remove(this);
        }      
    }
}

ThreadLocal 常用方法

  • set(x) : 設置此執行緒的想要放的值 x
  • get() : 取得此執行緒當初存放的值,如果沒有存放過則返回 null
  • remove() : 刪除此執行緒的 key-value 對,也就是如果先執行 remove 再執行 get,會返回 null

可以創建多個 ThreadLocal<T> 物件,對每個 ThreadLocal<T> 都設置不同的值

  • 像是以下的例子,在 main 執行緒中的 ThreadLocalMap ,就有兩個 key-value 的映射,userIdThreadLocal -> 100、userNameThreadLocal -> hello

例子

public class Main {
    public static void main(String[] args){
        ThreadLocal<Integer> userIdThreadLocal = new ThreadLocal<>();
        ThreadLocal<String> userNameThreacLocal = new ThreadLocal<>();

        userId.set(100);
        userName.set("hello");
    }
}

ThreadLocal 存在內存洩露

這邊不多去探討這個議題,內存洩漏是可以避免的,只要當前執行緒要結束前記得即時的remove(),也就是是使得 ThreadLocalMap 中不要存在這個 key-value 對,這樣才能確保 GC 能正確回收

以下有更多的文章,仔細地談論 ThreadLocal 內存洩漏問題 與 Java 自身解決的方式,但都不治本!還是當前執行緒用完 ThreadLocal 記得呼叫remove(),才是確保線程安全的根本之道!

參考資料:


SimpleDateFormat 非線程安全?

由於 SimpleDateFormat 本身非 synchronized ,所以如果在不同執行緒使用同一個 SimpleDateFormat ,就會導致輸出的時間異常,請看以下的例子

例子

public static void main(String[] args) {
    DateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    Date date1 = new Date(1614820308016L);
    Date date2 = new Date(1615820309016L);
    System.out.println("初始定義時間1 : " + sdf.format(date1));
    System.out.println("初始定義時間2 : " + sdf.format(date2));

    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 100; i++) {
            String dateStr = sdf.format(date1);
            if (!"2021-03-04 09:11:48".equals(dateStr)) {
                System.out.println(Thread.currentThread().getName() + "異常時間為 : " + dateStr);
            }
        }
    }, "thread 1");

    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 100; i++) {
            String dateStr = sdf.format(date2);
            if (!"2021-03-15 22:58:29".equals(dateStr)) {
                System.out.println(Thread.currentThread().getName() + "異常時間為 : " + dateStr);
            }
        }
    }, "Thread 2");
    t1.start();
    t2.start();
    try {
        t1.join();
        t2.join();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

輸出異常:(Thread2 輸出了 Thread1 的時間)

初始定義時間1 : 2021-03-04 09:11:48
初始定義時間2 : 2021-03-15 22:58:29
Thread 2異常時間為 : 2021-03-04 09:11:48

但是, ThreadLocal 可以解決非線程安全的問題,只要在各自的執行緒,持有各自的ThreadLocal<DateFormat>,就不會有問題了,請看下面的例子

public static void main(String[] args) {
    DateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    Date date1 = new Date(1614820308016L);
    Date date2 = new Date(1615820309016L);
    System.out.println("初始定義時間1 : " + sdf.format(date1));
    System.out.println("初始定義時間2 : " + sdf.format(date2));

    Thread t1 = new Thread(() -> {
        ThreadLocal<DateFormat> currentSDF = new ThreadLocal<>();
        DateFormat df1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        currentSDF.set(df1);
        DateFormat dateFormat1 = currentSDF.get();

        for (int i = 0; i < 100; i++) {
            String dateStr = dateFormat1.format(date1);
            if (!"2021-03-04 09:11:48".equals(dateStr)) {
                System.out.println(Thread.currentThread().getName() + "異常時間為 : " + dateStr);
            }
        }
        currentSDF.remove();

    }, "thread 1");


    Thread t2 = new Thread(() -> {
        ThreadLocal<DateFormat> currentSDF = new ThreadLocal<>();
        DateFormat df2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        currentSDF.set(df2);
        DateFormat dateFormat2 = currentSDF.get();

        for (int i = 0; i < 100; i++) {
            String dateStr = dateFormat2.format(date2);
            if (!"2021-03-15 22:58:29".equals(dateStr)) {
                System.out.println(Thread.currentThread().getName() + "異常時間為 : " + dateStr);
            }
        }
        currentSDF.remove();

    }, "Thread 2");
    t1.start();
    t2.start();

    try {
        t1.join();
        t2.join();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

輸出正常:

初始定義時間1 : 2021-03-04 09:11:48
初始定義時間2 : 2021-03-15 22:58:29

參考來源