天天看點

Java 多線程為啥要有ThreadLocal,怎麼用,這篇講全了

作者:程式猿不相信眼淚

前面我們學習的線程并發時的同步控制,是為了保證多個線程對共享資料争用時的正确性的。那如果一個操作本身不涉及對共享資料的使用,相反,隻是希望變量隻能由建立它的線程使用(即線程隔離)就需要到線程本地存儲了。

Java 通過 ThreadLocal 提供了程式對線程本地存儲的使用。

通過建立 ThreadLocal 類的執行個體,讓我們能夠建立隻能由同一線程讀取和寫入的變量。是以,即使兩個線程正在執行相同的代碼,并且代碼引用了相同名稱的 ThreadLocal 變量,這兩個線程也無法看到彼此的存儲在 ThreadLocal 裡的值。否則也就不能叫線程本地存儲了。

本文大綱如下:

Java 多線程為啥要有ThreadLocal,怎麼用,這篇講全了

ThreadLocal

ThreadLocal 是 Java 内置的類,全稱 java.lang.ThreadLoal, java.lang 包裡定義的類和接口在程式裡都是可以直接使用,不需要導入的。

ThreadLocal 的類定義如下:

public class ThreadLocal<T> {
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        //......
        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 m = getMap(Thread.currentThread());
         if (m != null) {
             m.remove(this);
         }
     }

    protected T initialValue() {
        return null;
    }
    
    public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
        return new SuppliedThreadLocal<>(supplier);
    }
    
    // ...
}
複制代碼           

上面隻是列出了 ThreadLocal類裡我們經常會用到的方法,這幾個方法他們的說明如下。

  • T get()- 用于擷取 ThreadLocal 在目前線程中儲存的變量副本。
  • void set(T value) - 用于向ThreadLocal中設定目前線程中變量的副本。
  • void remove() - 用于删除目前線程儲存在ThreadLocal中的變量副本。
  • initialValue() - 為 ThreadLocal 設定預設的 get方法擷取到的始值,預設是 null ,想修改的話需要用子類重寫 initialValue 方法,或者是用TheadLocal提供的withInitial方法 。

下面我們詳細看一下 ThreadLocal 的使用。

建立和讀寫 ThreadLocal

通過上面 ThreadLocal 類的定義我們能看出來, ThreadLocal 是支援泛型的,是以在建立 ThreadLocal 時沒有什麼特殊需求的情況下,我們都會為其提供類型參數,這樣在讀取使用 ThreadLocal 變量時就能免去類型轉換的操作。

private ThreadLocal threadLocal = new ThreadLocal();
threadLocal.set("A thread local value");
// 建立時沒有使用泛型指定類型,預設是 Object
// 使用時要先做類型轉換
String threadLocalValue = (String) threadLocal.get();
複制代碼           

上面這個例子,在建立 ThreadLocal 時沒有使用泛型指定類型,是以存儲在其中的值預設是 Object 類型,這樣就需要在使用時先做類型轉換才行。

下面再看一個使用泛型的版本

private ThreadLocal<String> myThreadLocal = new ThreadLocal<String>();

myThreadLocal.set("Hello ThreadLocal");
String threadLocalValue = myThreadLocal.get();
複制代碼           

現在我們隻能把 String 類型的值存到 ThreadLocal 中,并且從 ThreadLocal 讀取出值後也不再需要進行類型轉換。

關于泛型使用方面的詳細講解,可以看本系列中的泛型章節。

看了這篇Java 泛型通關指南,再也不怵滿屏尖括号了

想要删除一個 ThreadLocal 執行個體裡存儲的值,隻需要調用ThreadLocal執行個體中的 remove 方法即可。

myThreadLocal.remove();
複制代碼           

當然,這個删除操作隻是删除的變量在本地線程中的副本,其他線程不會受到本線程中删除操作的影響。下面我們把 ThreadLocal 的建立、讀寫和删除攢一個簡單的例子,做下示範。

// 源碼: https://github.com/kevinyan815/JavaXPlay/blob/main/src/com/threadlocal/ThreadLocalExample.java
package com.threadlocal;

public class ThreadLocalExample {

    private  ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

    private void setAndPrintThreadLocal() {
        threadLocal.set((int) (Math.random() * 100D) );
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println( Thread.currentThread().getName() + ": " + threadLocal.get() );

        if ( threadLocal.get() % 2 == 0) {
            // 測試删除 ThreadLocal
            System.out.println(Thread.currentThread().getName() + ": 删除ThreadLocal");
            threadLocal.remove();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ThreadLocalExample tlExample = new ThreadLocalExample();
        Thread thread1 = new Thread(() -> tlExample.setAndPrintThreadLocal(), "線程1");
        Thread thread2 = new Thread(() -> tlExample.setAndPrintThreadLocal(), "線程2");

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();
    }
}
複制代碼           

上面的例程會有如下輸出,當然如果恰好兩個線程裡 ThreadLocal 變量裡存儲的都是偶數的話,就不會有第三行輸出啦。

線程2: 97
線程1: 64
線程1: 删除ThreadLocal
複制代碼           

本例子的源碼項目放在了GitHub上,需要的可自行取用進行參考:ThreadLocal變量操作示例--增删查

為 ThreadLocal 設定初始值

在程式裡,聲明ThreadLocal類型的變量時,我們可以同時為變量設定一個自定義的初始值,這樣做的好處是,即使沒有使用 set 方法給 ThreadLocal 變量設定值的情況下,調用ThreadLocal變量的 get() 時能傳回一個對業務邏輯來說更有意義的初始值,而不是預設的 Null 值。

在 Java 中有兩種方式可以指定 ThreadLocal 變量的自定義初始值:

  • 建立一個 ThreadLocal 的子類,覆寫 initialValue() 方法,程式中則使用ThreadLocal子類建立執行個體變量。
  • 使用 ThreadLocal 類提供的的靜态方法 withInitial(Supplier<? extends S> supplier) 來建立 ThreadLocal 執行個體變量,該方法接收一個函數式接口 Supplier 的實作作為參數,在 Supplier 實作中為 ThreadLocal 設定初始值。

關于函數式接口Supplier如果你還不太清楚的話,可以檢視系列中函數式程式設計接口章節中的詳細内容。下面我們看看分别用這兩種方式怎麼給 ThreadLocal 變量提供初始值。

使用子類覆寫 initialValue() 設定初始值

通過定義ThreadLocal 的子類,在子類中覆寫 initialValue() 方法的方式給 ThreadLocal 變量設定初始值的方式,可以使用匿名類,簡化建立子類的步驟。

下面我們在程式裡建立 ThreadLocal 執行個體時,直接使用匿名類來覆寫 initialValue() 方法的一個例子。

public class ThreadLocalExample {

    private ThreadLocal threadLocal = new ThreadLocal<Integer>() {
        @Override protected Integer initialValue() {
            return (int) System.currentTimeMillis();
        }
    };
    
	......   
}
複制代碼           

有同學可能會問,這塊能不能用 Lambda 而不是用匿名類,答案是不能,在這個專欄講 Lambda 的文章中我們說過,Lambda 隻能用于實作函數式接口(接口中有且隻有一個抽象方法,是以這裡隻能使用匿名了簡化建立子類的步驟,不過另外一種通過withInitial方法建立并自定義初始化ThreadLocal變量的時候,是可以使用Lambda 的,我們下面看看使用 withInital 靜态方法設定 ThreadLocal 變量初始值的示範。

通過 withInital 靜态方法設定初始值

為 ThreadLocal 執行個體變量指定初始值的第二種方式是使用 ThreadLocal 類提供的靜态工廠方法 withInitial 。withInitial 方法接收一個函數式接口 Supplier 的實作作為參數,在 Supplier 的實作中我們可以為要建立的 ThreadLocal 變量設定初始值。

Supplier 接口是一個函數式接口,表示提供某種值的函數。 Supplier 接口也可以被認為是工廠接口。

@FunctionalInterface public interface Supplier { T get(); }

下面的程式裡,我們用 ThreadLocal 的 withInitial 方法為 ThreadLocal 執行個體變量設定了初始值

public class ThreadLocalExample {

    private ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(new Supplier<Integer>() {
        @Override
        public String get() {
            return (int) System.currentTimeMillis();
        }
    });
    
	......   
}
複制代碼           

對于函數式接口,理所當然會想到用 Lambda 來實作。上面這個 withInitial 的例子用 Lambda 實作的話能進一步簡化成:

public class ThreadLocalExample {

	private ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> (int) System.currentTimeMillis());
	......
}
複制代碼           

關于 Lambda 和 函數式接口 Supplier 的詳細内容,可以通過本系列中與這兩個主題相關的文章進行學習。

Java Lambda 表達式的各種形态和使用場景,看這篇就夠了 Java 中那些繞不開的内置接口 -- 函數式程式設計和 Java 的内置函數式接口

ThreadLocal 在父子線程間的傳遞

ThreadLocal 提供的線程本地存儲,給資料提供了線程隔離,但是有的時候用一個線程開啟的子線程,往往是需要些相關性的,那麼父線程的ThreadLocal中存儲的資料能在子線程中使用嗎?答案是不行......那怎麼能讓父子線程上下文能關聯起來,Java 為這種情況專門提供了InheritableThreadLocal 給我們使用。

InheritableThreadLocal 是 ThreadLocal 的一個子類,其定義如下:

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    
    protected T childValue(T parentValue) {
        return parentValue;
    }

    /**
     * Get the map associated with a ThreadLocal.
     *
     * @param t the current thread
     */
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    /**
     * Create the map associated with a ThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the table.
     */
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}
複制代碼           

與 ThreadLocal 讓線程擁有變量在本地存儲的副本這個形式不同的是,InheritableThreadLocal 允許讓建立它的線程和其子線程都能通路到在它裡面存儲的值。

下面是一個 InheritableThreadLocal 的使用示例

// 源碼: https://github.com/kevinyan815/JavaXPlay/blob/main/src/com/threadlocal/InheritableThreadLocalExample.java
package com.threadlocal;

public class InheritableThreadLocalExample {

    public static void main(String[] args) {

        ThreadLocal<String> threadLocal = new ThreadLocal<>();
        InheritableThreadLocal<String> inheritableThreadLocal =
                new InheritableThreadLocal<>();

        Thread thread1 = new Thread(() -> {
            System.out.println("===== Thread 1 =====");
            threadLocal.set("Thread 1 - ThreadLocal");
            inheritableThreadLocal.set("Thread 1 - InheritableThreadLocal");

            System.out.println(threadLocal.get());
            System.out.println(inheritableThreadLocal.get());

            Thread childThread = new Thread( () -> {
                System.out.println("===== ChildThread =====");
                System.out.println(threadLocal.get());
                System.out.println(inheritableThreadLocal.get());
            });
            childThread.start();
        });

        thread1.start();

        Thread thread2 = new Thread(() -> {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("===== Thread2 =====");
            System.out.println(threadLocal.get());
            System.out.println(inheritableThreadLocal.get());
        });
        thread2.start();
    }
}
複制代碼           

運作程式後,會有如下輸出

===== Thread 1 =====
Thread 1 - ThreadLocal
Thread 1 - InheritableThreadLocal
===== ChildThread =====
null
Thread 1 - InheritableThreadLocal
===== Thread2 =====
null
null
複制代碼           

這個例程中建立了分别建立了 ThreadLocal 和 InheritableThreadLocal的 執行個體,然後例程中建立的線程Thread1, 線上程 Thread1中向 ThreadLocal 和 InheritableThreadLocal 執行個體中都存儲了資料,并嘗試在開啟了的子線程 ChildThread 中通路這兩個資料。按照上面的解釋,ChildThread 應該隻能通路到父線程存儲在 InheritableThreadLocal 執行個體中的資料。

在例程的最後,程式又建立了一個與 Thread1 不相幹的線程 Thread2, 它在通路 ThreadLocal 和 InheritableThreadLocal 執行個體中存儲的資料時,因為它自己沒有設定過,是以最後得到的結果都是 null。

ThreadLocal 的實作原理

梳理完 ThreadLocal 相關的常用功能都怎麼使用後,我們再來簡單過一下 ThreadLocal 在 Java 中的實作原理。

在 Thread 類中維護着一個 ThreadLocal.ThreadLocalMap 類型的成員變量threadLocals。這個成員變量就是用來存儲目前線程獨占的變量副本的。

public class Thread implements Runnable {
    // ...
    ThreadLocal.ThreadLocalMap threadLocals = null;
    // ...
}
複制代碼           

ThreadLocalMap類 是 ThreadLocal 中的靜态内部類,其定義如下。

package java.lang;

public class ThreadLocal<T> {
    // ...
	static class ThreadLocalMap {
    	// ...
    	static class Entry extends WeakReference<ThreadLocal<?>> {
        	/** The value associated with this ThreadLocal. */
        	Object value;

        	Entry(ThreadLocal<?> k, Object v) {
            	super(k);
            	value = v;
        	}
    	}
    	// ...
	}
}
複制代碼           

它維護着一個 Entry 數組,Entry 繼承了 WeakReference ,是以是弱引用。 Entry 用于儲存鍵值對,其中:

  • key 是 ThreadLocal 對象;
  • value 是傳遞進來的對象(變量副本)。

ThreadLocalMap 雖然是類似 HashMap 結構的資料結構,但它解決哈希碰撞的時候,使用的方案并非像 HashMap 那樣使用拉鍊法(用連結清單儲存沖突的元素)。

實際上,ThreadLocalMap 采用了線性探測的方式來解決哈希碰撞沖突。所謂線性探測,就是根據初始 key 的 hashcode 值确定元素在哈希表數組中的位置,如果發現這個位置上已經被其他的 key 值占用,則利用固定的算法尋找一定步長的下個位置,依次判斷,直至找到能夠存放的位置。

總結

關于 ThreadLocal 的内容就介紹到這了,這塊内容在一些基礎的面試中還是挺常被問到的,與它一起經常被問到的還有一個 volatile 關鍵字,這部分内容我們放到下一篇再講,喜歡本文的内容還請給點個贊,點個關注,這樣就能及時跟上後面的更新啦。

引用連結

  • Java并發程式設計--多線程間的同步控制和通信
  • 看了這篇Java 泛型通關指南,再也不怵滿屏尖括号了
  • Java Lambda 表達式的各種形态和使用場景,看這篇就夠了
  • Java 中那些繞不開的内置接口 -- 函數式程式設計和 Java 的内置函數式接口
  • ThreadLocal變量操作示例--增删查源代碼

原文連結:https://juejin.cn/post/7170614613683175454

來源:稀土掘金

繼續閱讀