ThreadLocal 基礎知識

ThreadLocal是什麼

     ·java

        早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal爲解決多線程程序的併發問題提供了一種新的思路。使用這個工具類能夠很簡潔地編寫出優美的多線程程序。sql

        ThreadLocal,顧名思義,它不是一個線程,而是線程的一個本地化對象。當工做於多線程中的對象使用ThreadLocal維護變量時,ThreadLocal爲每一個使用該變量的線程分配一個獨立的變量副本。因此每個線程均可以獨立的改變本身的副本,而不會影響其餘線程所對應的副本,這個變量就像是線程的本地變量,這也是類名中「Local'所要表達的意思。安全

        線程局部變量在別的語言中也有,而且在語法層面實現,在java中沒有提供語言級支持,而是一種變通的方法,經過ThreadLocal的類提供支持,因此,在java中編寫線程局部變量的代碼相對來講要笨拙一些所以形成線程局部變量沒有在Java開發者中獲得很好的普及。多線程


ThreadLocal的接口方法

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

  • void set(Object value)設置當前線程的線程局部變量的值。工具

  • public Object get()該方法返回當前線程所對應的線程局部變量。this

  • public void remove()將當前線程局部變量的值刪除,目的是爲了減小內存的佔用,該方法是JDK 5.0新增的方法。須要指出的是,當線程結束後,對應該線程的局部變量將自動被垃圾回收,因此顯式調用該方法清除線程的局部變量並非必須的操做,但它能夠加快內存回收的速度。spa

  • 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原理:

import java.util.Collections;
import java.util.HashMap;
import java.util.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;
	}

}


一個ThreadLcal實例:

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();
		//線程共享sn,各自產生序列號
		TestClient c1= new TestClient(sn);
		TestClient c2= new TestClient(sn);
		TestClient c3= new TestClient(sn);
		c1.start();
		c2.start();
		c3.start();
	}
	
}

public 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的子類,提供初始的變量值。

與Thread同步機制的比較

    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

圖1同一線程貫通三層

        這樣你就能夠根據須要,將一些非線程安全的變量以ThreadLocal存放,在同一次請求響應的調用線程中,全部關聯的對象引用到的都是同一個變量

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

代碼清單3 TopicDao:非線程安全

public class TopicDao {

private Connection conn;一個非線程安全的變量

public void addTopic(){

Statement stat = conn.createStatement();引用非線程安全變量

}

}

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

代碼清單4 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中獲取線程對應的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。


關於InheritableThreadLocal

InheritableThreadLocal類是ThreadLocal類的子類。ThreadLocal中每一個線程擁有它本身的值,與ThreadLocal不一樣的是,InheritableThreadLocal容許一個線程以及該線程建立的全部子線程均可以訪問它保存的值。

【注:全部子線程都會繼承父線程保存的ThreadLocal值】

小結

    ThreadLocal是解決線程安全問題一個很好的思路,它經過爲每一個線程提供一個獨立的變量副本解決了變量併發訪問的衝突問題。在不少狀況下,ThreadLocal比直接使用synchronized同步機制解決線程安全問題更簡單,更方便,且結果程序擁有更高的併發性。

    最後再提一句,ThreadLocal變量的這種隔離策略,也不是任何狀況下都能使用的。若是多個線程併發訪問的對象實例只容許,也只能建立那麼一個,那就沒有別的辦法了,老老實實的使用同步機制來訪問吧。

相關文章
相關標籤/搜索