線程本地變更,即ThreadLocal-->Spring事務管理


我們知道Spring通過各種模板類降低了開發者使用各種數據持久技術的難度。這些模板類都是線程安全的,也就是說,多個DAO可以復用同一個模板實例而不會發生沖突。我們使用模板類訪問底層數據,根據持久化技術的不同,模板類需要綁定數據連接或會話的資源。但這些資源本身是非線程安全的,也就是說它們不能在同一時刻被多個線程共享。雖然模板類通過資源池獲取數據連接或會話,但資源池本身解決的是數據連接或會話的緩存問題,並非數據連接或會話的線程安全問題。 

按照傳統經驗,如果某個對象是非線程安全的,在多線程環境下,對對象的訪問必須采用synchronized進行線程同步。但模板類並未采用線程同步機制,因為線程同步會降低並發性,影響系統性能。此外,通過代碼同步解決線程安全的挑戰性很大,可能會增強好幾倍的實現難度。那么模板類究竟仰仗何種魔法神功,可以在無須線程同步的情況下就化解線程安全的難題呢?答案就是ThreadLocal! 

ThreadLocal在Spring中發揮着重要的作用,在管理request作用域的Bean、事務管理、任務調度、AOP等模塊都出現了它們的身影,起着舉足輕重的作用。要想了解Spring事務管理的底層技術,ThreadLocal是必須攻克的山頭堡壘。 

ThreadLocal是什么 

早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal為解決多線程程序的並發問題提供了一種新的思路。使用這個工具類可以很簡潔地編寫出優美的多線程程序。 
ThreadLocal,顧名思義,它不是一個線程,而是線程的一個本地化對象。當工作於多線程中的對象使用ThreadLocal維護變量時,ThreadLocal為每個使用該變量的線程分配一個獨立的變量副本。所以每一個線程都可以獨立地改變自己的副本,而不會影響其他線程所對應的副本。從線程的角度看,這個變量就像是線程的本地變量,這也是類名中“Local”所要表達的意思。 

線程局部變量並不是Java的新發明,很多語言(如IBM XL、FORTRAN)在語法層面就提供線程局部變量。在Java中沒有提供語言級支持,而以一種變通的方法,通過ThreadLocal的類提供支持。所以,在Java中編寫線程局部變量的代碼相對來說要笨拙一些,這也是為什么線程局部變量沒有在Java開發者中得到很好普及的原因。 


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。 




值得一提的是,在JDK5.0中,ThreadLocal已經支持泛型,該類的類名已經變為ThreadLocal<T>。API方法也相應進行了調整,新版本的API方法分別是void set(T value)、T get()以及T initialValue()。 

ThreadLocal是如何做到為每一個線程維護變量的副本的呢?其實實現的思路很簡單:在ThreadLocal類中有一個Map,用於存儲每一個線程的變量副本,Map中元素的鍵為線程對象,而值對應線程的變量副本。我們自己就可以提供一個簡單的實現版本: 

代碼清單9-3  SimpleThreadLocal 

Java代碼   收藏代碼
  1. public class SimpleThreadLocal {  
  2.     private Map valueMap = Collections.synchronizedMap(new HashMap());  
  3.     public void set(Object newValue) {  
  4.                 //①鍵為線程對象,值為本線程的變量副本  
  5.         valueMap.put(Thread.currentThread(), newValue);  
  6.     }  
  7.     public Object get() {  
  8.         Thread currentThread = Thread.currentThread();  
  9.   
  10.                 //②返回本線程對應的變量  
  11.         Object o = valueMap.get(currentThread);   
  12.                   
  13.                 //③如果在Map中不存在,放到Map中保存起來  
  14.                if (o == null && !valueMap.containsKey(currentThread)) {  
  15.             o = initialValue();  
  16.             valueMap.put(currentThread, o);  
  17.         }  
  18.         return o;  
  19.     }  
  20.     public void remove() {  
  21.         valueMap.remove(Thread.currentThread());  
  22.     }  
  23.     public Object initialValue() {  
  24.         return null;  
  25.     }  
  26. }  



雖然代碼清單9 3中這個ThreadLocal實現版本顯得比較幼稚,但它和JDK所提供的ThreadLocal類在實現思路上是非常相近的。 

一個TheadLocal實例 

下面,我們通過一個具體的實例了解一下ThreadLocal的具體使用方法。 

代碼清單9-4  SequenceNumber 

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



通常我們通過匿名內部類的方式定義ThreadLocal的子類,提供初始的變量值,如①處所示。TestClient線程產生一組序列號,在③處,我們生成3個TestClient,它們共享同一個SequenceNumber實例。運行以上代碼,在控制台上輸出以下的結果: 

引用
thread[Thread-2] sn[1] 
thread[Thread-0] sn[1] 
thread[Thread-1] sn[1] 
thread[Thread-2] sn[2] 
thread[Thread-0] sn[2] 
thread[Thread-1] sn[2] 
thread[Thread-2] sn[3] 
thread[Thread-0] sn[3] 
thread[Thread-1] sn[3]



考查輸出的結果信息,我們發現每個線程所產生的序號雖然都共享同一個Sequence Number實例,但它們並沒有發生相互干擾的情況,而是各自產生獨立的序列號,這是因為我們通過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就能夠以singleton的方式在多線程中正常工作了。 

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

 

這樣用戶就可以根據需要,將一些非線程安全的變量以ThreadLocal存放,在同一次請求響應的調用線程中,所有對象所訪問的同一ThreadLocal變量都是當前線程所綁定的。 
下面的實例能夠體現Spring對有狀態Bean的改造思路: 

代碼清單9-5  TopicDao:非線程安全 

Java代碼   收藏代碼
  1. public class TopicDao {  
  2.    //①一個非線程安全的變量  
  3.    private Connection conn;   
  4.    public void addTopic(){  
  5.         //②引用非線程安全變量  
  6.        Statement stat = conn.createStatement();  
  7.        …  
  8.    }  
  9. }  



由於①處的conn是成員變量,因為addTopic()方法是非線程安全的,必須在使用時創建一個新TopicDao實例(非singleton)。下面使用ThreadLocal對conn這個非線程安全的“狀態”進行改造: 

代碼清單9-6  TopicDao:線程安全 

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



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

當然,這個例子本身很粗糙,將Connection的ThreadLocal直接放在Dao只能做到本Dao的多個方法共享Connection時不發生線程安全問題,但無法和其他Dao共用同一個Connection,要做到同一事務多Dao共享同一個Connection,必須在一個共同的外部類使用ThreadLocal保存Connection。但這個實例基本上說明了Spring對有狀態類線程安全化的解決思路。在本章后面的內容中,我們將詳細說明Spring如何通過ThreadLocal解決事務管理的問題。 

這些文章摘自於我的《Spring 3.x企業應用開發實戰》,我將通過連載的方式,陸續在此發出。歡迎大家討論


免責聲明!

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



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