【多線程】ThreadLocal的解惑

首先,我沒有用過ThreadLocal類,前幾天被問及相關使用場景和實現原理,一臉懵逼,只知道是本地線程,所以特學習一下相關的知識

由於水平所限,歡迎批評指正

什麼是ThreadLocal

引用百度百科的解釋:早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal爲解決多線程程序的併發問題提供了一種新的思路。使用這個工具類可以很簡潔地編寫出優美的多線程程序。
ThreadLocal很容易讓人望文生義,想當然地認爲是一個“本地線程”。其實,ThreadLocal並不是一個Thread,而是Thread的局部變量,也許把它命名爲ThreadLocalVariable更容易讓人理解一些。

可是還是不明白,翻閱資料,覺得這個解釋是比較容易理解的:

如果你創建一個類對象,實現Runnable接口,然後多個Thread對象使用同樣的Runnable對象,全部的線程都共享同樣的屬性。這意味着,如果你在一個線程裏改變一個屬性,全部的線程都會受到這個改變的影響。
BUT
有時,你希望程序裏的各個線程的屬性不會被共享。 Java 併發 API提供了一個很清楚的機制叫本地線程變量。

我們看一個舉個栗子分析一下

class ConnectionManager {

    private static Connection connect = null;

    public static Connection openConnection() {
        if(connect == null){
            connect = DriverManager.getConnection();
        }
        return connect;
    }

    public static void closeConnection() {
        if(connect!=null)
            connect.close();
    }
}

假設有這樣一個數據庫鏈接管理類,這段代碼在單線程中使用是沒有任何問題的,但是如果在多線程中使用呢?很顯然,在多線程中使用會存在線程安全問題:第一,這裏面的2個方法都沒有進行同步,很可能在openConnection方法中會多次創建connect;第二,由於connect是共享變量,那麼必然在調用connect的地方需要使用到同步來保障線程安全,因爲很可能一個線程在使用connect進行數據庫操作,而另外一個線程調用closeConnection關閉鏈接。
BUT

我們真的需要共享connect變量嗎?真的必須保證線程是同步執行的嗎?答案是否定的。
假如每個線程中都有一個connect變量,各個線程之間對connect變量的訪問實際上是沒有依賴關係的,即一個線程不需要關心其他線程是否對這個connect進行了修改的。如果有線程關閉掉了connect的連接,那麼我們正在使用數據庫的其他線程就會無法與數據庫進行訪問,由於多個線程之間沒有依賴關係,那麼我們也不是必須要保證線程同步。這不是我們希望的場景。

 到這裏,可能會有朋友想到,既然不需要在線程之間共享這個變量,可以直接這樣處理,在每個需要使用數據庫連接的方法中具體使用時才創建數據庫鏈接,然後在方法調用完畢再釋放這個連接。比如下面這樣:

class ConnectionManager {

    private  Connection connect = null;

    public Connection openConnection() {
        if(connect == null){
            connect = DriverManager.getConnection();
        }
        return connect;
    }

    public void closeConnection() {
        if(connect!=null)
            connect.close();
    }
}


class Dao{
    public void insert() {
        ConnectionManager connectionManager = new ConnectionManager();
        Connection connection = connectionManager.openConnection();

        //使用connection進行操作
         doSomething();
        connectionManager.closeConnection();
    }
}

這樣處理確實也沒有任何問題,由於每次都是在方法內部創建的連接,那麼線程之間自然不存在線程安全問題。但是這樣會有一個致命的影響:導致服務器壓力非常大,並且嚴重影響程序執行性能。由於在方法中需要頻繁地開啓和關閉數據庫連接,這樣不僅嚴重影響程序執行效率,還可能導致服務器壓力巨大。

那麼這種情況下使用ThreadLocal是再適合不過的了,因爲ThreadLocal在每個線程中對該變量會創建一個副本,即每個線程內部都會有一個該變量,且在線程內部任何地方都可以使用,線程之間互不影響,這樣一來就不存在線程安全問題,也不會嚴重影響程序執行性能。

但是要注意,雖然ThreadLocal能夠解決上面說的問題,但是由於在每個線程中都創建了副本,所以要考慮它對資源的消耗,比如內存的佔用會比不使用ThreadLocal要大。

深入解析ThreadLocal類

解析.ThreadLocal類,無外乎就是閱讀源碼

我們先看一下ThreadLocal裏面的幾個重要的方法

public T get() { }
public void set(T value) { }
public void remove() { }
protected T initialValue() { }
  • get方法
    get()方法是用來獲取ThreadLocal在當前線程中保存的變量副本

    看一下相關源碼:

    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();
    }

第一句是取得當前線程,然後通過getMap(t)方法獲取到一個map,map的類型爲ThreadLocalMap。然後接着下面獲取到 < key, value>鍵值對,注意這裏獲取鍵值對傳進去的是 this,而不是當前線程t。
如果獲取成功,則返回value值。

如果map爲空,則調用setInitialValue方法返回value。
首先看一下getMap方法中做了什麼:

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

  可能大家沒有想到的是,在getMap中,是調用當期線程t,返回當前線程t中的一個成員變量threadLocals。

  那麼我們繼續取Thread類中取看一下成員變量threadLocals是什麼:

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

也就是threadLocals 是一個ThreadLocalMap ,那麼ThreadLocalMap 是什麼呢?

  static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal k, Object v) {
                super(k);
                value = v;
            }
        }

 可以看到ThreadLocalMap的Entry繼承了WeakReference,並且使用ThreadLocal作爲鍵值。
 我們再看get方法中的最後一句話,如果map是空則返回setInitialValue();
 

   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;
    }

 很容易瞭解,就是如果map不爲空,就設置鍵值對,爲空,再創建Map,看一下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);
    }

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

  首先,在每個線程Thread內部有一個ThreadLocal.ThreadLocalMap類型的成員變量threadLocals,這個threadLocals就是用來存儲實際的變量副本的,鍵值爲當前ThreadLocal變量,value爲變量副本(即T類型的變量)。

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

  然後在當前線程裏面,如果要使用副本變量,就可以通過get方法在threadLocals裏面查找。

  • set方法
    set()用來設置當前線程中變量的副本

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方法時,直接set值即可,實際上剛纔我們談到的getMap方法,返回當前線程t中的一個成員變量threadLocals,那麼當獲得的Map不爲空,我們還是需要執行createMap方法,來創建當前線程可用的ThreadLocalMap,如果已經創建好了Map,那麼我們就可以直接對Map進行set操作,同樣set的key爲this。

  • remove方法
    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);
     }

remove的方法我們可以看到,實際上我們要remove掉的是當前線程ThreadLocalMap 裏的值,我們當然要調用的是ThreadLocalMap 裏面的remove的方法
源碼如下:

 /**
         * Remove the entry for key.
         */
        private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

我們可以看到,先計算出來key在map中的地址,然後再進行遍歷查找,找到對應的entry就clear掉,也就是相當於remove了。

  • initialValue 方法
    initialValue()是一個protected方法,一般是用來在使用時進行重寫的,它是一個 延遲加載方法

    /**
     * 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 {@code initialValue} 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 {@code null}; if the
     * programmer desires thread-local variables to have an initial
     * value other than {@code null}, {@code ThreadLocal} 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,一般情況下,我們會對這個方法進行重寫,來獲得一些細節

舉個例子

下面通過一個例子來證明通過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(test.getLong());
        System.out.println(test.getString());


        Thread thread1 = new Thread(){
            public void run() {
                test.set();
                System.out.println(test.getLong());
                System.out.println(test.getString());
            };
        };
        thread1.start();
        thread1.join();

        System.out.println(test.getLong());
        System.out.println(test.getString());
    }
}

這裏寫圖片描述

從這段代碼的輸出結果可以看出,在main線程中和thread1線程中,longLocal保存的副本值和stringLocal保存的副本值都不一樣。最後一次在main線程再次打印副本值是爲了證明在main線程中和thread1線程中的副本值確實是不同的。

 總結一下:

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

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

  3)在進行get之前,必須先set,否則會報空指針異常;

   如果想在get之前不需要調用set就能正常訪問的話,必須重寫initialValue()方法。

    因爲在上面的代碼分析過程中,我們發現如果沒有先set的話,即在map中查找不到對應的存儲,則會通過調用setInitialValue方法返回,而在setInitialValue方法中,有一個語句是T value = initialValue(), 而默認情況下,initialValue方法返回的是null。

看下面這個例子

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();

        System.out.println(test.getLong());
        System.out.println(test.getString());

        Thread thread1 = new Thread(){
            public void run() {
                test.set();
                System.out.println(test.getLong());
                System.out.println(test.getString());
            };
        };
        thread1.start();
        thread1.join();

        System.out.println(test.getLong());
        System.out.println(test.getString());
    }
}

 在main線程中,沒有先set,直接get的話,運行時會報空指針異常。
 這裏寫圖片描述
 我們分析返回值是null沒有問題,空指針異常的問題是由於null需要強制轉換爲long類型是報的錯誤

 如果改成下面這段代碼,即重寫了initialValue方法:

    ThreadLocal<Long> longLocal= new ThreadLocal<Long>(){;
        protected String initialValue() {
            return Thread.currentThread().getId();
        };
    };

 就可以直接不用先set而直接調用get了。
 

ThreadLocal的應用場景

最常見的ThreadLocal使用場景爲 用來解決 數據庫連接、Session管理等。
如:

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

public static Connection getConnection() {
return connectionHolder.get();
}
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;
}

參考文獻:
https://www.cnblogs.com/dolphin0520/p/3920407.html
https://baike.baidu.com/item/ThreadLocal/4915311?fr=aladdin
http://ifeve.com/thread-management-10/

本文總結了上述文獻的一些內容,增加了對於一些方法的講解,對於已知的描述不清之處進行了修正

最後非常感謝 張孝祥 老師,深入淺出講明白了ThreadLocal本文中的部分代碼也是套用了張孝祥老師的演示代碼。附上視頻鏈接,侵權刪。
https://www.bilibili.com/video/av7592261/

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