咱們知道Spring經過各類模板類下降了開發者使用各類數據持久技術的難度。這些模板類都是線程安全的,也就是說,多個DAO能夠複用同一個模板實例而不會發生衝突。咱們使用模板類訪問底層數據,根據持久化技術的不一樣,模板類須要綁定數據鏈接或會話的資源。但這些資源自己是非線程安全的,也就是說它們不能在同一時刻被多個線程共享。雖然模板類經過資源池獲取數據鏈接或會話,但資源池自己解決的是數據鏈接或會話的緩存問題,並不是數據鏈接或會話的線程安全問題。
按照傳統經驗,若是某個對象是非線程安全的,在多線程環境下,對對象的訪問必須採用synchronized進行線程同步。但模板類並未採用線程同步機制,由於線程同步會下降併發性,影響系統性能。此外,經過代碼同步解決線程安全的挑戰性很大,可能會加強好幾倍的實現難度。那麼模板類究竟仰仗何種魔法神功,能夠在無須線程同步的狀況下就化解線程安全的難題呢?答案就是ThreadLocal!
ThreadLocal在Spring中發揮着重要的做用,在管理request做用域的Bean、事務管理、任務調度、AOP等模塊都出現了它們的身影,起着舉足輕重的做用。要想了解Spring事務管理的底層技術,ThreadLocal是必須攻克的山頭堡壘。
java
早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal爲解決多線程程序的併發問題提供了一種新的思路。使用這個工具類能夠很簡潔地編寫出優美的多線程程序。
ThreadLocal,顧名思義,它不是一個線程,而是線程的一個本地化對象。當工做於多線程中的對象使用ThreadLocal維護變量時,ThreadLocal爲每一個使用該變量的線程分配一個獨立的變量副本。因此每個線程均可以獨立地改變本身的副本,而不會影響其餘線程所對應的副本。從線程的角度看,這個變量就像是線程的本地變量,這也是類名中「Local」所要表達的意思。
線程局部變量並非Java的新發明,不少語言(如IBM XL、FORTRAN)在語法層面就提供線程局部變量。在Java中沒有提供語言級支持,而以一種變通的方法,經過ThreadLocal的類提供支持。因此,在Java中編寫線程局部變量的代碼相對來講要笨拙一些,這也是爲何線程局部變量沒有在Java開發者中獲得很好普及的緣由。
sql
學習JDK中的類,首先看下JDK API對此類的描述,描述以下:編程
該類提供了線程局部 (thread-local) 變量。這些變量不一樣於它們的普通對應物,由於訪問某個變量(經過其 get 或 set 方法)的每一個線程都有本身的局部變量,它獨立於變量的初始化副本。ThreadLocal 實例一般是類中的 private static 字段,它們但願將狀態與某一個線程(例如,用戶 ID 或事務 ID)相關聯。
API表達了下面幾種觀點:緩存
一、ThreadLocal不是線程,是線程的一個變量,你能夠先簡單理解爲線程類的屬性變量。安全
二、ThreadLocal在類中一般定義爲靜態變量。多線程
三、每一個線程有本身的一個ThreadLocal,它是變量的一個「拷貝」,修改它不影響其餘線程。併發
既然定義爲類變量,爲什麼爲每一個線程維護一個副本(姑且稱爲「拷貝」容易理解),讓每一個線程獨立訪問?多線程編程的經驗告訴咱們,對於線程共享資源(你能夠理解爲屬性),資源是否被全部線程共享,也就是說這個資源被一個線程修改是否影響另外一個線程的運行,若是影響咱們須要使用synchronized同步,讓線程順序訪問。框架
ThreadLocal適用於資源共享但不須要維護狀態的狀況,也就是一個線程對資源的修改,不影響另外一個線程的運行;這種設計是‘空間換時間’,synchronized順序執行是‘時間換取空間’。ide
ThreadLocal<T>類在Spring,Hibernate等框架中起到了很大的做用。爲了解釋ThreadLocal類的工做原理,必須同時介紹與其工做甚密的其餘幾個類,包括內部類ThreadLocalMap,和線程類Thread。全部方法以下圖:工具
四個核心方法說明以下:
T get() 返回此線程局部變量的當前線程副本中的值。
protected T initialValue() 返回此線程局部變量的當前線程的「初始值」。
void remove() 移除此線程局部變量當前線程的值。
void set(T value) 將此線程局部變量的當前線程副本中的值設置爲指定值。
可能有人會以爲Thread與ThreadLocal有什麼關係,其實真正的奧祕就在Thread類中的一行代碼:
ThreadLocal.ThreadLocalMap threadLocals = null;
其中ThreadLocalMap的定義是在ThreadLocal類中,真正的引用倒是在Thread類中。那麼ThreadLocalMap到底是什麼呢?能夠看到這個類應該是一個Map,JDK的解釋是:ThreadLocalMap is a customized hash map suitable only for maintaining thread local values。
接下來的重點是ThreadLocalMap中用於存儲數據的entry:
static class Entry extends WeakReference<ThreadLocal> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal k, Object v) { super(k); value = v; } }
從中咱們能夠發現這個Map的key是ThreadLocal變量,value爲用戶的值,並非不少人認爲的key是線程的名字或者標識。到這裏,咱們就能夠理解ThreadLocal到底是如何工做的了。
1. Thread類中有一個成員變量叫作ThreadLocalMap,它是一個Map,他的Key是ThreadLocal類
2. 每一個線程擁有本身的申明爲ThreadLocal類型的變量,因此這個類的名字叫ThreadLocal:線程本身的(變量)。
3. 此變量生命週期是由該線程決定的,開始於第一次初始化(get或者set方法)。
4. 由ThreadLocal的工做原理決定了:每一個線程獨自擁有一個變量,並不是共享或者拷貝。
下面,咱們經過一個具體的實例瞭解一下ThreadLocal的具體使用方法。
package com.baobaotao.basic; public class SequenceNumber { //①經過匿名內部類覆蓋ThreadLocal的initialValue()方法,指定初始值 private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>(){ public Integer initialValue(){ return 0; } }; //②獲取下一個序列值 public int getNextNum(){ seqNum.set(seqNum.get()+1); return seqNum.get(); } public static void main(String[ ] args) { SequenceNumber sn = new SequenceNumber(); //③ 3個線程共享sn,各自產生序列號 TestClient t1 = new TestClient(sn); TestClient t2 = new TestClient(sn); TestClient t3 = new TestClient(sn); t1.start(); t2.start(); t3.start(); } private static class TestClient extends Thread { private SequenceNumber sn; public TestClient(SequenceNumber sn) { this.sn = sn; } public void run() { //④每一個線程打出3個序列值 for (int i = 0; i < 3; i++) { System.out.println("thread["+Thread.currentThread().getName()+ "] sn["+sn.getNextNum()+"]"); } } } }
一般咱們經過匿名內部類的方式定義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爲每個線程提供了單獨的副本。
接下來分析一下ThreadLocal的源碼,就更加清楚了。
ThreadLocal有一個ThreadLocalMap靜態內部類,你能夠簡單理解爲一個Map,這個Map爲每一個線程複製一個變量的‘拷貝’存儲其中。
當線程調用ThreadLocal.get()方法獲取變量時,首先獲取當前線程引用,以此爲key去獲取響應的ThreadLocalMap,若是此‘Map’不存在則初始化一個,不然返回其中的變量,代碼以下:
public T get() { Thread t = Thread.currentThread(); /** * 獲得當前線程的ThreadLocalMap */ ThreadLocalMap map = getMap(t); if (map != null) { /** * 在此線程的ThreadLocalMap中查找key爲當前ThreadLocal對象的entry */ ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) return (T)e.value; } return setInitialValue(); } /** * 關鍵方法,返回當前Thread的ThreadLocalMap * [[[每一個Thread返回各自的ThreadLocalMap,因此各個線程中的ThreadLocal均爲獨立的]]] */ ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
調用get方法若是此Map不存在首先初始化,建立此map,將線程爲key,初始化的vlaue存入其中,注意此處的initialValue,咱們能夠覆蓋此方法,在首次調用時初始化一個適當的值。
對於ThreadLocal在何處存儲變量副本,咱們看getMap方法:獲取的是當前線程的ThreadLocal類型的threadLocals屬性。顯然變量副本存儲在每個線程中。
setInitialValue代碼以下:
private T setInitialValue() { /** * 默認返回null,這個方法爲protected能夠繼承 */ T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else /** * 初次建立 */ createMap(t, value); return value; } /** * 給當前thread初始ThreadlocalMap */ void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
set方法相對比較簡單,若是理解以上倆個方法。獲取當前線程的引用,從map中獲取該線程對應的map,若是map存在更新緩存值,不然建立並存儲,代碼以下:
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }
上面咱們知道變量副本存放於何處,這裏咱們簡單說下如何被java的垃圾收集機制收集,當咱們不在使用時調用set(null),此時不在將引用指向該‘map’,而線程退出時會執行資源回收操做,將申請的資源進行回收,其實就是將屬性的引用設置爲null。這時已經不在有任何引用指向該map,故而會被垃圾收集。
咱們本身就能夠實現一個簡化版本的ThreadLocal,裏面有一個Map,用於存儲每個線程的變量副本,Map中元素的鍵爲線程對象,而值對應線程的變量副本。
public class SimpleThreadLocal { private Map valueMap = Collections.synchronizedMap(new HashMap()); public void set(Object newValue) { //①鍵爲線程對象,值爲本線程的變量副本 valueMap.put(Thread.currentThread(), newValue); } public Object get() { Thread currentThread = Thread.currentThread(); //②返回本線程對應的變量 Object o = valueMap.get(currentThread); //③若是在Map中不存在,放到Map中保存起來 if (o == null && !valueMap.containsKey(currentThread)) { o = initialValue(); valueMap.put(currentThread, o); } return o; } public void remove() { valueMap.remove(Thread.currentThread()); } public Object initialValue() { return null; } }
雖然上述代碼清單中這個ThreadLocal實現版本顯得比較幼稚,但它和JDK所提供的ThreadLocal類在實現思路上是很是相近的。
經過以上的分析,咱們發現,ThreadLocal類的使用雖然是用來解決多線程的問題的,可是仍是有很明顯的針對性。最明顯的,ThreadLoacl變量的活動範圍爲某線程,而且個人理解是該線程「專有的,獨自霸佔」,對該變量的全部操做均有該線程完成!也就是說,ThreadLocal不是用來解決共享,競爭問題的。典型的應用莫過於Spring,Hibernate等框架中對於多線程的處理了。
下面是hibernate中典型的ThreadLocal的應用:
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; }
這段代碼,每一個線程有本身的ThreadLocalMap,每一個ThreadLocalMap中根據須要初始加載threadSession,這樣的好處就是介於singleton與prototype之間,應用singleton沒法解決線程,應用prototype開銷又太大,有了ThreadLocal以後就行了,對於須要線程「霸佔」的變量用ThreadLocal,而該類實例的方法都可以共享。
關於內存泄漏:雖然ThreadLocalMap已經使用了weakReference,可是仍是建議可以顯示的使用remove方法。
ThreadLocal和線程同步機制相比有什麼優點呢?ThreadLocal和線程同步機制都是爲了解決多線程中相同變量的訪問衝突問題。
在同步機制中,經過對象的鎖機制保證同一時間只有一個線程訪問變量。這時該變量是多個線程共享的,使用同步機制要求程序縝密地分析何時對變量進行讀寫,何時須要鎖定某個對象,何時釋放對象鎖等繁雜的問題,程序設計和編寫難度相對較大。
而ThreadLocal則從另外一個角度來解決多線程的併發訪問。ThreadLocal爲每個線程提供一個獨立的變量副本,從而隔離了多個線程對訪問數據的衝突。由於每個線程都擁有本身的變量副本,從而也就沒有必要對該變量進行同步了。ThreadLocal提供了線程安全的對象封裝,在編寫多線程代碼時,能夠把不安全的變量封裝進ThreadLocal。
因爲ThreadLocal中能夠持有任何類型的對象,低版本JDK所提供的get()返回的是Object對象,須要強制類型轉換。但JDK 5.0經過泛型很好的解決了這個問題,在必定程度上簡化ThreadLocal的使用,代碼清單9-2就使用了JDK 5.0新的ThreadLocal<T>版本。
歸納起來講,對於多線程資源共享的問題,同步機制採用了「以時間換空間」的方式:訪問串行化,對象共享化。而ThreadLocal採用了「以空間換時間」的方式:訪問並行化,對象獨享化。前者僅提供一份變量,讓不一樣的線程排隊訪問,然後者爲每個線程都提供了一份變量,所以能夠同時訪問而互不影響。
咱們知道在通常狀況下,只有無狀態的Bean才能夠在多線程環境下共享,在Spring中,絕大部分Bean均可以聲明爲singleton做用域。就是由於Spring對一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非線程安全的「狀態性對象」採用ThreadLocal進行封裝,讓它們也成爲線程安全的「狀態性對象」,所以有狀態的Bean就可以以singleton的方式在多線程中正常工做了。
通常的Web應用劃分爲展示層、服務層和持久層三個層次,在不一樣的層中編寫對應的邏輯,下層經過接口向上層開放功能調用。在通常狀況下,從接收請求到返回響應所通過的全部程序調用都同屬於一個線程,如圖9-2所示。
這樣用戶就能夠根據須要,將一些非線程安全的變量以ThreadLocal存放,在同一次請求響應的調用線程中,全部對象所訪問的同一ThreadLocal變量都是當前線程所綁定的。
下面的實例可以體現Spring對有狀態Bean的改造思路:
TopicDao:非線程安全
public class TopicDao {
//①一個非線程安全的變量
private Connection conn;
public void addTopic(){
//②引用非線程安全變量
Statement stat = conn.createStatement();
// …
}
}
因爲①處的conn是成員變量,由於addTopic()方法是非線程安全的,必須在使用時建立一個新TopicDao實例(非singleton)。下面使用ThreadLocal對conn這個非線程安全的「狀態」進行改造:
TopicDao:線程安全
import java.sql.Connection;
import java.sql.Statement;
public class TopicDao {
//①使用ThreadLocal保存Connection變量
private static ThreadLocal<Connection> connThreadLocal = new ThreadLocal<Connection>();
public static Connection getConnection(){
//②若是connThreadLocal沒有本線程對應的Connection建立一個新的Connection,
//並將其保存到線程本地變量中
if (connThreadLocal.get() == null) {
Connection conn = ConnectionManager.getConnection();
connThreadLocal.set(conn);
return conn;
}else{
//③直接返回線程本地變量
return connThreadLocal.get();
}
}
public void addTopic() {
//④從ThreadLocal中獲取線程對應的
Statement stat = getConnection().createStatement();
}
}
不一樣的線程在使用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對有狀態類線程安全化的解決思路。
參考文獻: