1. 程式人生 > >【多執行緒】徹底理解ThreadLocal

【多執行緒】徹底理解ThreadLocal

徹底理解ThreadLocal

知其然


synchronized這類執行緒同步的機制可以解決多執行緒併發問題,在這種解決方案下,多個執行緒訪問到的,都是同一份變數的內容。為了防止在多執行緒訪問的過程中,可能會出現的併發錯誤。不得不對多個執行緒的訪問進行同步,這樣也就意味著,多個執行緒必須先後對變數的值進行訪問或者修改,這是一種以延長訪問時間來換取執行緒安全性的策略。

而ThreadLocal類為每一個執行緒都維護了自己獨有的變數拷貝。每個執行緒都擁有了自己獨立的一個變數,競爭條件被徹底消除了,那就沒有任何必要對這些執行緒進行同步,它們也能最大限度的由CPU排程,併發執行。並且由於每個執行緒在訪問該變數時,讀取和修改的,都是自己獨有的那一份變數拷貝,變數被徹底封閉在每個訪問的執行緒中,併發錯誤出現的可能也完全消除了。對比前一種方案,這是一種以空間來換取執行緒安全性的策略。

來看一個運用ThreadLocal來實現資料庫連線Connection物件執行緒隔離的例子。

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class ConnectionManager {

	private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
		@Override
		protected Connection initialValue() {
			Connection conn = null;
			try {
				conn = DriverManager.getConnection(
						"jdbc:mysql://localhost:3306/test", "username",
						"password");
			} catch (SQLException e) {
				e.printStackTrace();
			}
			return conn;
		}
	};

	public static Connection getConnection() {
		return connectionHolder.get();
	}

	public static void setConnection(Connection conn) {
		connectionHolder.set(conn);
	}
}

通過呼叫ConnectionManager.getConnection()方法,每個執行緒獲取到的,都是和當前執行緒繫結的那個Connection物件,第一次獲取時,是通過initialValue()方法的返回值來設定值的。通過ConnectionManager.setConnection(Connection conn)方法設定的Connection物件,也只會和當前執行緒繫結。這樣就實現了Connection物件在多個執行緒中的完全隔離。在Spring容器中管理多執行緒環境下的Connection物件時,採用的思路和以上程式碼非常相似。

知其所以然

那麼到底ThreadLocal類是如何實現這種“為每個執行緒提供不同的變數拷貝”的呢?先來看一下ThreadLocal的set()方法的原始碼是如何實現的:

    /**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

沒有什麼魔法,在這個方法內部我們看到,首先通過getMap(Thread t)方法獲取一個和當前執行緒相關的ThreadLocalMap,然後將變數的值設定到這個ThreadLocalMap物件中,當然如果獲取到的ThreadLocalMap物件為空,就通過createMap方法建立。


執行緒隔離的祕密,就在於ThreadLocalMap這個類。ThreadLocalMap是ThreadLocal類的一個靜態內部類,它實現了鍵值對的設定和獲取(對比Map物件來理解),每個執行緒中都有一個獨立的ThreadLocalMap副本,它所儲存的值,只能被當前執行緒讀取和修改。ThreadLocal類通過操作每一個執行緒特有的ThreadLocalMap副本,從而實現了變數訪問在不同執行緒中的隔離。因為每個執行緒的變數都是自己特有的,完全不會有併發錯誤。還有一點就是,ThreadLocalMap儲存的鍵值對中的鍵是this物件指向的ThreadLocal物件,而值就是你所設定的物件了。


為了加深理解,我們接著看上面程式碼中出現的getMap和createMap方法的實現:
    /**
     * Get the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param  t the current thread
     * @return the map
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    /**
     * 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);
    }

程式碼已經說的非常直白,就是獲取和設定Thread內的一個叫threadLocals的變數,而這個變數的型別就是ThreadLocalMap,這樣進一步驗證了上文中的觀點:每個執行緒都有自己獨立的ThreadLocalMap物件。開啟java.lang.Thread類的原始碼,我們能得到更直觀的證明:

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

那麼接下來再看一下ThreadLocal類中的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);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null)
                return (T)e.value;
        }
        return 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();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

這兩個方法的程式碼告訴我們,在獲取和當前執行緒繫結的值時,ThreadLocalMap物件是以this指向的ThreadLocal物件為鍵進行查詢的,這當然和前面set()方法的程式碼是相呼應的。


進一步地,我們可以建立不同的ThreadLocal例項來實現多個變數在不同執行緒間的訪問隔離,為什麼可以這麼做?因為不同的ThreadLocal物件作為不同鍵,當然也可以線上程的ThreadLocalMap物件中設定不同的值了。通過ThreadLocal物件,在多執行緒中共享一個值和多個值的區別,就像你在一個HashMap物件中儲存一個鍵值對和多個鍵值對一樣,僅此而已。


設定到這些執行緒中的隔離變數,會不會導致記憶體洩漏呢?ThreadLocalMap物件儲存在Thread物件中,當某個執行緒終止後,儲存在其中的執行緒隔離的變數,也將作為Thread例項的垃圾被回收掉,所以完全不用擔心記憶體洩漏的問題。在多個執行緒中隔離的變數,光榮的生,合理的死,真是圓滿,不是麼?


最後再提一句,ThreadLocal變數的這種隔離策略,也不是任何情況下都能使用的。如果多個執行緒併發訪問的物件例項只允許,也只能建立那麼一個,那就沒有別的辦法了,老老實實的使用同步機制來訪問吧。

參考地址:https://my.oschina.net/lichhao/blog/111362