ThreadLocal并不是一個Thread,而是Thread的局部變量,也許把它命名為ThreadLocalVariable更容易讓人理解一些。
當使用ThreadLocal維護變量時,ThreadLocal為每個使用該變量的線程提供獨立的變量副本,所以每一個線程都可以獨立地改變自己的副本,而不會影響其它線程所對應的副本。
從線程的角度看,目標變量就象是線程的本地變量,這也是類名中“Local”所要表達的意思。
ThreadLocal的接口方法ThreadLocal類接口很簡單,只有4個方法,我們先來了解一下:
設置當前線程的線程局部變量的值。
該方法返回當前線程所對應的線程局部變量。
將當前線程局部變量的值刪除,目的是為了減少內存的占用,該方法是JDK 5.0新增的方法。需要指出的是,當線程結束后,對應該線程的局部變量將自動被垃圾回收,所以顯式調用該方法清除線程的局部變量并不是必須的操作,但它可以加快內存回收的速度。
返回該線程局部變量的初始值,該方法是一個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是如何做到為每一個線程維護變量的副本的呢?其實實現的思路很簡單:在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);//②返回本線程對應的變量 if (o == null && !valueMap.containsKey(currentThread)) { //③如果在Map中不存在,放到Map中保存起來。 o = initialValue(); valueMap.put(currentThread, o); } return o; } public void remove() { valueMap.remove(Thread.currentThread()); } public Object initialValue() { return null; }}雖然上面代碼ThreadLocal實現版本顯得比較幼稚,但它和JDK所提供的ThreadLocal類在實現思路上是相近的。
一個TheadLocal實例下面,我們通過一個具體的實例了解一下ThreadLocal的具體使用方法。
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() { for (int i = 0; i < 3; i++) {//④每個線程打出3個序列值 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]
考察輸出的結果信息,我們發現每個線程所產生的序號雖然都共享同一個SequenceNumber實例,但它們并沒有發生相互干擾的情況,而是各自產生獨立的序列號,這是因為我們通過ThreadLocal為每一個線程提供了單獨的副本。
ThreadLocal的作用ThreadLocal 不是用來解決共享對象的多線程訪問問題的,一般情況下,通過ThreadLocal.set() 到線程中的對象是該線程自己使用的對象,其他線程是不需要訪問的,也訪問不到的。各個線程中訪問的是不同的對象。(注意這里說的只是“一般情況”,如果通過ThreadLocal.set() 到線程中的對象是多線程共享的同一個對象,各個線程中訪問的將是同一個共享對象)。
1.提供了保存對象的方法:每個線程中都有一個自己的ThreadLocalMap類對象,可以將線程自己的對象保持到其中,各管各的,線程可以正確的訪問到自己的對象。
2.避免參數傳遞的方便的對象訪問方式:將一個共用的ThreadLocal靜態實例作為key,將不同對象的引用保存到不同線程的ThreadLocalMap中,然后在線程執行的各處通過這個靜態ThreadLocal實例的get()方法取得自己線程保存的那個對象,避免了將這個對象作為參數傳遞的麻煩。
理解ThreadLocal中提到的變量副本“當使用ThreadLocal維護變量時,ThreadLocal為每個使用該變量的線程提供獨立的變量副本” —— 并不是通過ThreadLocal.set( )實現的,而是每個線程使用“new對象”(或拷貝) 的操作來創建對象副本, 通過ThreadLocal.set()將這個新創建的對象的引用保存到各線程的自己的一個map中,每個線程都有這樣一個map,執行ThreadLocal.get()時,各線程從自己的map中取出放進去的對象,因此取出來的是各自自己線程中的對象(ThreadLocal實例是作為map的key來使用的)。
如果ThreadLocal.set( )進去的對象是多線程共享的同一個對象,那么ThreadLocal.get( )取得的還是這個共享對象本身 —— 那么ThreadLocal還是有并發訪問問題的!
/* * 如果ThreadLocal.set()進去的是一個多線程共享對象,那么Thread.get()獲取的還是這個共享對象本身—————并不是該共享對象的副本。 * 假如:其中其中一個線程對這個共享對象內容作了修改,那么將會反映到其它線程獲取的共享對象中————所以說 ThreadLocal還是有并發訪問問題的! */ public class Test implements Runnable { private ThreadLocal<Person> threadLocal = new ThreadLocal<Person>(); private Person person; public Test(Person person) { this.person = person; } public static void main(String[] args) throws InterruptedException { //多線程共享的對象 Person sharePerson = new Person(110,"Sone"); Test test = new Test(sharePerson); System.out.println("sharePerson原始內容:"+sharePerson); Thread th = new Thread(test); th.start(); th.join(); //通過ThreadLocal獲取對象 Person localPerson = test.getPerson(); System.out.println("判斷localPerson與sharePerson的引用是否一致:"+(localPerson==localPerson)); System.out.println("sharePerson被改動之后的內容:"+sharePerson); } @Override public void run() { String threadName = Thread.currentThread().getName(); System.out.println(threadName+":Get a copy of the variable and change!!!"); Person p = getPerson(); p.setId(741741); p.setName("Boy"); } public Person getPerson(){ Person p = (Person)threadLocal.get(); if (p==null) { p= this.person; //set():進去的是多線程共享的對象 threadLocal.set(p); } return p; }ThreadLocal使用的一般步驟 1、在多線程的類(如ThreadDemo類)中,創建一個ThreadLocal對象threadXxx,用來保存線程間需要隔離處理的對象xxx。
2、在ThreadDemo類中,創建一個獲取要隔離訪問的數據的方法getXxx(),在方法中判斷,若ThreadLocal對象為null時候,應該new()一個隔離訪問類型的對象,并強制轉換為要應用的類型。
3、在ThreadDemo類的run()方法中,通過getXxx()方法獲取要操作的數據,這樣可以保證每個線程對應一個數據對象,在任何時刻都操作的是這個對象。
Code/** * 學生 */public class Student { private int age = 0; //年齡 public int getAge() { return this.age; } public void setAge(int age) { this.age = age; }}/** * 多線程下測試程序 */public class ThreadLocalDemo implements Runnable { //創建線程局部變量studentLocal,在后面你會發現用來保存Student對象 private final static ThreadLocal studentLocal = new ThreadLocal(); public static void main(String[] agrs) { ThreadLocalDemo td = new ThreadLocalDemo(); Thread t1 = new Thread(td, "a"); Thread t2 = new Thread(td, "b"); t1.start(); t2.start(); } public void run() { accessStudent(); } /** * 示例業務方法,用來測試 */ public void accessStudent() { //獲取當前線程的名字 String currentThreadName = Thread.currentThread().getName(); System.out.println(currentThreadName + " is running!"); //產生一個隨機數并打印 Random random = new Random(); int age = random.nextInt(100); System.out.println("thread " + currentThreadName + " set age to:" + age); //獲取一個Student對象,并將隨機數年齡插入到對象屬性中 Student student = getStudent(); student.setAge(age); System.out.println("thread " + currentThreadName + " first read age is:" + student.getAge()); try { Thread.sleep(500); } catch (InterruptedException ex) { ex.printStackTrace(); } System.out.println("thread " + currentThreadName + " second read age is:" + student.getAge()); } protected Student getStudent() { //獲取本地線程變量并強制轉換為Student類型 Student student = (Student) studentLocal.get(); //線程首次執行此方法的時候,studentLocal.get()肯定為null if (student == null) { //創建一個Student對象,并保存到本地線程變量studentLocal中 student = new Student(); studentLocal.set(student); } return student; }}運行結果:
a is running! thread a set age to:76 b is running! thread b set age to:27 thread a first read age is:76 thread b first read age is:27 thread a second read age is:76 thread b second read age is:27
可以看到a、b兩個線程age在不同時刻打印的值是完全相同的。這個程序通過妙用ThreadLocal,既實現多線程并發,游兼顧數據的安全性。
Code下面通過這樣一個例子來說明:模擬一個游戲,預先隨機設定一個[1, 10]的整數,然后每個玩家去猜這個數字,每個玩家不知道其他玩家的猜測結果,看誰用最少的次數猜中這個數字。
這個游戲確實比較無聊,不過這里恰好可以把每個玩家作為一個線程,然后用ThreadLocal來記錄玩家猜測的歷史記錄,這樣就很容易理解ThreadLocal的作用。
Judge:用來設定目標數字以及判斷猜測的結果。
Player:每個Player作為一個線程,多個Player并行地去嘗試猜測,猜中時線程終止。
Attempt:具有ThreadLocal字段和猜測動作靜態方法的類,ThreadLocal用于保存猜過的數字。
Record:保存歷史記錄的數據結構,有一個List<Integer>字段。
ThreadLocal為了可以兼容各種類型的數據,實際的內容是再通過set和get操作的對象,詳見Attempt的getRecord()。
運行的時候,每個Player Thread都是去調用Attemp.guess()方法,進而操作同一個ThreadLocal變量history,但卻可以保存每個線程自己的數據,這就是ThreadLocal的作用。
public class ThreadLocalTest { public static void main(String[] args) { Judge.prepare(); new Player(1).start(); new Player(2).start(); new Player(3).start(); } }class Judge { public static int MAX_VALUE = 10; private static int targetValue; public static void prepare() { Random random = new Random(); targetValue = random.nextInt(MAX_VALUE) + 1; } public static boolean judge(int value) { return value == targetValue; } }class Player extends Thread { private int playerId; public Player(int playerId) { this.playerId = playerId; } @Override public void run() { boolean success = false; while(!success) { int value = Attempt.guess(Judge.MAX_VALUE); success = Judge.judge(value); System.out.println(String.format("Plyaer %s Attempts %s and %s", playerId, value, success ? " Success" : "Failed")); } Attempt.review(String.format("[IFNO] Plyaer %s Completed by ", playerId)); } }class Attempt { private static ThreadLocal<Record> history = new ThreadLocal<Record>(); public static int guess(int maxValue) { Record record = getRecord(); Random random = new Random(); int value = 0; do { value = random.nextInt(maxValue) + 1; } while (record.contains(value)); record.save(value); return value; } public static void review(String info) { System.out.println(info + getRecord()); } private static Record getRecord() { Record record = history.get(); if(record == null) { record = new Record(); history.set(record); } return record; } }class Record { private List<Integer> attemptList = new ArrayList<Integer>();; public void save(int value) { attemptList.add(value); } public boolean contains(int value) { return attemptList.contains(value); } @Override public String toString() { StringBuffer buffer = new StringBuffer(); buffer.append(attemptList.size() + " Times: "); int count = 1; for(Integer attempt : attemptList) { buffer.append(attempt); if(count < attemptList.size()) { buffer.append(", "); count++; } } return buffer.toString(); } }運行結果
Plyaer 2 Attempts 8 and FailedPlyaer 3 Attempts 6 and FailedPlyaer 1 Attempts 5 and FailedPlyaer 2 Attempts 7 and SuccessPlyaer 3 Attempts 9 and FailedPlyaer 1 Attempts 9 and FailedPlyaer 3 Attempts 2 and FailedPlyaer 1 Attempts 2 and Failed[IFNO] Plyaer 2 Completed by 2 Times: 8, 7Plyaer 3 Attempts 4 and FailedPlyaer 1 Attempts 1 and FailedPlyaer 3 Attempts 5 and FailedPlyaer 1 Attempts 3 and FailedPlyaer 3 Attempts 1 and FailedPlyaer 1 Attempts 10 and FailedPlyaer 3 Attempts 8 and FailedPlyaer 1 Attempts 6 and FailedPlyaer 3 Attempts 7 and SuccessPlyaer 1 Attempts 4 and Failed[IFNO] Plyaer 3 Completed by 8 Times: 6, 9, 2, 4, 5, 1, 8, 7Plyaer 1 Attempts 7 and Success[IFNO] Plyaer 1 Completed by 9 Times: 5, 9, 2, 1, 3, 10, 6, 4, 7hreadLocal get()
關于ThreadLocal的原理,可以從其get()方法的實現來看
public class ThreadLocal<T> { ... public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) return (T)e.value; } return setInitialValue(); } ThreadLocalMap getMap(Thread t) { return t.threadLocals; } ...}執行get()時首先獲取當前的Thread,再獲取Thread中的ThreadLocalMap - t.threadLocals,并以自身為key取出實際的value。于是可以看出,ThreadLocal的變量實際還是保存在Thread中的,容器是一個Map,Thread用到多少ThreadLocal變量,就會有多少以其為key的Entry。
我是天王蓋地虎的分割線參考:http://gaofeihang.blog.163.com/blog/static/84508285201293071940706/
http://blog.csdn.net/qjyong/article/details/2158097(據說這篇文章有些問題,我只選擇其中一部分參考)
http://lavasoft.blog.51cto.com/62575/51926/
新聞熱點
疑難解答