threadlocal原理及常用應用場景

想必很多朋友對ThreadLocal並不陌生,今天我們就來一起探討下ThreadLocal的使用方法和實現原理。首先,本文先談一下對ThreadLocal的理解,然後根據ThreadLocal類的源碼分析了其實現原理和使用需要注意的地方,最後給出了兩個應用場景。

一.對ThreadLocal的理解

        ThreadLocal,很多地方叫做線程本地變量,也有些地方叫做線程本地存儲,其實意思差不多。可能很多朋友都知道ThreadLocal爲變量在每個線程中都創建了一個副本,那麼每個線程可以訪問自己內部的副本變量。

        這句話從字面上看起來很容易理解,但是真正理解並不是那麼容易。

        我們還是先來看一個例子:

Java代碼  收藏代碼
  1. class ConnectionManager {  
  2.    
  3.  private static Connection connect = null;  
  4.    
  5.  public static Connection openConnection() {  
  6.  if(connect == null){  
  7.  connect = DriverManager.getConnection();  
  8.  }  
  9.  return connect;  
  10.  }  
  11.    
  12.  public static void closeConnection() {  
  13.  if(connect!=null)  
  14.  connect.close();  
  15.  }  
  16. }  

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

        所以出於線程安全的考慮,必須將這段代碼的兩個方法進行同步處理,並且在調用connect的地方需要進行同步處理。

        這樣將會大大影響程序執行效率,因爲一個線程在使用connect進行數據庫操作的時候,其他線程只有等待。

        那麼大家來仔細分析一下這個問題,這地方到底需不需要將connect變量進行共享?事實上,是不需要的。假如每個線程中都有一個connect變量,各個線程之間對connect變量的訪問實際上是沒有依賴關係的,即一個線程不需要關心其他線程是否對這個connect進行了修改的。

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

Java代碼  收藏代碼
  1. class ConnectionManager {  
  2.    
  3.  private Connection connect = null;  
  4.    
  5.  public Connection openConnection() {  
  6.  if(connect == null){  
  7.  connect = DriverManager.getConnection();  
  8.  }  
  9.  return connect;  
  10.  }  
  11.    
  12.  public void closeConnection() {  
  13.  if(connect!=null)  
  14.  connect.close();  
  15.  }  
  16. }  
  17.    
  18. class Dao{  
  19.  public void insert() {  
  20.  ConnectionManager connectionManager = new ConnectionManager();  
  21.  Connection connection = connectionManager.openConnection();  
  22.    
  23.  //使用connection進行操作  
  24.    
  25.  connectionManager.closeConnection();  
  26.  }  
  27. }  

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

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

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

 

二.深入解析ThreadLocal類

        在上面談到了對ThreadLocal的一些理解,那我們下面來看一下具體ThreadLocal是如何實現的。

        先了解一下ThreadLocal類提供的幾個方法:

Java代碼  收藏代碼
  1. public T get() { }  
  2. public void set(T value) { }  
  3. public void remove() { }  
  4. protected T initialValue() { }  

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

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

        先看下get方法的實現:


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

        如果獲取成功,則返回value值。

        如果map爲空,則調用setInitialValue方法返回value。

        我們上面的每一句來仔細分析:

        首先看一下getMap方法中做了什麼:


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

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


        實際上就是一個ThreadLocalMap,這個類型是ThreadLocal類的一個內部類,我們繼續取看ThreadLocalMap的實現:


        可以看到ThreadLocalMap的Entry繼承了WeakReference,並且使用ThreadLocal作爲鍵值。

        然後再繼續看setInitialValue方法的具體實現:


        很容易瞭解,就是如果map不爲空,就設置鍵值對,爲空,再創建Map,看一下createMap的實現:


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

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

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

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

        下面通過一個例子來證明通過ThreadLocal能達到在每個線程中創建變量副本的效果:

Java代碼  收藏代碼
  1. package com.bijian.study;  
  2.   
  3. public class Test {  
  4.       
  5.     ThreadLocal<Long> longLocal = new ThreadLocal<Long>();  
  6.     ThreadLocal<String> stringLocal = new ThreadLocal<String>();  
  7.   
  8.     public void set() {  
  9.         longLocal.set(Thread.currentThread().getId());  
  10.         stringLocal.set(Thread.currentThread().getName());  
  11.     }  
  12.   
  13.     public long getLong() {  
  14.         return longLocal.get();  
  15.     }  
  16.   
  17.     public String getString() {  
  18.         return stringLocal.get();  
  19.     }  
  20.   
  21.     public static void main(String[] args) throws InterruptedException {  
  22.         final Test test = new Test();  
  23.   
  24.         test.set();  
  25.         System.out.println(test.getLong());  
  26.         System.out.println(test.getString());  
  27.   
  28.         Thread thread1 = new Thread() {  
  29.             public void run() {  
  30.                 test.set();  
  31.                 System.out.println(test.getLong());  
  32.                 System.out.println(test.getString());  
  33.             };  
  34.         };  
  35.         thread1.start();  
  36.         thread1.join();  
  37.   
  38.         System.out.println(test.getLong());  
  39.         System.out.println(test.getString());  
  40.     }  
  41. }  

運行結果:

Text代碼  收藏代碼
  1. 1  
  2. main  
  3. 11  
  4. Thread-0  
  5. 1  
  6. 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。


        看下面這個例子:

Java代碼  收藏代碼
  1. package com.bijian.study;  
  2.   
  3. public class Test02 {  
  4.   
  5.     ThreadLocal<Long> longLocal = new ThreadLocal<Long>();  
  6.     ThreadLocal<String> stringLocal = new ThreadLocal<String>();  
  7.   
  8.     public void set() {  
  9.         longLocal.set(Thread.currentThread().getId());  
  10.         stringLocal.set(Thread.currentThread().getName());  
  11.     }  
  12.   
  13.     public long getLong() {  
  14.         return longLocal.get();  
  15.     }  
  16.   
  17.     public String getString() {  
  18.         return stringLocal.get();  
  19.     }  
  20.   
  21.     public static void main(String[] args) throws InterruptedException {  
  22.         final Test02 test = new Test02();  
  23.   
  24.         System.out.println(test.getLong());  
  25.         System.out.println(test.getString());  
  26.   
  27.         Thread thread1 = new Thread() {  
  28.             public void run() {  
  29.                 test.set();  
  30.                 System.out.println(test.getLong());  
  31.                 System.out.println(test.getString());  
  32.             };  
  33.         };  
  34.         thread1.start();  
  35.         thread1.join();  
  36.   
  37.         System.out.println(test.getLong());  
  38.         System.out.println(test.getString());  
  39.     }  
  40. }  

運行結果:

Text代碼  收藏代碼
  1. Exception in thread "main" java.lang.NullPointerException  
  2.     at com.bijian.study.Test02.getLong(Test02.java:14)  
  3.     at com.bijian.study.Test02.main(Test02.java:24)  

        在main線程中,沒有先set,直接get的話,運行時會報空指針異常。

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

Java代碼  收藏代碼
  1. package com.bijian.study;  
  2.   
  3. public class Test03 {  
  4.   
  5.     ThreadLocal<Long> longLocal = new ThreadLocal<Long>() {  
  6.         protected Long initialValue() {  
  7.             return Thread.currentThread().getId();  
  8.         };  
  9.     };  
  10.       
  11.     ThreadLocal<String> stringLocal = new ThreadLocal<String>() {  
  12.         protected String initialValue() {  
  13.             return Thread.currentThread().getName();  
  14.         };  
  15.     };  
  16.   
  17.     public void set() {  
  18.         longLocal.set(Thread.currentThread().getId());  
  19.         stringLocal.set(Thread.currentThread().getName());  
  20.     }  
  21.   
  22.     public long getLong() {  
  23.         return longLocal.get();  
  24.     }  
  25.   
  26.     public String getString() {  
  27.         return stringLocal.get();  
  28.     }  
  29.   
  30.     public static void main(String[] args) throws InterruptedException {  
  31.         final Test03 test = new Test03();  
  32.   
  33.         //test.set();  
  34.         System.out.println(test.getLong());  
  35.         System.out.println(test.getString());  
  36.   
  37.         Thread thread1 = new Thread() {  
  38.             public void run() {  
  39.                 //test.set();  
  40.                 System.out.println(test.getLong());  
  41.                 System.out.println(test.getString());  
  42.             };  
  43.         };  
  44.         thread1.start();  
  45.         thread1.join();  
  46.   
  47.         System.out.println(test.getLong());  
  48.         System.out.println(test.getString());  
  49.     }  
  50. }  

運行結果:

Text代碼  收藏代碼
  1. 1  
  2. main  
  3. 8  
  4. Thread-0  
  5. 1  
  6. main  

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

 

三.ThreadLocal的應用場景

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

        數據庫連接:

Java代碼  收藏代碼
  1. private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {  
  2.     public Connection initialValue() {  
  3.         return DriverManager.getConnection(DB_URL);  
  4.     }  
  5. };  
  6.   
  7. public static Connection getConnection() {  
  8.     return connectionHolder.get();  
  9. }  

        Session管理:

Java代碼  收藏代碼
  1. private static final ThreadLocal threadSession = new ThreadLocal();  
  2.   
  3. public static Session getSession() throws InfrastructureException {  
  4.     Session s = (Session) threadSession.get();  
  5.     try {  
  6.         if (s == null) {  
  7.             s = getSessionFactory().openSession();  
  8.             threadSession.set(s);  
  9.         }  
  10.     } catch (HibernateException ex) {  
  11.         throw new InfrastructureException(ex);  
  12.     }  
  13.     return s;  
  14. }  
發佈了48 篇原創文章 · 獲贊 13 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章