1. 程式人生 > >ThreadLocal:多執行緒共享資源安全訪問新思路

ThreadLocal:多執行緒共享資源安全訪問新思路

ThreadLocal是解決執行緒安全問題一個很好的思路,ThreadLocal類中有一個Map,用於儲存每一個執行緒的變數副本,Map中元素的鍵為執行緒物件,而值對應執行緒的變數副本,由於Key值不可重複,每一個“執行緒物件”對應執行緒的“變數副本”,從而保證了執行緒安全。

我們知道Spring通過各種DAO模板類降低了開發者使用各種資料持久技術的難度。這些模板類都是執行緒安全的,也就是說,多個DAO可以複用同一個模板例項而不會發生衝突。

我們使用模板類訪問底層資料,根據持久化技術的不同,模板類需要繫結資料連線或會話的資源。但這些資源本身是非執行緒安全的,也就是說它們不能在同一時刻被多個執行緒共享。


雖然模板類通過資源池獲取資料連線或會話,但資源池本身解決的是資料連線或會話的快取問題,並非資料連線或會話的執行緒安全問題。

按照傳統經驗,如果某個物件是非執行緒安全的,在多執行緒環境下,對物件的訪問必須採用synchronized進行執行緒同步。但Spring的DAO模板類並未採用執行緒同步機制,因為執行緒同步限制了併發訪問,會帶來很大的效能損失。

此外,通過程式碼同步解決效能安全問題挑戰性很大,可能會增強好幾倍的實現難度。那模板類究竟仰丈何種魔法神功,可以在無需同步的情況下就化解執行緒安全的難題呢?答案就是ThreadLocal!

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


一,ThreadLocal是什麼?

早在JDK 1.2的版本中就提供java.lang.ThreadLocal,
ThreadLocal為解決多執行緒程式的併發問題提供了一種新的思路。使用這個工具類可以很簡潔地編寫出優美的多執行緒程式。

ThreadLocal很容易讓人望文生義,想當然地認為是一個“本地執行緒”。其實,ThreadLocal並不是一個Thread,而是Thread的區域性變數,也許把它命名為ThreadLocalVariable更容易讓人理解一些。

使用ThreadLocal維護變數時,ThreadLocal為每個使用該變數的執行緒提供獨立的變數副本,所以每一個執行緒都可以獨立地改變自己的副本,而不影響其它執行緒所對應的副本。


從執行緒的角度看,目標變數就像是執行緒的本地變數,這也是類名中“Local”所要表達的意思。

執行緒區域性變數並不是Java的新發明,很多語言(如IBM IBM XL FORTRAN)在語法層面就提供執行緒區域性變數。在Java中沒有提供在語言級支援,而是變相地通過ThreadLocal的類提供支援。

所以,在Java中編寫執行緒區域性變數的程式碼相對來說要笨拙一些,因此造成執行緒區域性變數沒有在Java開發者中得到很好的普及。

1,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中元素的鍵為執行緒物件,而值對應執行緒的變數副本。我們自己就可以提供一個簡單的實現版本:

// 程式碼清單1:SimpleThreadLocal.java
public class SimpleThreadLocal {
	private Map valueMap = Collections.synchronizedMap(new HashMap());

	public void set(Object newValue) {
		// 1,鍵為執行緒物件,值為本執行緒的變數副本
		valueMap.put(Thread.currentThread(), newValue);
	}

	public Object get() {
		Thread currentThread = Thread.currentThread();
		// 2,返回本執行緒對應的變數
		Object object = valueMap.get(currentThread);
		// 3,如果在Map中不存在,放到Map中儲存起來。
		if (object == null && !valueMap.containsKey(currentThread)) {
			object = initialValue();
			valueMap.put(currentThread, object);
		}
		return object;
	}

	public void remove() {
		valueMap.remove(Thread.currentThread());
	}

	public Object initialValue() {
		return null;
	}
}



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

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

// 程式碼清單2 SequenceNumber
public class SequenceNumber {

	//1,通過匿名內部類覆蓋ThreadLocal的initialValue()方法,指定初始值
	private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>() {
		public Integer initialValue() {
			return 0;
		}
	};

	//2,獲取下一個序列值
	public int getNextNum() {
		seqNum.set(seqNum.get() + 1);
		return seqNum.get();
	}

	public static void main(String[] args) {
		//3,3個執行緒共享sn,各自產生序列號
		SequenceNumber sn = new SequenceNumber();
		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() {
			//4,每個執行緒打出3個序列值
			for (int i = 0; i < 3; i++) {
				System.out.println("thread[" + Thread.currentThread().getName() + "] sn[" + sn.getNextNum() + "]");
			}
		}
	}
}



通常我們通過匿名內部類的方式定義ThreadLocal的子類,提供初始的變數值,如例子中1處所示。TestClient執行緒產生一組序列號,在3處,我們生成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]

考察輸出的結果資訊,我們發現每個執行緒所產生的序號雖然都共享同一個SequenceNumber例項,但它們並沒有發生相互干擾的情況,而是各自產生獨立的序列號,這是因為我們通過ThreadLocal為每一個執行緒提供了單獨的副本。

二,ThreadLocal和執行緒同步機制相比有什麼優勢呢?

1,相同點:ThreadLocal和執行緒同步機制都是為了解決多執行緒中相同變數的訪問衝突問題。
在同步機制中,通過物件的鎖機制保證同一時間只有一個執行緒訪問變數。這時該變數是多個執行緒共享的,使用同步機制要求程式慎密地分析什麼時候對變數進行讀寫,什麼時候需要鎖定某個物件,什麼時候釋放物件鎖等繁雜的問題,程式設計和編寫難度相對較大。
而ThreadLocal則從另一個角度來解決多執行緒的併發訪問。ThreadLocal會為每一個執行緒提供一個獨立的變數副本,從而隔離了多個執行緒對資料的訪問衝突。因為每一個執行緒都擁有自己的變數副本,從而也就沒有必要對該變數進行同步了。ThreadLocal提供了執行緒安全的共享物件,在編寫多執行緒程式碼時,可以把不安全的變數封裝進ThreadLocal。當然ThreadLocal並不能替代同步機制,兩者面向的問題領域不同。

2,不同點:同步機制是為了同步多個執行緒對相同資源的併發訪問,是為了多個執行緒之間進行通訊的有效方式;而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應用劃分為展現層、服務層和持久層三個層次,在不同的層中編寫對應的邏輯,下層通過介面向上層開放功能呼叫。在一般情況下,從接收請求到返回響應所經過的所有程式呼叫都同屬於一個執行緒。

這樣你就可以根據需要,將一些非執行緒安全的變數以ThreadLocal存放,在同一次請求響應的呼叫執行緒中,所有關聯的物件引用到的都是同一個變數。

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

// 程式碼清單3 TopicDao:非執行緒安全
public class TopicDao {
	// 1,一個非執行緒安全的變數
	private Connection conn;
	public void addTopic() {
		// 2,引用非執行緒安全變數
		Statement stat = conn.createStatement();
	}
}


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

// 程式碼清單4 TopicDao:執行緒安全
public class TopicDao {


	// 1,使用ThreadLocal儲存Connection變數
	private static ThreadLocal<Connection> connThreadLocal = new ThreadLocal<Connection>();


	public static Connection getConnection() {


		// 2,如果connThreadLocal沒有本執行緒對應的Connection建立一個新的Connection,並將其儲存到執行緒本地變數中。
		if (connThreadLocal.get() == null) {
			Connection conn = ConnectionManager.getConnection();
			connThreadLocal.set(conn);
			return conn;
		} else {
			// 3,直接返回執行緒本地變數
			return connThreadLocal.get();
		}
	}


	public void addTopic() {
		// 4,從ThreadLocal中獲取執行緒對應的Connection
		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對有狀態類執行緒安全化的解決思路。

小結

ThreadLocal是解決執行緒安全問題一個很好的思路,它通過為每個執行緒提供一個獨立的變數副本解決了變數併發訪問的衝突問題。在很多情況下,ThreadLocal比直接使用synchronized同步機制解決執行緒安全問題更簡單,更方便,且結果程式擁有更高的併發性。

最後,提供個Myeclipse對Hibernate自動生成的建立會化工廠類(HibernateSessionFactory.java),此類就是用ThreadLocal起到了每個執行緒擁有完全獨立的Session物件的作用。