1. 程式人生 > >Android多執行緒之Java 8中ThreadLocal內部實現機制詳解

Android多執行緒之Java 8中ThreadLocal內部實現機制詳解

前言ThreadLocal是執行緒內部的儲存類,通過它可以實現在每個執行緒中儲存自己的私有資料。即資料儲存以後,只能在指定的執行緒中獲取這個儲存的物件,而其它執行緒則不能獲取到當前執行緒儲存的這個物件。ThreadLocal有一個典型的應用場景,即我們在前文中說到的Android執行緒間通訊中的Looper。每一個執行緒都有一個私有的Looper物件去處理當前執行緒的訊息佇列,有不清楚的同學,可以去上篇文章檢視。話不多說,今天我們主要探討的是ThreadLocal實現執行緒儲存私有資料的工作原理。

  • 上面我們提到,通過ThreadLocal能實現線上程中儲存的私有資料,下面我們來看一個典型的應用案例,我們在UI執行緒中執行如下程式碼:
 private void testThreadLocal() {
        final ThreadLocal<String> nameLocal = new ThreadLocal<>();
        nameLocal.set("我是UI主執行緒儲存的資料");
        new Thread(new Runnable() {
            @Override
            public void run() {
                nameLocal.set("我是子執行緒儲存的資料");
                //打印出當前執行緒和其儲存的資料
System.out.println(Thread.currentThread() + ":" + nameLocal.get()); } }).start(); //打印出當前執行緒和其儲存的資料 System.out.println(Thread.currentThread() + ":" + nameLocal.get()); }

上面程式碼中,我們在主執行緒中建立了一個nameLocal物件,並且向裡面寫入了一個字串。接下來,我們又開啟了一個新的子執行緒,又向同一個nameLocal

再寫入一個數據。按理說,既然nameLocal都是一個ThreadLocal物件,因此呼叫get方法去獲取存入的字串時,應該是一個相同的字串。但實際結果是怎樣?實際輸出結果如下:

03-12 15:49:49.726 19346-19346/? I/System.out: Thread[main,5,main]:我是UI主執行緒儲存的資料
03-12 15:49:49.726 19346-19358/? I/System.out: Thread[Thread-209,5,main]:我是子執行緒儲存的資料

通過列印的結果可以看出:在不同執行緒中,即使操作的同一個ThreadLocal物件,也能夠實現資料的私密儲存。但是,我們呼叫ThreadLocal的set方法的時候,操作的是同一個ThreadLocal物件,而且也沒有不同的Key去區分不同的value值,為什麼不會覆蓋上一次儲存的value?呼叫get方法為什麼能獲取到當前執行緒儲存的資料?帶著上面的兩個問題,我們一起走入ThreadLocal的原始碼世界,一探究竟。

ThreadLocal原始碼分析

  • ThreadLocal儲存值set方法詳解

從上面的示例可以看出,我們利用ThreadLocal儲存資料的時候,只需要簡單的在ThreadLocal物件上呼叫set(value)方法,即可以實現資料的儲存,而且實現執行緒的區分,怎麼實現的?接下來進入ThreadLocalset方法一探究竟:

 public void set(T var1) {
        //獲取呼叫set方法的當前執行緒
        Thread var2 = Thread.currentThread();
        //從執行緒中獲取當前執行緒中儲存的ThreadLocal的儲存物件ThreadLocalMap
        ThreadLocal.ThreadLocalMap var3 = this.getMap(var2);
        if(var3 != null) {
        //如果以前對ThreadLocalMap進行過初始化,直接儲存
            var3.set(this, var1);
        } else {
        //未進行過初始化,呼叫createMap方法先建立,再儲存資料
            this.createMap(var2, var1);
        }
    }

set方法內部實現邏輯非常簡便清晰,先獲取到當前執行緒的ThreadLocalMap物件,再通過這個物件去實現資訊的儲存。說起來簡單,但我們仍然對getMap幹了什麼事情?set方法和createMap方法怎麼進行資訊儲存?有很大的疑惑,接下來我們一一解析。先看getMap內部實現邏輯:

//ThreadLocal類中的getMap方法
    ThreadLocal.ThreadLocalMap getMap(Thread var1) {
        return var1.threadLocals;
    }

//Thread類中的threadLocals變數定義,每個執行緒都有這個變數
ThreadLocalMap threadLocals = null;

//ThreadLocalMap類的定義
static class ThreadLocalMap {
        //陣列的初始容量
        private static final int INITIAL_CAPACITY = 16;
        //儲存的ThreadLocal陣列,我們實現執行緒間私有化資料,就存放在這個陣列中
        private ThreadLocal.ThreadLocalMap.Entry[] table;
        //陣列的大小,不包括空資料
        private int size;
        //用於儲存陣列的總容量,包括為空的資料
        private int threshold;
  }

上邊羅列了getMap方法的實現和它用到的ThreadLocalMap 類的定義。getMap方法就是從當前執行緒中獲取它的ThreadLocalMap成員變數而已。關於ThreadLocalMap,我們現在只要知道它是一個儲存資料的物件即可,至於它內部實現機制,我們接下來會詳細講解。

接下來看一下ThreadLocalMap中關鍵的set方法是如何實現資料儲存的,原始碼如下:

private void set(ThreadLocal<?> var1, Object var2) {
            //ThreadLocal.ThreadLocalMap.Entry的定義見該方法下,其實它也是ThreadLocal物件,只是是虛引用物件
            ThreadLocal.ThreadLocalMap.Entry[] var3 = this.table;
            //獲取陣列的大小
            int var4 = var3.length;
            //通過按位與運算,獲取var1在陣列中的儲存位置。
            //補充(&類似於取模(%)運算,但是效率比%高很多,a%b可以用位運算計算:a&b-1)
            int var5 = var1.threadLocalHashCode & var4 - 1;
            //從陣列中取出ThreadLocal物件,相當於遍歷陣列中儲存的資料,只要取出來的資料不為空,就一直通過nextIndex方法獲取下一個位置的物件。一開始的時候,陣列為空,不會執行for迴圈中的程式碼。
            for(ThreadLocal.ThreadLocalMap.Entry var6 = var3[var5]; var6 != null; var6 = var3[var5 = nextIndex(var5, var4)]) {
            //當陣列不為空的時候,進入迴圈遍歷陣列操作,nextIndex方法就是取下一個位置
            //獲取到從陣列中取出來的那個ThreadLocal物件
                ThreadLocal var7 = (ThreadLocal)var6.get();
                //如果以前通過var1物件儲存過資料,只更新其值,結束方法(var即是呼叫set方法的那個ThreadLocal物件)
                if(var7 == var1) {
                    var6.value = var2;
                    return;
                }
                //遍歷完陣列,沒有找到以前通過var1儲存過資料的痕跡,就把資料儲存到陣列第一個為null的位置.結束方法
                if(var7 == null) {
                    this.replaceStaleEntry(var1, var2, var5);
                    return;
                }
            }
            //當陣列為空的時候,直接建立一個節點,並且新增到陣列中
            var3[var5] = new ThreadLocal.ThreadLocalMap.Entry(var1, var2);
            //下面的操作是對陣列進行重新計算操作
            int var8 = ++this.size;
            if(!this.cleanSomeSlots(var5, var8) && var8 >= this.threshold) {
                this.rehash();
            }
        }

//Entry 物件的定義,比ThreadLocal多了一個value物件
static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;

            Entry(ThreadLocal<?> var1, Object var2) {
                super(var1);
                this.value = var2;
            }
        }

最後,我們看一下通過ThreadLocal儲存資料時,整個呼叫鏈:

//建立ThreadLocal物件
ThreadLocal<String> nameLocal = new ThreadLocal<>();
//呼叫set方法儲存資料
nameLocal.set("value");
//獲取當前執行緒儲存的ThreadLocalMap 物件
Thread var2 = Thread.currentThread();
ThreadLocal.ThreadLocalMap var3 = this.getMap(var2);
//呼叫set方法或createMap儲存資料
var3.set(this, var1);//存在同一個ThreadLocal物件this.createMap(var2, var1);//不存在同一個ThreadLocal物件
//最後就是向table陣列中新增新的節點或更新舊結點
呼叫我們上面分析的set方法:
//當陣列為空時,直接呼叫如下程式碼新增節點
var3[var5] = new ThreadLocal.ThreadLocalMap.Entry(var1, var2);
//當陣列不為空時,迴圈更新或新增。更新為var6.value = var2覆蓋其值,新增是呼叫如下方法
this.replaceStaleEntry(var1, var2, var5);
  • ThreadLocal儲存值get方法詳解

    相比通過set方法儲存資料來說,獲取資料的get方法就要簡便得多。我們一般是通過呼叫nameLocal.get()來獲取資料,我們就先看一下ThreadLocal類的get方法是怎麼獲取資料的。方法實現如下:

 public T get() {
        //首先還是拿到當前執行緒關聯的ThreadLocalMap物件
        Thread var1 = Thread.currentThread();
        ThreadLocal.ThreadLocalMap var2 = this.getMap(var1);
        if(var2 != null) {
        //從陣列中取到指定位置(即通過hashcode和陣列大小計算出的,和上面儲存時獲取var5類似)的那個Entry物件,方法詳情見下
            ThreadLocal.ThreadLocalMap.Entry var3 = var2.getEntry(this);
            if(var3 != null) {
            //有資料就直接返回儲存的資料
                Object var4 = var3.value;
                return var4;
            }
        }
        //當沒有儲存的有資料,就是設定資料為null並且返回。方法實現邏輯會在後面說到。
        return this.setInitialValue();
    }

ThreadLocalMapgetEntry方法是怎麼獲取到指定ThreadLocal.ThreadLocalMap.Entry物件的?看一下內部實現:

private ThreadLocal.ThreadLocalMap.Entry getEntry(ThreadLocal<?> var1) {
            //獲取在陣列中的位置
            int var2 = var1.threadLocalHashCode & this.table.length - 1;
            //取當前位置的資料
            ThreadLocal.ThreadLocalMap.Entry var3 = this.table[var2];
            return var3 != null && var3.get() == var1?var3:this.getEntryAfterMiss(var1, var2, var3);
        }

ThreadLocalMapsetInitialValue方法又是怎麼去設定預設值並返回資料的?看一下它的原始碼:

 private T setInitialValue() {
         //this.initialValue()方法就是返回了一個null,因此var賦值為null
        Object var1 = this.initialValue();
        //獲取與當前執行緒相關聯的ThreadLocalMap物件
        Thread var2 = Thread.currentThread();
        ThreadLocal.ThreadLocalMap var3 = this.getMap(var2);
        if(var3 != null) {
           //上面講過方法的含義,這裡是重寫為null值
            var3.set(this, var1);
        } else {
            //上面講過方法的含義,這裡是新增一個為null的新節點
            this.createMap(var2, var1);
        }
        //返回null值
        return var1;
    }

//上面提到的initialValue()方法原始碼
   protected T initialValue() {
        return null;
    }

最後,還是看一下完整的get方法呼叫鏈:

//呼叫set方法從nameLocal中獲取資料
nameLocal.get();
//獲取當前執行緒儲存的ThreadLocalMap 物件
Thread var2 = Thread.currentThread();
ThreadLocal.ThreadLocalMap var3 = this.getMap(var2);
//如果var3為空,就建立一個ThreadLocalMap,並且給資料賦值為null,並返回。呼叫如下程式碼
return this.setInitialValue();、
//如果var3不為空,就獲取指定位置(通過hashcode計算而來,原理上面講過)的Entry物件
ThreadLocal.ThreadLocalMap.Entry var3 = var2.getEntry(this);
//返回entry中的資料
Object var4 = var3.value;
return var4;

總結:通過ThreadLocal實現資料的儲存和獲取原理到現在已經告一段落了。它是怎麼實現資料的執行緒私有化?其實很簡單,主要是通過執行緒的私有成員變數ThreadLocalMap實現的,而ThreadLocalMap中又有一個ThreadLocal.ThreadLocalMap.Entry[]實現資料的儲存。每次我們儲存或獲取資料都是對這個陣列進行操作而已。關於Android多執行緒的詳細講解,大家可以去 Android多執行緒相關知識總結 檢視。希望能幫助到你額。。。