(轉)Java多線程學習之ThreadLocal源碼分析

來源:互聯網
上載者:User

標籤:思想   衝突   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 的設計思想如下:

  1. ThreadLocal  只是個訪問局部變數的入口。
  2. 局部變數的值存線上程Thread 類本地,預設為null,只有通過ThreadLocal 訪問時才會進行初始化。
  3. [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源碼分析

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.