深入理解ThreadLocal

  JDK 1.2的版本中就提供了java.lang.ThreadLocal,ThreadLocal爲解決多線程程序的併發問題提供了一種新的思路。使用這個工具類可以很簡潔地編寫出優美的多線程程序,ThreadLocal並不是一個Thread,而是專屬於某個Thread的局部變量集。

  上面提到“ThreadLocal爲解決多線程程序的併發問題提供了一種新的思路”,那麼,我們就先來看下多線程併發時帶來的線程安全問題的解決思路。

  synchronized同步機制是解決多線程併發問題的一個重要思路。synchronized利用鎖的機制,使臨界區【指的是一個訪問共用資源(如共用設備或是共用存儲器)的程序片段;臨界區維護的共用資源被稱爲臨界資源】在某一時刻只能被一個線程訪問到,從而保障臨界區的線程安全。因此,synchronized用於線程間的數據共享,通過鎖機制確保某一時刻只能有一個獲得鎖的線程進入臨界區操作臨界資源。

  可以說,ThreadLocal在處理併發問題上採取了與synchronized截然不同的解決方案。按照Java官方解釋,ThreadLocal provides thread-local variables,也就是說,ThreadLocal提供了線程的本地變量,這個變量裏面的值(通過get方法獲取)是和其他線程分割開來的,變量的值只有當前線程能訪問到。實際上,ThreadLocal爲每個線程提供了都需要訪問的變量的副本,這樣的話,每個線程訪問的並非同一個對象,從而隔離了多個線程對數據的共享訪問,實現了線程間的數據隔離,既然線程間各自訪問各自專屬的變量,自然也就不會涉及多線程訪問共享數據時帶來的線程安全問題了。

  通過上面的闡述,總結:synchronized在多線程訪問臨界區時,採用鎖機制保障臨界資源的線程安全;ThreadLocal則是爲每個線程提供一個變量副本(相當於每個線程的局部變量),線程們各自操作自己的“局部變量”即可,互不干擾,也就是說,每一個線程都可以獨立地改變自己的副本,而不會影響其它線程所對應的副本。

  那麼,ThreadLocal都有哪些使用場景呢?大名鼎鼎的Spring框架裏就用到了ThreadLocal。

   Spring的事務管理器通過AOP切入業務代碼,在進入業務代碼前,會依據相應的事務管理器提取出相應的事務對象,假如事務管理器是DataSourceTransactionManager,就會從DataSource中獲取一個連接對象,通過一定的包裝後將其保存在ThreadLocal中。這樣的話,Spring就能讓線程內多次獲取到的Connection對象是同一個。Spring爲什麼要把一個Coonection連接對象放在ThreadLocal裏面呢?這是因爲,Spring通過ThreadLocal保證連接對象始終在線程內部,不論什麼時候都能拿到。因此,Spring就可以實現對這個連接對象的控制。

  那麼,ThreadLocal是如何做到爲每一個線程維護變量的副本的呢?在ThreadLocal類中有一個內部靜態Map,用於存儲每一個線程的變量副本,Map中元素的鍵爲線程對象,而值對應線程的變量副本。

static class ThreadLocalMap {
...
}

   ThreadLocal最常見的操作就是set、get、remove三個操作:

public class ThreadLocal<T> {
    ...
    public T get() {... }
    public void set(T value) {... }
    public void remove() {... }
    protected T initialValue() {... }
    ...
    static class ThreadLocalMap {
         ...       
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        ...
    }
} 

  get()方法是用來獲取ThreadLocal在當前線程中保存的變量副本,set()用來設置當前線程中變量的副本,remove()用來移除當前線程中變量的副本,initialValue()是一個protected方法,一般是用來在使用時進行重寫的,它是一個延遲加載的方法。

  我們來看ThreadLocal是如何爲每個線程創建變量的副本的:

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

public class Thread implements Runnable {
  ...
    ThreadLocal.ThreadLocalMap threadLocals = null;   
  ...
}

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

public class ThreadLocal<T> {
...
public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

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

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

  一般來說,ThreadLocal最常見的使用場景爲:用來解決數據庫連接、Session管理等。案例:

package com.itszt.test8;
/**
 * ThreadLocal
 */
public class ThreadLocalTest {
    /**
     * 聲明兩個ThreadLocal實例
     * 一個用來存儲long型值,即所依附線程的id
     * 一個用來存儲String型對象,即所依附線程的name
     */
    ThreadLocal<Long> longLocal = new ThreadLocal<Long>();
    ThreadLocal<String> stringLocal = new ThreadLocal<String>();

    /**
     * ThreadLocal實例位於哪個線程t下,
     *      就把該線程的成員屬性t.threadLocals賦值一個ThreadLocalMap實例,
     *      接着把當前的ThreadLocal對象作爲該Map的key鍵,
     *      把longLocal.set(Object val)傳入的val作爲該Map的key鍵對應的value值
     *  這樣的話,當前線程下就擁有了一個ThreadLocal.ThreadLocalMap作爲局部變量
     */
    public void set() {
        longLocal.set(Thread.currentThread().getId());
        stringLocal.set(Thread.currentThread().getName());
    }

    /**
     * 下述兩個方法,可以獲取當前線程下相應的數據
     * @return
     */
    public long getLong() {
        return longLocal.get();
    }
    public String getString() {
        return stringLocal.get();
    }

    /**
     * main是主線程
     * @param args
     * @throws InterruptedException
     */
    public static void main(String[] args) throws InterruptedException {
        final ThreadLocalTest test = new ThreadLocalTest();

        test.set();//給主線程下的兩個ThreadLocal中存儲數據
        System.out.println(Thread.currentThread().getName()+"線程編號--"+test.getLong());
        System.out.println(Thread.currentThread().getName()+"名稱--"+test.getString());

        Thread thread1 = new Thread("haha"){
            public void run() {
                test.set();//給當前子線程下的兩個ThreadLocal中存儲數據
                System.out.println(Thread.currentThread().getName()+"線程編號--"+test.getLong());
                System.out.println(Thread.currentThread().getName()+"名稱--"+test.getString());
            };
        };

        thread1.start();//啓動子線程
        thread1.join();//讓父線程(此處爲主線程)等待子線程結束之後,父線程才能繼續運行

        System.out.println(Thread.currentThread().getName()+"線程編號--"+test.getLong());
        System.out.println(Thread.currentThread().getName()+"名稱--"+test.getString());
    }
}

   控制檯打印結果:

main線程編號--1
main名稱--main
haha線程編號--9
haha名稱--haha
main線程編號--1
main名稱--main

   可見,在多線程環境下運用ThreadLocal後,每個線程下都有了自己“局部變量”,從而使得線程間互不干擾,實現了線程安全。

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