標籤:思想 衝突 main swa ges 產生 詳細 mes runnable
(轉) http://www.cnblogs.com/moongeek/p/7857794.html
ThreadLocal,即執行緒區域變數。它可以將變數綁定到特定的線程上的入口,使每個線程都擁有改變數的一個拷貝,各線程相同變數間互不影響,是實現共用資源的輕量級同步。
下面是個ThreadLocal使用的執行個體,兩個任務共用同一個變數,並且兩個任務都把該變數設定為了線程私人變數,這樣,雖然兩個任務都”持有“同一變數,但各自持有該變數的拷貝。因此,當一個線程修改該變數時,不會影響另一線程該變數的值。
1 public class LocalTest1 implements Runnable { 2 // 一般會把 ThreadLocal 設定為static 。它只是個為線程設定局部變數的入口,多個線程只需要一個入口 3 private static ThreadLocal<Student> localStudent = new ThreadLocal() { 4 // 一般會重寫初始化方法,一會分析源碼時候會解釋為什麼 5 @Override 6 public Student initialValue() { 7 return new Student(); 8 } 9 };10 11 private Student student = null;12 13 @Override14 public void run() {15 String threadName = Thread.currentThread().getName();16 17 System.out.println("【" + threadName + "】:is running !");18 19 Random ramdom = new Random();20 //隨機產生一個變數21 int age = ramdom.nextInt(100);22 23 System.out.println("【" + threadName + "】:set age to :" + age);24 // 獲得線程局部變數,改變屬性值25 Student stu = getStudent();26 stu.setAge(age);27 28 System.out.println("【" + threadName + "】:第一次讀到的age值為 :" + stu.getAge());29 30 try {31 TimeUnit.SECONDS.sleep(2);32 } catch (InterruptedException e) {33 e.printStackTrace();34 }35 36 System.out.println("【" + threadName + "】:第二次讀到的age值為 :" + stu.getAge());37 }38 39 public Student getStudent() {40 student = localStudent.get();41 42 // 如果不重寫初始化方法,則需要判斷是否為空白,然後手動為ThreadLocal賦值43 // if(student == null){44 // student = new Student();45 // localStudent.set(student);46 // }47 48 return student;49 }50 51 public static void main(String[] args) {52 LocalTest1 ll = new LocalTest1();53 Thread t1 = new Thread(ll, "線程1");54 Thread t2 = new Thread(ll, "線程2");55 56 t1.start();57 t2.start();58 }59 }60 61 public class Student{62 private int age;63 64 public Student(){65 66 }67 public Student(int age){68 this.age = age;69 }70 71 public int getAge() {72 return age;73 }74 75 public void setAge(int age) {76 this.age = age;77 }78 }
運行結果:
【線程1】:is running !【線程2】:is running !【線程2】:set age to :45【線程1】:set age to :25【線程1】:第一次讀到的age值為 :25【線程2】:第一次讀到的age值為 :45【線程1】:第二次讀到的age值為 :25【線程2】:第二次讀到的age值為 :45
1、ThreadLocal 源碼分析
ThreadLocal 源碼有很多方法,但是暴露出來的公用介面只有三個:
public ThreadLocal{ public T get() {} public void set(T value) {} public void remove() {}}
set(T value) 是設定局部變數的方法,源碼如下:
1 public void set(T value) { 2 // 獲得當前線程 3 Thread t = Thread.currentThread(); 4 // 獲得當前線程的 ThreadLocalMap 引用,詳細見下 5 ThreadLocalMap map = getMap(t); 6 // 如果不為空白,則更新局部變數的值 7 if (map != null) 8 map.set(this, value); 9 //如果不是第一次使用,先進行初始化10 else11 createMap(t, value);12 }
getMap(t) 源碼如下,每一個Thread變數都內建了一個ThreadLocalMap類型的成員變數,用於儲存該線程的成員變數。
1 ThreadLocalMap getMap(Thread t) {2 //返回該線程Thread的成員變數threadLocals3 return t.threadLocals;4 }
但是,Thread 預設把threadLocals設定為了null,因此第一次使用局部變數時候需要先初始化。
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocalMap 是定義在ThreadLocal 類裡的內部類,它的作用是儲存線程的局部變數。ThreadLocalMap 以ThreadLocal的引用作為鍵,以局部變數作為值,儲存在ThreadLocalMap.Entry (一種儲存索引值的資料結構)裡。關於ThreadLocalMap 的源碼,後文會詳細介紹,這裡只要知道大概原理即可。
由此我們可以總結ThreadLocal 的設計思想如下:
ThreadLocal 只是個訪問局部變數的入口。
- 局部變數的值存線上程
Thread 類本地,預設為null,只有通過ThreadLocal 訪問時才會進行初始化。
- [ThreadLocalMap 的設計思路在後文介紹
ThreadLocalMap 源碼時候會分析]
get() 是獲得執行緒區域變數,源碼如下:
1 public T get() { 2 //獲得當前線程 3 Thread t = Thread.currentThread(); 4 //得到當前線程的一個threadLocals 變數 5 ThreadLocalMap map = getMap(t); 6 if (map != null) { 7 // 如果不為空白,以當前ThreadLocal為主鍵獲得對應的Entry 8 ThreadLocalMap.Entry e = map.getEntry(this); 9 if (e != null) {10 @SuppressWarnings("unchecked")11 T result = (T)e.value;12 return result;13 }14 }15 //如果值為空白,則進行初始化16 return setInitialValue();17 }
再來看看初始化函數setInitialValue() 所進行的操作:
1 private T setInitialValue() { 2 //獲得初始預設值 3 T value = initialValue(); 4 //得到當前線程 5 Thread t = Thread.currentThread(); 6 // 獲得該線程的ThreadLocalMap引用 7 ThreadLocalMap map = getMap(t); 8 //不為空白則覆蓋 9 if (map != null)10 map.set(this, value);11 else12 //若是為空白,則進行初始化,鍵為本ThreadLocal變數,值為預設值13 createMap(t, value);14 }15 16 // 預設初始化返回null值,這也是為什麼需要重寫該方法的原因。如果沒有重寫,第一次get()操作獲得的執行緒區域變數為null,需要進行判斷並手動調用set()進行初始化17 protected T initialValue() {18 return null;19 }2、ThreadLocalMap 源碼分析
Thread類中包含一個ThreadLocalMap 類型的成員變數threadLocals,這是直接儲存線程局部變數的資料結構。ThreadLocal 只是一個入口,通過ThreadLocal操作threadLocals,進行局部變數的查改操作。這也是為什麼ThreadLocal 暴露的公有介面才三個的原因吧。同時,由於ThreadLocalMap 中的鍵是ThreadLocal類,也說明了,如果想為一個線程設定多個本地局部變數,需要設定多個 ThreadLocal。下面來分析下ThreadLocalMap 的源碼。
ThreadLocalMap 裡有幾個核心的屬性,和HashMap相似:
// table 預設大小,大小為2的次方,用於hash定位private static final int INITIAL_CAPACITY = 16;// 存放索引值對的數組private Entry[] table;// 擴容的臨界值,當table元素大到這個值,會進行擴容private int threshold;
在調用ThreadLocal 中的set(T) 方法時,調用了ThreadLocalMap 的set(ThreadLocal, T) 方法,
1 private void set(ThreadLocal<?> key, Object value) { 2 Entry[] tab = table; 3 int len = tab.length; 4 // Hash 定址,與table數組長度減1(二進位全是1)相與,所以數組長度必須為2的次方,減小hash重複的可能性 5 int i = key.threadLocalHashCode & (len-1); 6 7 //從hash值計算出的下標開始遍曆 8 for (Entry e = tab[i]; 9 e != null;10 e = tab[i = nextIndex(i, len)]) {11 //獲得該Entry的鍵12 ThreadLocal<?> k = e.get();13 //如果鍵和傳過來的相同,覆蓋原值,也說明,一個ThreadLocal變數只能為一個線程儲存一個局部變數14 if (k == key) {15 e.value = value;16 return;17 }18 // 鍵為空白,則替換該節點19 if (k == null) {20 replaceStaleEntry(key, value, i);21 return;22 }23 }24 25 tab[i] = new Entry(key, value);26 int sz = ++size;27 //是否需要擴容28 if (!cleanSomeSlots(i, sz) && sz >= threshold)29 rehash();30 }
為什麼說數組長度為2的次方有利於hash計算不重複呢?我們來看下,顯然,和一個二進位全是1的數相於,能最大限度的保證原數的所有位元,因而重複幾率會變小。
可以看出ThreadLocalMap 採用線性探測再散列解決Hash衝突的問題。即,如果一次Hash計算出來的數組下標被佔用,即hash值重複了,則在該下標的基礎上加1測試下一個下標,直到找到空值。比如說,Hash計算出來下標i為6,table[6] 已經有值了,那麼就嘗試table[7]是否被佔用,依次類推,直到找到空值。以上,就是儲存執行緒區域變數的方法。
再來分析下ThreadLocal 中的get() 方法,其中調用了ThreadLocalMap 的map.getEntry(this) 方法,並把本ThreadLocal作為參數傳入,返回一個ThreadLocalMap.Entry對象(以後簡稱Entry),源碼如下:
1 private Entry getEntry(ThreadLocal<?> key) { 2 //Hash計算數組下標 3 int i = key.threadLocalHashCode & (table.length - 1); 4 //得到該下標的節點 5 Entry e = table[i]; 6 //如果該節點存在,並且鍵和傳過來的ThreadLocal對象相同,則返回該節點(說明該節點沒有進行Hash衝突處理) 7 if (e != null && e.get() == key) 8 return e; 9 //如果該節點不直接滿足需求,可能進行了Hash衝突處理,則另外處理10 else11 return getEntryAfterMiss(key, i, e);12 }
再來分析下getEntryAfterMiss(ThreadLocal, int , Entry) 的源碼:
1 // if (e == null || e.get() != key) 2 private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { 3 Entry[] tab = table; 4 int len = tab.length; 5 //從洗標為i開始遍曆,直到遇到下一空節點或或是滿足需求的節點 6 while (e != null) { 7 ThreadLocal<?> k = e.get(); 8 if (k == key) 9 return e;10 if (k == null)11 //節點不為空白,鍵為空白,則清理該節點12 expungeStaleEntry(i);13 else14 // i後移15 i = nextIndex(i, len);16 e = tab[i];17 }18 //否則返回空值19 return null;20 }
以上就是ThreadLocalMap 幾個比較關鍵的源碼分析。
綜上所述可知,ThreadLocal 只是訪問Thread本地變數的一個入口,正真儲存本地變數的其實是在Thread本地,同時ThreadLocal也作為一個鍵去Hash找到變數所在的位置。也許你會想,為什麼不把ThreadLocalMap設定為< Thread,Variable>類型,把Thread作為主鍵,而要增加一個中間模組ThreadLocal?我的想法是,一來,這樣確實可以滿足需求,但是這樣無法進行hash尋找,如果一個Thread的本地變數過多,通過線性尋找會花費大量時間,使用ThreadLocal作為中間鍵,可以進行Hash尋找;二來,其實本地變數的添加、尋找和刪除需要進行大量的操作,設計者的思路是把這些操作封裝在一個ThreadLocal類裡,而只暴露了三個常用的介面,如果把ThreadLocal去掉,這些操作可能要寫在Thread類裡,違背了設計類的“單一性”原則;三來,我們這樣相當於為每個本地變數取了個“名字”(即,一個ThreadLocal對應一個本地變數),使用方便。
(轉)Java多線程學習之ThreadLocal源碼分析