1. 程式人生 > >[轉]Java 併發:深入理解 ThreadLocal

[轉]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 classThreadimplementsRunnable {

    ...

    /* 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 classUniqueThreadIdGenerator {
    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 classTest {

    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()方法獲取要操作的資料,這樣可以保證每個執行緒對應一個數據物件,在任何時刻都操作的是這個物件,不會交叉。

引用