Java高級編程:深入剖析ThreadLocal

以下是本文目錄大綱:
  一.對ThreadLocal的理解
  二.深入解析ThreadLocal類
  三.ThreadLocal的應用場景
       四.總結
 
一. 對ThreadLocal的理解
 
      ThreadLocal,很多地方叫做線程本地變量,也有些地方叫做線程本地存儲,其實意思差不多。可能很多朋友都知道ThreadLocal爲變量在每個線程中都創建了一個副本,那麼每個線程可以訪問自己內部的副本變量。這句話從字面上看起來很容易理解,但是真正理解並不是那麼容易。
 
我們還是先來看一個例子:

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關閉鏈接。所以出於線程安全的考慮,必須將這段代碼的兩個方法進行同步處理,並且在調用connect的地方需要進行同步處理。這樣將會大大影響程序執行效率,因爲一個線程在使用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進行操作
        connectionManager.closeConnection();
    }
}

這樣處理確實也沒有任何問題,由於每次都是在方法內部創建的連接,那麼線程之間自然不存在線程安全問題。但是這樣會有一個致命的影響:導致服務器壓力非常大,並且嚴重影響程序執行性能。由於在方法中需要頻繁地開啓和關閉數據庫連接,這樣不盡嚴重影響程序執行效率,還可能導致服務器壓力巨大。那麼這種情況下使用ThreadLocal是再適合不過的了,因爲ThreadLocal在每個線程中對該變量會創建一個副本,即每個線程內部都會有一個該變量,且在線程內部任何地方都可以使用,線程之間互不影響,這樣一來就不存在線程安全問題,也不會嚴重影響程序執行性能。但是要注意,雖然ThreadLocal能夠解決上面說的問題,但是由於在每個線程中都創建了副本,所以要考慮它對資源的消耗,比如內存的佔用會比不使用ThreadLocal要大。
 
二. 深入解析ThreadLocal類
   在上面談到了對ThreadLocal的一些理解,那我們下面來看一下具體ThreadLocal是如何實現的。
先了解一下ThreadLocal類提供的幾個方法:

public T get() { }
public void set(T value) { }
public void remove() { }
protected T initialValue() { } 

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

首先我們來看一下ThreadLocal類是如何爲每個線程創建一個變量的副本的。
先看下set方法的實現:

/** 
 * Sets the current thread's copy of this thread-local variable 
 * to the specified value.  Many applications will have no need for 
 * this functionality, relying solely on the {@link #initialValue} 
 * method to set the values of thread-locals. 
 * 
 * @param value the value to be stored in the current threads' copy of 
 *        this thread-local. 
 */  
public void set(T value) {  
   Thread t = Thread.currentThread();  
   ThreadLocalMap map = getMap(t);  // 獲取當前線程對應的ThreadLocalMap
   if (map != null)  
      map.set(this, value);  // 把當前ThreadLocal作爲key,線程中的變量value作爲對應的值
   else  
      createMap(t, value);  // ThreadLocalMap不存在時創建
} 
get方法的實現:
/** 
 * Returns the value in the current thread's copy of this thread-local 
 * variable.  Creates and initializes the copy if this is the first time 
 * the thread has called this 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)  
      return (T)map.get(this);  // 返回ThreadLocalMap中ThreadLocal對應的線程中的變量值
   // Maps are constructed lazily.  if the map for this thread doesn't exist, create it, with this ThreadLocal and its  
   // initial value as its only entry.  
   T value = initialValue();  // 
   createMap(t, value);  
   return value;  
}  

第一句是取得當前線程,然後通過getMap(t)方法獲取到一個map,map的類型爲ThreadLocalMap。然後接着下面獲取到<key,value>鍵值對,注意這裏獲取鍵值對傳進去的是  this,而不是當前線程t。如果獲取成功,則返回value值。如果map爲空,則調用setInitialValue方法返回value。
 
我們上面的每一句來仔細分析:
首先看一下getMap方法中做了什麼:

/** 
 * 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;  // 一個線程對應一個ThreadLocalMap
}

可能大家沒有想到的是,在getMap中,是調用當期線程t,返回當前線程t中的一個成員變量threadLocals。
那麼我們繼續取Thread類中取看一下成員變量threadLocals是什麼:

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

實際上就是一個ThreadLocalMap,這個類型是ThreadLocal類的一個內部類,我們繼續取看ThreadLocalMap的實現:可以看到ThreadLocalMap的Entry繼承WeakReference,並且使用ThreadLocal作爲鍵值。
然後再繼續看setInitialValue方法的具體實現:

/**
 *  Variant of set() to establish initialValue. Use instead of set() in case
 *  user has overridden the set() method.
 *  @return the initial value
 */
 private T setInitialValue(){
    T value = initailValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if(map != null)
       map.set(this, value);
    else
       createMap(t, value);
    return value;
 }
很容易瞭解,就是如果map不爲空,就設置鍵值對,爲空,再創建Map,看一下createMap的實現:
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;
   }
}

至此,可能大部分朋友已經明白了ThreadLocal是如何爲每個線程創建變量的副本的:
首先,在每個線程Thread內部有一個ThreadLocal.ThreadLocalMap類型的成員變量threadLocals,這個threadLocals就是用來存儲實際的變量副本的,鍵值爲當前ThreadLocal變量,value爲變量副本(即T類型的變量)。初始時,在Thread裏面,threadLocals爲空,當通過ThreadLocal變量調用get()方法或者set()方法,就會對Thread類中的threadLocals進行初始化,並且以當前ThreadLocal變量爲鍵值,以ThreadLocal要保存的副本變量爲value,存到threadLocals。然後在當前線程裏面,如果要使用副本變量,就可以通過get方法在threadLocals裏面查找。
下面通過一個例子來證明通過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());
    }
} 

這段代碼的輸出結果爲:
1
main
8
Thread-0
1
main

從這段代碼的輸出結果可以看出,在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方法返回i,而在setInitialValue方法中,有一個語句是T value = initialValue(), 而默認情況下,initialValue方法返回的是null。

/** 
  * Returns the value in the current thread's copy of this thread-local 
  * variable.  Creates and initializes the copy if this is the first time 
  * the thread has called this 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)  
      return (T)map.get(this);  
  
   // Maps are constructed lazily.  if the map for this thread  
   // doesn't exist, create it, with this ThreadLocal and its  
   // initial value as its only entry.  
   T value = initialValue();  
   createMap(t, value);  
   return value;  
} 

/**
 *  Variant of set() to establish initialValue. Use instead of set() in case
 *  user has overridden the set() method.
 
 *  @return the initial value
 */
private T setInitialValue(){
   T value = initailValue();
   Thread t = Thread.currentThread();
   ThreadLocalMap map = getMap(t);
   if(map != null)
      map.set(this, value);
   else
      createMap(t, value);
   return value;
}

看下面這個例子:

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的話,運行時會報空指針異常。但是如果改成下面這段代碼,即重寫了initialValue方法:

public class Test {

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

    ThreadLocal<String> stringLocal = new ThreadLocal<String>(){

        protected String initialValue() {
            return Thread.currentThread().getName();
        };
    };

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

就可以直接不用先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;
} 

四. 總結
    一般情況下,通過ThreadLocal.set()到線程中的對象是該線程自己使用的對象,其他線程是不需要訪問的,也訪問不到的,各個線程中訪問的是不同的對象。
總之,ThreadLocal不是用來解決對象共享訪問問題的,而主要是提供了保持對象的方法和避免參數傳遞的方便的對象訪問方式。歸納了兩點:
   1.每個線程中都有一個自己的ThreadLocalMap類對象,可以將線程自己的對象保持到其中,各管各的,線程可以正確的訪問到自己的對象。
   2.將一個共用的ThreadLocal靜態實例作爲key,將不同對象的引用保存到不同線程的ThreadLocalMap中,然後在線程執行的各處通過這個靜態ThreadLocal實例的get()方法取得自己線程保存的那個對象,避免了將這個對象作爲參數傳遞的麻煩。
   3.
一個Thread對應一個ThreadLocalMap,每個ThreadLocal當然只能放一個對象,需要放其他對象就再new一個新的ThreadLocal出來,這個新的ThreadLocal作爲key,需要放的對象作爲value,放在ThreadLocalMap中
 

轉載:http://www.cnblogs.com/dolphin0520/p/3920407.html

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