Java 併發編程:深入理解 ThreadLocal




摘要:

  ThreadLocal 又名線程局部變量,是 Java 中一種較爲特殊的線程綁定機制,用於保證變量在不同線程間的隔離性,以方便每個線程處理自己的狀態。進一步地,本文以ThreadLocal類的源碼爲切入點,深入分析了ThreadLocal類的作用原理,並給出應用場景和一般使用步驟。


一. 對 ThreadLocal 的理解

1). ThreadLocal 概述

  ThreadLocal 又名 線程局部變量 ,是 Java 中一種較爲特殊的線程綁定機制,可以爲每一個使用該變量的線程都提供一個變量值的副本,並且每一個線程都可以獨立地改變自己的副本,而不會與其它線程的副本發生衝突。一般而言,通過 ThreadLocal 存取的數據總是與當前線程相關,也就是說,JVM 爲每個運行的線程綁定了私有的本地實例存取空間,從而爲多線程環境常出現的併發訪問問題提供了一種 隔離機制

  如果一段代碼中所需要的數據必須與其他代碼共享,那就看看這些共享數據的代碼能否保證在同一個線程中執行?如果能保證,我們就可以把共享數據的可見範圍限制在同一個線程之內,這樣,無須同步也能保證線程之間不出現數據爭用的問題。也就是說,如果一個某個變量要被某個線程 獨享,那麼我們就可以通過ThreadLocal來實現線程本地存儲功能。


2). ThreadLocal 在 JDK 中的定義

ThreadLocal
This class provides thread-local variables. These variables differ from their normal counterparts(副本) in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread ( e.g., a user ID or Transaction ID ). (如果我們希望通過將某個類的狀態(例如用戶ID、事務ID)與線程關聯起來,那麼通常在這個類中定義private static類型的ThreadLocal 實例。)

Each thread holds an implicit reference to its copy of a thread-local variable (見下圖) as long as the thread is alive and the ThreadLocal instance is accessible; after a thread goes away, all of its copies of thread-local instances are subject to garbage collection (unless other references to these copies exist).

public class Thread implements Runnable {

    ...

    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

我們可以從中摘出三條要點:

  • 每個線程都有關於該 ThreadLocal變量 的私有值
     每個線程都有一個獨立於其他線程的上下文來保存這個變量的值,並且對其他線程是不可見的。

  • 獨立於變量的初始值
     ThreadLocal 可以給定一個初始值,這樣每個線程就會獲得這個初始化值的一個拷貝,並且每個線程對這個值的修改對其他線程是不可見的。

  • 將某個類的狀態與線程相關聯
     我們從JDK中對ThreadLocal的描述中可以看出,ThreadLocal的一個重要作用是就是將類的狀態與線程關聯起來,這個時候通常的解決方案就是在這個類中定義一個 private static ThreadLocal 實例。


3). 應用場景

  類 ThreadLocal 主要解決的就是爲每個線程綁定自己的值,以方便其處理自己的狀態。形象地講,可以將 ThreadLocal變量 比喻成全局存放數據的盒子,盒子中可以存儲每個線程的私有數據。例如,以下類用於生成對每個線程唯一的局部標識符。線程 ID 是在第一次調用 uniqueNum.get() 時分配的,在後續調用中不會更改。

import java.util.concurrent.atomic.AtomicInteger;

public class UniqueThreadIdGenerator {
    private static final AtomicInteger uniqueId = new AtomicInteger(0);

    private static final ThreadLocal<Integer> uniqueNum = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return uniqueId.getAndIncrement();
        }
    };

    public static void main(String[] args) {
        Thread[] threads = new Thread[5];
        for (int i = 0; i < 5; i++) {
            String name = "Thread-" + i;
            threads[i] = new Thread(name){
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + ": "
                            + uniqueNum.get());
                }
            };
            threads[i].start();
        }

        System.out.println(Thread.currentThread().getName() + ": "
                + uniqueNum.get());
    }
}/* Output(輸出結果不唯一): 
        Thread-1: 2
        Thread-0: 0
        Thread-2: 3
        main: 1
        Thread-3: 4
        Thread-4: 5
 *///:~
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

二. 深入分析ThreadLocal類

  下面,我們來看一下 ThreadLocal 的具體實現,該類一共提供的四個方法:

public T get() { }
public void set(T value) { }
public void remove() { }
protected T initialValue() { }
  • 1
  • 2
  • 3
  • 4

  其中,get()方法是用來獲取 ThreadLocal變量 在當前線程中保存的值,set() 用來設置 ThreadLocal變量 在當前線程中的值,remove() 用來移除當前線程中相關 ThreadLocal變量,initialValue() 是一個 protected 方法,一般需要重寫。


1、 原理探究

1). 切入點:get()

  首先,我們先看其源碼:

    /**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    public T get() {
        Thread t = Thread.currentThread();    // 獲取當前線程對象
        ThreadLocalMap map = getMap(t);     // 獲取當前線程的成員變量 threadLocals
        if (map != null) {
            // 從當前線程的 ThreadLocalMap 獲取該 thread-local variable 對應的 entry
            ThreadLocalMap.Entry e = map.getEntry(this);    
            if (e != null)      
                return (T)e.value;   // 取得目標值
        }
        return setInitialValue();  
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

2).關鍵點:setInitialValue()

/**
     * Variant of set() to establish initialValue. Used instead
     * of set() in case user has overridden the set() method.
     *
     * @return the initial value
     */
    private T setInitialValue() {
        T value = initialValue();     // 默認實現返回 null
        Thread t = Thread.currentThread();   // 獲得當前線程
        ThreadLocalMap map = getMap(t);     // 得到當前線程 ThreadLocalMap類型域 threadLocals
        if (map != null)
            map.set(this, value);  // 該 map 的鍵是當前 ThreadLocal 對象
        else
            createMap(t, value);   
        return value;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

  我們緊接着看上述方法涉及到的三個方法:initialValue(),set(this, value) 和 createMap(t, value)。

(1) initialValue()

   /**
     * Returns the current thread's "initial value" for this
     * thread-local variable.  This method will be invoked the first
     * time a thread accesses the variable with the {@link #get}
     * method, unless the thread previously invoked the {@link #set}
     * method, in which case the <tt>initialValue</tt> method will not
     * be invoked for the thread.  Normally, this method is invoked at
     * most once per thread, but it may be invoked again in case of
     * subsequent invocations of {@link #remove} followed by {@link #get}.
     *
     * <p>This implementation simply returns <tt>null</tt>; if the
     * programmer desires thread-local variables to have an initial
     * value other than <tt>null</tt>, <tt>ThreadLocal</tt> must be
     * subclassed, and this method overridden.  Typically, an
     * anonymous inner class will be used.
     *
     * @return the initial value for this thread-local
     */
    protected T initialValue() {
        return null;            // 默認實現返回 null
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

(2) createMap()

/**
     * Create the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the map
     * @param map the map to store.
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue); // this 指代當前 ThreadLocal 變量,爲 map 的鍵  
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

至此,可能大部分朋友已經明白了 ThreadLocal類 是如何爲每個線程創建變量的副本的:

  ① 每個線程內部有一個 ThreadLocal.ThreadLocalMap 類型的成員變量 threadLocals,這個 threadLocals 存儲了與該線程相關的所有 ThreadLocal 變量及其對應的值(”ThreadLocal 變量及其對應的值” 就是該Map中的一個 Entry)。我們知道,Map 中存放的是一個個 Entry,其中每個 Entry 都包含一個 Key 和一個 Value。在這裏,就threadLocals 而言,它的 Entry 的 Key 是 ThreadLocal 變量, Value 是該 ThreadLocal 變量對應的值;

  ② 初始時,在Thread裏面,threadLocals爲空,當通過ThreadLocal變量調用get()方法或者set()方法,就會對Thread類中的threadLocals進行初始化,並且以當前ThreadLocal變量爲鍵值,以ThreadLocal要保存的值爲value,存到 threadLocals;

  ③ 然後在當前線程裏面,如果要使用ThreadLocal對象,就可以通過get方法獲得該線程的threadLocals,然後以該ThreadLocal對象爲鍵取得其對應的 Value,也就是ThreadLocal對象中所存儲的值。


2、實例驗證

  下面通過一個例子來證明通過ThreadLocal能達到在每個線程中創建變量副本的效果:

public class Test {

    ThreadLocal<Long> longLocal = new ThreadLocal<Long>();
    ThreadLocal<String> stringLocal = new ThreadLocal<String>();

    public void set() {
        longLocal.set(Thread.currentThread().getId());
        stringLocal.set(Thread.currentThread().getName());
    }

    public long getLong() {
        return longLocal.get();
    }

    public String getString() {
        return stringLocal.get();
    }

    public static void main(String[] args) throws InterruptedException {
        final Test test = new Test();

        test.set();
        System.out.println("父線程 main :");
        System.out.println(test.getLong());
        System.out.println(test.getString());

        Thread thread1 = new Thread() {
            public void run() {
                test.set();
                System.out.println("\n子線程 Thread-0 :");
                System.out.println(test.getLong());
                System.out.println(test.getString());
            };
        };
        thread1.start();
    }
}/* Output: 
        父線程 main :
                    1
                    main

        子線程 Thread-0 :
                    12
                    Thread-0
 *///:~
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45

  從這段代碼的輸出結果可以看出,在main線程中和thread1線程中,longLocal保存的副本值和stringLocal保存的副本值都不一樣,並且進一步得出:

  • 實際上,通過 ThreadLocal 創建的副本是存儲在每個線程自己的threadLocals中的;

  • 爲何 threadLocals 的類型 ThreadLocalMap 的鍵值爲 ThreadLocal 對象,因爲每個線程中可有多個 threadLocal變量,就像上面代碼中的 longLocal 和 stringLocal;

  • 在進行get之前,必須先set,否則會報空指針異常;若想在get之前不需要調用set就能正常訪問的話,必須重寫initialValue()方法。


三. ThreadLocal的應用場景

  在 Java 中,類 ThreadLocal 解決的是變量在不同線程間的隔離性。最常見的 ThreadLocal 使用場景有 數據庫連接問題、Session管理等。

(1) 數據庫連接問題

private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
    public Connection initialValue() {
        return DriverManager.getConnection(DB_URL);
    }
};

public static Connection getConnection() {
    return connectionHolder.get();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

(2) Session管理

private static final ThreadLocal threadSession = new ThreadLocal();

public static Session getSession() throws InfrastructureException {
    Session s = (Session) threadSession.get();
    try {
        if (s == null) {
            s = getSessionFactory().openSession();
            threadSession.set(s);
        }
    } catch (HibernateException ex) {
        throw new InfrastructureException(ex);
    }
    return s;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

(3) Thread-per-Request (一個請求對應一個服務器線程)

  在經典Web交互模型中,請求的處理基本上採用的都是“一個請求對應一個服務器線程”的處理方式,因此就可以將請求設置成類似ThreadLocal<Request>的形式,這樣,當某個服務器線程來處理請求時,就可以獨享該請求的處理了。


四. ThreadLocal 一般使用步驟

ThreadLocal 使用步驟一般分爲三步:

  • 創建一個 ThreadLocal 對象 threadXxx,用來保存線程間需要隔離處理的對象 xxx;

  • 提供一個獲取要隔離訪問的數據的方法 getXxx(),在方法中判斷,若 ThreadLocal對象爲null時候,應該 new() 一個隔離訪問類型的對象;

  • 在線程類的run()方法中,通過getXxx()方法獲取要操作的數據,這樣可以保證每個線程對應一個數據對象,在任何時刻都操作的是這個對象,不會交叉。



引用

《Java 併發編程實戰》
《Java 多線程編程核心技術》
[Java併發編程:Thread類的使用][16]
Java併發編程:深入剖析ThreadLocal
正確理解ThreadLocal
深入理解ThreadLocal
深入研究java.lang.ThreadLocal類





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