ThreadLocal類的實現用法


ThreadLocal是什么呢?其實ThreadLocal並非是一個線程的本地實現版本,它並不是一個Thread,而是threadlocalvariable(線程局部變量)。也許把它命名為ThreadLocalVar更加合適。線程局部變量(ThreadLocal)其實的功用非常簡單,就是為每一個使用該變量的線程都提供一個變量值的副本,是Java中一種較為特殊的線程綁定機制,是每一個線程都可以獨立地改變自己的副本,而不會和其它線程的副本沖突。
從線程的角度看,每個線程都保持一個對其線程局部變量副本的隱式引用,只要線程是活動的並且 ThreadLocal 實例是可訪問的;在線程消失之后,其線程局部實例的所有副本都會被垃圾回收(除非存在對這些副本的其他引用)。
通過ThreadLocal存取的數據,總是與當前線程相關,也就是說,JVM 為每個運行的線程,綁定了私有的本地實例存取空間,從而為多線程環境常出現的並發訪問問題提供了一種隔離機制。
ThreadLocal是如何做到為每一個線程維護變量的副本的呢?其實實現的思路很簡單,在ThreadLocal類中有一個Map,用於存儲每一個線程的變量的副本。
概括起來說,對於多線程資源共享的問題,同步機制采用了“以時間換空間”的方式,而ThreadLocal采用了“以空間換時間”的方式。前者僅提供一份變量,讓不同的線程排隊訪問,而后者為每一個線程都提供了一份變量,因此可以同時訪問而互不影響。

ThreadLocal的接口方法

ThreadLocal類接口很簡單,只有4個方法,我們先來了解一下:

  • void set(Object value)設置當前線程的線程局部變量的值。
  • public Object get()該方法返回當前線程所對應的線程局部變量。
  • public void remove()將當前線程局部變量的值刪除,目的是為了減少內存的占用,該方法是JDK 5.0新增的方法。需要指出的是,當線程結束后,對應該線程的局部變量將自動被垃圾回收,所以顯式調用該方法清除線程的局部變量並不是必須的操作,但它可以加快內存回收的速度。
  • protected Object initialValue()返回該線程局部變量的初始值,該方法是一個protected的方法,顯然是為了讓子類覆蓋而設計的。這個方法是一個延遲調用方法,在線程第1次調用get()或set(Object)時才執行,並且僅執行1次。ThreadLocal中的缺省實現直接返回一個null。

模擬實現ThreadLocal代碼:

 1 public class ThreadShareData {
 2     static int num = 0;
 3     
 4     
 5     public static void main(String[] args) {
 6         Map<Thread, Integer> map = new HashMap<Thread, Integer>();
 7         for (int i = 0; i < 2; i++) {
 8             new Thread(new Runnable() {
 9                 @Override
10                 public void run() {
11                     int num = new Random().nextInt();
12                     System.out.println(Thread.currentThread().getName()+":"+" get num: "+num);
13                     new A().get();
14                     new B().get();
15                 }
16             }).start();
17         }
18     }
19     
20     static class A{
21         public void get(){
22             System.out.println("A: "+Thread.currentThread().getName() +"get num: "+num);
23         }
24     }
25     
26     static class B{
27         public void get(){
28             System.out.println("A: "+Thread.currentThread().getName() +"get num: "+num);
29         }
30     }
31     
32 }

ThreadLocal的經典用法:

 1 public class ThreadLocalShareData2 {
 2     static ThreadLocal<People> threadLocal = new ThreadLocal<People>();
 3     public static void main(String[] args) {
 4         for (int i = 0; i < 2; i++) {
 5             new Thread(new Runnable() {
 6                 @Override
 7                 public void run() {
 8                     int data = new Random().nextInt();
 9                     People people = new People().getInstance();
10                     people.setName("name"+data);
11                     people.setAge(data);
12                     System.out.println(Thread.currentThread().getName()+" set name "+people.getName()+" set age "+people.getAge());
13                     new A().get();
14                     new B().get();
15                 }
16             }).start();
17         }
18     }
19     
20     static class A{
21         public void get(){
22             System.out.println("A: "+Thread.currentThread().getName() +"get name "+new People().getInstance().getName()+" get age "+new People().getInstance().getAge());
23         }
24     }
25     static class B{
26         public void get(){
27             System.out.println("B: "+Thread.currentThread().getName() +"get name "+new People().getInstance().getName()+" get age "+new People().getInstance().getAge());
28         }
29     }
30     static class People{
31         private People(){
32             
33         }
34         public People getInstance(){
35             People people = threadLocal.get();
36             if(people == null){
37                 people = new People();
38                 threadLocal.set(people);
39             }
40             return people;
41         }
42         private int age;
43         private String name;
44         public int getAge() {
45             return age;
46         }
47         public String getName() {
48             return name;
49         }
50         public void setAge(int age) {
51             this.age = age;
52         }
53         public void setName(String name) {
54             this.name = name;
55         }
56     }
57 }

將ThreadLocal仿單例模式進行實現,更加面向對象。

在ThreadLocal類中有一個Map,用於存儲每一個線程的變量副本,Map中元素的鍵為線程對象,而值對應線程的變量副本。

 1 public class TestNum {  
 2     // ①通過匿名內部類覆蓋ThreadLocal的initialValue()方法,指定初始值  
 3     private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>() {  
 4         public Integer initialValue() {  
 5             return 0;  
 6         }  
 7     };  
 8   
 9     // ②獲取下一個序列值  
10     public int getNextNum() {  
11         seqNum.set(seqNum.get() + 1);  
12         return seqNum.get();  
13     }  
14   
15     public static void main(String[] args) {  
16         TestNum sn = new TestNum();  
17         // ③ 3個線程共享sn,各自產生序列號  
18         TestClient t1 = new TestClient(sn);  
19         TestClient t2 = new TestClient(sn);  
20         TestClient t3 = new TestClient(sn);  
21         t1.start();  
22         t2.start();  
23         t3.start();  
24     }  
25   
26     private static class TestClient extends Thread {  
27         private TestNum sn;  
28   
29         public TestClient(TestNum sn) {  
30             this.sn = sn;  
31         }  
32   
33         public void run() {  
34             for (int i = 0; i < 3; i++) {  
35                 // ④每個線程打出3個序列值  
36                 System.out.println("thread[" + Thread.currentThread().getName() + "] --> sn["  
37                          + sn.getNextNum() + "]");  
38             }  
39         }  
40     }  
41 }  

我們發現每個線程所產生的序號雖然都共享同一個TestNum實例,但它們並沒有發生相互干擾的情況,而是各自產生獨立的序列號,這是因為我們通過ThreadLocal為每一個線程提供了單獨的副本。

Thread同步機制的比較:

ThreadLocal和線程同步機制相比有什么優勢呢?ThreadLocal和線程同步機制都是為了解決多線程中相同變量的訪問沖突問題。

  在同步機制中,通過對象的鎖機制保證同一時間只有一個線程訪問變量。這時該變量是多個線程共享的,使用同步機制要求程序慎密地分析什么時候對變量進行讀寫,什么時候需要鎖定某個對象,什么時候釋放對象鎖等繁雜的問題,程序設計和編寫難度相對較大。

  而ThreadLocal則從另一個角度來解決多線程的並發訪問。ThreadLocal會為每一個線程提供一個獨立的變量副本,從而隔離了多個線程對數據的訪問沖突。因為每一個線程都擁有自己的變量副本,從而也就沒有必要對該變量進行同步了。ThreadLocal提供了線程安全的共享對象,在編寫多線程代碼時,可以把不安全的變量封裝進ThreadLocal。

  由於ThreadLocal中可以持有任何類型的對象,低版本JDK所提供的get()返回的是Object對象,需要強制類型轉換。但JDK 5.0通過泛型很好的解決了這個問題,在一定程度地簡化ThreadLocal的使用,代碼清單 9 2就使用了JDK 5.0新的ThreadLocal<T>版本。

  概括起來說,對於多線程資源共享的問題,同步機制采用了“以時間換空間”的方式,而ThreadLocal采用了“以空間換時間”的方式。前者僅提供一份變量,讓不同的線程排隊訪問,而后者為每一個線程都提供了一份變量,因此可以同時訪問而互不影響。

  Spring使用ThreadLocal解決線程安全問題我們知道在一般情況下,只有無狀態的Bean才可以在多線程環境下共享,在Spring中,絕大部分Bean都可以聲明為singleton作用域。就是因為Spring對一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非線程安全狀態采用ThreadLocal進行處理,讓它們也成為線程安全的狀態,因為有狀態的Bean就可以在多線程中共享了。

  一般的Web應用划分為展現層、服務層和持久層三個層次,在不同的層中編寫對應的邏輯,下層通過接口向上層開放功能調用。在一般情況下,從接收請求到返回響應所經過的所有程序調用都同屬於一個線程,如圖9‑2所示:

通通透透理解ThreadLocal

  同一線程貫通三層這樣你就可以根據需要,將一些非線程安全的變量以ThreadLocal存放,在同一次請求響應的調用線程中,所有關聯的對象引用到的都是同一個變量。

  下面的實例能夠體現Spring對有狀態Bean的改造思路:

  非線程安全代碼:

  

1 public class TestDao {  
2     private Connection conn;// ①一個非線程安全的變量  
3   
4     public void addTopic() throws SQLException {  
5         Statement stat = conn.createStatement();// ②引用非線程安全變量  
6         //
7     }  
8 }  

  線程安全代碼:

 1 public class TestDaoNew {  
 2     // ①使用ThreadLocal保存Connection變量  
 3     private static ThreadLocal<Connection> connThreadLocal = new ThreadLocal<Connection>();  
 4   
 5     public static Connection getConnection() {  
 6         // ②如果connThreadLocal沒有本線程對應的Connection創建一個新的Connection,  
 7         // 並將其保存到線程本地變量中。  
 8         if (connThreadLocal.get() == null) {  
 9             Connection conn = getConnection();  
10             connThreadLocal.set(conn);  
11             return conn;  
12         } else {  
13             return connThreadLocal.get();// ③直接返回線程本地變量  
14         }  
15     }  
16   
17     public void addTopic() throws SQLException {  
18         // ④從ThreadLocal中獲取線程對應的Connection  
19         Statement stat = getConnection().createStatement();  
20     }  
21 }  

不同的線程在使用TopicDao時,先判斷connThreadLocal.get()是否是null,如果是null,則說明當前線程還沒有對應的Connection對象,這時創建一個Connection對象並添加到本地線程變量中;如果不為null,則說明當前的線程已經擁有了Connection對象,直接使用就可以了。這樣,就保證了不同的線程使用線程相關的Connection,而不會使用其它線程的Connection。因此,這個TopicDao就可以做到singleton共享了。

  當然,這個例子本身很粗糙,將Connection的ThreadLocal直接放在DAO只能做到本DAO的多個方法共享Connection時不發生線程安全問題,但無法和其它DAO共用同一個Connection,要做到同一事務多DAO共享同一Connection,必須在一個共同的外部類使用ThreadLocal保存Connection。

 1 public class ConnectionManager {  
 2   
 3     private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {  
 4         @Override  
 5         protected Connection initialValue() {  
 6             Connection conn = null;  
 7             try {  
 8                 conn = DriverManager.getConnection(  
 9                         "jdbc:mysql://localhost:3306/test", "username",  
10                         "password");  
11             } catch (SQLException e) {  
12                 e.printStackTrace();  
13             }  
14             return conn;  
15         }  
16     };  
17   
18     public static Connection getConnection() {  
19         return connectionHolder.get();  
20     }  
21   
22     public static void setConnection(Connection conn) {  
23         connectionHolder.set(conn);  
24     }  
25 }  

還有一個經典實例,在HibernateUtil中,用於session的管理:

 1 public class HibernateUtil {
 2     private static Log log = LogFactory.getLog(HibernateUtil.class);
 3     private static final SessionFactory sessionFactory;     //定義SessionFactory
 4  
 5     static {
 6         try {
 7             // 通過默認配置文件hibernate.cfg.xml創建SessionFactory
 8             sessionFactory = new Configuration().configure().buildSessionFactory();
 9         } catch (Throwable ex) {
10             log.error("初始化SessionFactory失敗!", ex);
11             throw new ExceptionInInitializerError(ex);
12         }
13     }
14 
15     //創建線程局部變量session,用來保存Hibernate的Session
16     public static final ThreadLocal session = new ThreadLocal();
17  
18     /**
19      * 獲取當前線程中的Session
20      * @return Session
21      * @throws HibernateException
22      */
23     public static Session currentSession() throws HibernateException {
24         Session s = (Session) session.get();
25         // 如果Session還沒有打開,則新開一個Session
26         if (s == null) {
27             s = sessionFactory.openSession();
28             session.set(s);         //將新開的Session保存到線程局部變量中
29         }
30         return s;
31     }
32  
33     public static void closeSession() throws HibernateException {
34         //獲取線程局部變量,並強制轉換為Session類型
35         Session s = (Session) session.get();
36         session.set(null);
37         if (s != null)
38             s.close();
39     }
40 }

總結:

  ThreadLocal使用場合主要解決多線程中數據數據因並發產生不一致問題。ThreadLocal為每個線程的中並發訪問的數據提供一個副本,通過訪問副本來運行業務,這樣的結果是耗費了內存,單大大減少了線程同步所帶來性能消耗,也減少了線程並發控制的復雜度。
  ThreadLocal不能使用原子類型,只能使用Object類型。ThreadLocal的使用比synchronized要簡單得多。
  ThreadLocal和Synchonized都用於解決多線程並發訪問。但是ThreadLocal與synchronized有本質的區別。synchronized是利用鎖的機制,使變量或代碼塊在某一時該只能被一個線程訪問。而ThreadLocal為每一個線程都提供了變量的副本,使得每個線程在某一時間訪問到的並不是同一個對象,這樣就隔離了多個線程對數據的數據共享。而 Synchronized卻正好相反,它用於在多個線程間通信時能夠獲得數據共享。
   Synchronized用於線程間的數據共享,而ThreadLocal則用於線程間的數據隔離。
  當然ThreadLocal並不能替代synchronized,它們處理不同的問題域。Synchronized用於實現同步機制,比ThreadLocal更加復雜。
參考文檔:
JDK 官方文檔
http://blog.csdn.net/lufeng20/article/details/24314381
http://lavasoft.blog.51cto.com/62575/51926/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM