ThreadLocal詳解

使用ThreadLocal的原因

在多線程訪問的時候,爲了解決線程安全問題,使用 synchronized 關鍵字來實現線程同步的可以解決多線程併發訪問的安全問題,但是在這種解決方案存在有性能問題,多個線程訪問到的都是同一份變量的內容,在多線程同時訪問的時候每次只允許一個線程讀取變量內容,對變量值進行訪問或者修改,其他線程只能處於排隊等候狀態,順序執行,誰先搶佔到系統資源誰先執行,導致系統效率低下。這是一種以延長訪問時間來換取線程安全性的策略。簡單來說就是以時間長度換取線程安全,在多用戶併發訪問的時候,由於等待時間太長,這對用戶來說是不能接受的。

而使用 ThreadLocal 類,該類在每次實例化創建線程的時候都爲每一個線程在本地變量中創建了自己獨有的變量副本。每個線程都擁有了自己獨立的一個變量,競爭條件被徹底消除了,那就沒有必要使用 synchronized 關鍵字對這些線程進行同步,它們也能最大限度的使用系統資源,由CPU調度併發執行。並且由於每個線程在訪問該變量時,讀取和修改的,都是自己獨有的那一份變量拷貝副本,不會對其他的任何副本產生影響,併發錯誤出現的可能也完全消除了。對比前一種方案,這是一種以空間來換取線程安全性的策略。在效率上來說比同步高了很多,可以應對多線程併發訪問。

通過查看ThreadLocal類源碼,該類中提供了兩個主要的方法 get()set(),還有一個用於回收本地變量中的方法remove()也是常用的如下:

下面是 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);
    }

set() 中通過 getMap(Thread t)方法獲取一個和當前線程相關的 ThreadLocalMap,然後將變量的值設置到這個
ThreadLocalMap對象中,如果獲取到的 ThreadLocalMap 對象爲空,就通過createMap()方法創建。

線程隔離的祕密,就在於 ThreadLocalMap這個類。ThreadLocalMapThreadLocal類的一個靜態內部類,它實現了鍵值對的設置和獲取(類似於 Map<K,V> 存儲的key-value),每個線程中都有一個獨立的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
     */
    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;

下面來看一下 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) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        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() 方法是設置變量的拷貝副本,get() 方法通過鍵值對的方式獲取到這個本地變量的副本的value

remove() 源碼分析:

/**
     * Removes the current thread's value for this thread-local
     * variable.  If this thread-local variable is subsequently
     * {@linkplain #get read} by the current thread, its value will be
     * reinitialized by invoking its {@link #initialValue} method,
     * unless its value is {@linkplain #set set} by the current thread
     * in the interim.  This may result in multiple invocations of the
     * {@code initialValue} method in the current thread.
     *
     * @since 1.5
     */
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

通過源碼可以知道,該方法就是通過 this 找到ThreadLocalMap 中保存的變量副本做回收處理。

具體實現例子(實現數據庫的連接關閉)

下面看一個使用 ThreadLocal 實現的數據庫 Connection 連接:

package cn.czl.util.dbc;

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

/**
 * 爲了更加方便的傳輸關鍵的引用對象(Connection),所以在本類之中將利用ThreadLocal進行數據庫連接的保存
 * 這個類中的ThreadLocal應該作爲一個公共的數據,公共的數據使用static聲明
 * @author czl
 */
public class DatabaseConnection  {
    private static final String DBDRIVER = "oracle.jdbc.driver.OracleDriver" ;
    private static final String DBURL = "jdbc:oracle:thin:@localhost:1521:orcl" ;
    private static final String USER = "scott" ;
    private static final String PASSWORD = "tiger" ;
    private static ThreadLocal<Connection> threadLocal = new ThreadLocal<Connection>() ;
    /**
     * 取得數據庫的連接對象,如果在取得的時候沒有進行連接則自動創建新的連接
     * 但是需要防止同一個線程可能重複調用此操作的問題
     * @return
     */
    public static Connection getConnection() {
        Connection conn = threadLocal.get() ;   // 首先判斷一下在ThreadLocal裏面是否保存有當前連接對象
        if (conn == null) { // 如果此時的連接對象是空,那麼就表示沒有連接過,則創建一個新的連接
            conn = rebuildConnection() ;    // 創建新的連接
            threadLocal.set(conn);  // 保存到ThreadLocal之中,以便一個線程執行多次數據庫的時候使用
        }
        return conn ;   // 返回連接對象
    }
    /**
     * 重新建立新的數據庫連接
     * @return Connection接口對象
     */
    private static Connection rebuildConnection() { // 創建新的連接對象
        try {
            Class.forName(DBDRIVER) ;
            return DriverManager.getConnection(DBURL,USER,PASSWORD); 
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null ;
    }
    /**
     * 關閉數據庫連接
     */
    public static void close() {
        System.out.println(Thread.currentThread().getName() + " 關閉數據庫連接。");
        Connection conn = threadLocal.get() ;   // 取得數據庫連接
        if (conn != null) {
            try {
                conn.close();
                threadLocal.remove();   // 從ThreadLocal中刪除掉保存的數據
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

當用戶需要調用連接數據庫的時候,只需要通過 DatabaseConnection.getConnection()連接數據庫,在實現業務邏輯的時候,每次調用都會爲調用處創建一個連接數據庫的線程副本,每次所作的修改互不干擾,在多用戶併發訪問使用的時候,很好的避免了使用同步帶來的性能問題。

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