1. 程式人生 > >Java多執行緒:神祕的執行緒變數 ThreadLocal 你瞭解嗎?

Java多執行緒:神祕的執行緒變數 ThreadLocal 你瞭解嗎?

前言

  • Java多執行緒中,執行緒變數ThreadLocal非常重要,但對於很多開發者來說,這並不容易理解,甚至覺得有點神祕
  • 今天,我將獻上一份 ThreadLocal的介紹 & 實戰攻略,希望你們會喜歡。

目錄

示意圖

1. 簡介

示意圖

2. 使用流程

主要是建立ThreadLocal變數 & 訪問ThreadLocal變數

2.1 建立ThreadLocal變數

共有3種方式,具體如下

// 1. 直接建立物件
private ThreadLocal myThreadLocal = new ThreadLocal()

// 2. 建立泛型物件
private ThreadLocal myThreadLocal = new ThreadLocal<String>(); // 3. 建立泛型物件 & 初始化值 // 指定泛型的好處:不需要每次對使用get()方法返回的值作強制型別轉換 private ThreadLocal myThreadLocal = new ThreadLocal<String>() { @Override protected String initialValue() { return "This is the initial value"; } }; // 特別注意:
// 1. ThreadLocal例項 = 類中的private、static欄位 // 2. 只需例項化物件一次 & 不需知道它是被哪個執行緒例項化 // 3. 每個執行緒都保持 對其執行緒區域性變數副本 的隱式引用 // 4. 執行緒消失後,其執行緒區域性例項的所有副本都會被垃圾回收(除非存在對這些副本的其他引用) // 5. 雖然所有的執行緒都能訪問到這個ThreadLocal例項,但是每個執行緒只能訪問到自己通過呼叫ThreadLocal的set()設定的值 // 即 哪怕2個不同的執行緒在同一個`ThreadLocal`物件上設定了不同的值,他們仍然無法訪問到對方的值

2.2 訪問ThreadLocal變數

// 1. 設定值:set()
// 需要傳入一個Object型別的引數
myThreadLocal.set("初始值”);

// 2. 讀取ThreadLocal變數中的值:get()
// 返回一個Object物件
String threadLocalValue = (String) myThreadLocal.get();

3. 具體使用

以下則是測試程式碼

 public class ThreadLocalTest {

        // 測試程式碼
        public static void main(String[] args){
            // 新開2個執行緒用於設定 & 獲取 ThreadLoacl的值
            MyRunnable runnable = new MyRunnable();
            new Thread(runnable, "執行緒1").start();
            new Thread(runnable, "執行緒2").start();
        }

        // 執行緒類
        public static class MyRunnable implements Runnable {

            // 建立ThreadLocal & 初始化
            private ThreadLocal<String> threadLocal = new ThreadLocal<String>(){
                @Override
                protected String initialValue() {
                    return "初始化值";
                }
            };

            @Override
            public void run() {

                // 執行執行緒時,分別設定 & 獲取 ThreadLoacl的值
                String name = Thread.currentThread().getName();
                threadLocal.set(name + "的threadLocal"); // 設定值 = 執行緒名
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(name + ":" + threadLocal.get());
            }
        }
    }
  • 測試結果
執行緒1:執行緒1的threadLocal
執行緒2:執行緒2的threadLocal

// 從上述結果看出,在2個執行緒分別設定ThreadLocal值 & 分別獲取,結果並未互相干擾

4. 實現原理

  • 核心原理
    ThreadLocal類中有1個Map(稱:ThreadLocalMap):用於儲存每個執行緒 & 該執行緒設定的儲存在ThreadLocal變數的值

    1. ThreadLocalMap的鍵Key = 當前ThreadLocal例項、值value = 該執行緒設定的儲存在ThreadLocal變數的值
    2. keyThreadLocal物件的弱引用;當要拋棄掉ThreadLocal物件時,垃圾收集器會忽略該 key的引用而清理掉ThreadLocal物件
  • 關於如何設定 & 獲取 ThreadLocal變數裡的值,具體請看下面的原始碼分析

    請直接看程式碼註釋


// ThreadLocal的原始碼

public class ThreadLocal<T> {

    ...

  /** 
    * 設定ThreadLocal變數引用的值
    *  ThreadLocal變數引用 指向 ThreadLocalMap物件,即設定ThreadLocalMap的值 = 該執行緒設定的儲存在ThreadLocal變數的值
    *  ThreadLocalMap的鍵Key = 當前ThreadLocal例項
    *  ThreadLocalMap的值 = 該執行緒設定的儲存在ThreadLocal變數的值
    **/  
    public void set(T value) {

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

        // 2. 獲取該執行緒的ThreadLocalMap物件 ->>分析1
        ThreadLocalMap map = getMap(t);

        // 3. 若該執行緒的ThreadLocalMap物件已存在,則替換該Map裡的值;否則建立1個ThreadLocalMap物件
        if (map != null)
            map.set(this, value);// 替換
        else
            createMap(t, value);// 建立->>分析2
    }

  /** 
    * 獲取ThreadLocal變數裡的值
    * 由於ThreadLocal變數引用 指向 ThreadLocalMap物件,即獲取ThreadLocalMap物件的值 = 該執行緒設定的儲存在ThreadLocal變數的值
    **/ 
    public T get() {

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

        // 2. 獲取該執行緒的ThreadLocalMap物件
        ThreadLocalMap map = getMap(t);

        // 3. 若該執行緒的ThreadLocalMap物件已存在,則直接獲取該Map裡的值;否則則通過初始化函式建立1個ThreadLocalMap物件
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null)
                return (T)e.value; // 直接獲取值
        }
        return setInitialValue(); // 初始化
    }

  /** 
    * 初始化ThreadLocal的值
    **/ 
    private T setInitialValue() {

        T value = initialValue();

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

        // 2. 獲取該執行緒的ThreadLocalMap物件
        ThreadLocalMap map = getMap(t);

         // 3. 若該執行緒的ThreadLocalMap物件已存在,則直接替換該值;否則則建立
        if (map != null)
            map.set(this, value); // 替換
        else
            createMap(t, value); // 建立->>分析2
        return value;
    }


  /** 
    * 分析1:獲取當前執行緒的threadLocals變數引用
    **/ 
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

  /** 
    * 分析2:建立當前執行緒的ThreadLocalMap物件
    **/ 
    void createMap(Thread t, T firstValue) {
    // 新建立1個ThreadLocalMap物件 放入到 Thread類的threadLocals變數引用中:
        // a. ThreadLocalMap的鍵Key = 當前ThreadLocal例項
        // b. ThreadLocalMap的值 = 該執行緒設定的儲存在ThreadLocal變數的值
        t.threadLocals = new ThreadLocalMap(this, firstValue);
        // 即 threadLocals變數 屬於 Thread類中 ->> 分析3
    }


    ...
}

  /** 
    * 分析3:Thread類 原始碼分析
    **/ 

    public class Thread implements Runnable {
       ...

       ThreadLocal.ThreadLocalMap threadLocals = null;
       // 即 Thread類持有threadLocals變數
       // 執行緒類例項化後,每個執行緒物件擁有獨立的threadLocals變數變數
       // threadLocals變數在 ThreadLocal物件中 通過set() 或 get()進行操作

       ...
}

5. 額外補充

5.1 ThreadLocal如何做到執行緒安全

  • 每個執行緒擁有自己獨立的ThreadLocals變數(指向ThreadLocalMap物件 )
  • 每當執行緒 訪問 ThreadLocals變數時,訪問的都是各自執行緒自己的ThreadLocalMap變數(鍵 - 值)
  • ThreadLocalMap變數的鍵 key = 唯一 = 當前ThreadLocal例項

上述3點 保證了執行緒間的資料訪問隔離,即執行緒安全

  • 測試程式碼
 public class ThreadLocalTest {

        // 測試程式碼
        public static void main(String[] args){
            // 新開2個執行緒用於設定 & 獲取 ThreadLoacl的值
            MyRunnable runnable = new MyRunnable();
            new Thread(runnable, "執行緒1").start();
            new Thread(runnable, "執行緒2").start();
        }

        // 執行緒類
        public static class MyRunnable implements Runnable {

            // 建立ThreadLocal & 初始化
            private ThreadLocal<String> threadLocal = new ThreadLocal<String>(){
                @Override
                protected String initialValue() {
                    return "初始化值";
                }
            };

            @Override
            public void run() {

                // 執行執行緒時,分別設定 & 獲取 ThreadLoacl的值
                String name = Thread.currentThread().getName();
                threadLocal.set(name + "的threadLocal"); // 設定值 = 執行緒名
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(name + ":" + threadLocal.get());
            }
        }
    }
  • 測試結果
執行緒1:執行緒1的threadLocal
執行緒2:執行緒2的threadLocal

// 從上述結果看出,在2個執行緒分別設定ThreadLocal值 & 分別獲取,結果並未互相干擾

5.2 與同步機制的區別

示意圖

6. 總結

  • 本文全面講解了Java多執行緒ThreadLocal的相關知識
  • 下面我將繼續對 Android中的知識進行深入講解 ,有興趣可以繼續關注Carson_Ho的安卓開發筆記

請 幫頂 / 評論點贊!因為你的鼓勵是我寫作的最大動力!