1. 程式人生 > 實用技巧 >AndroidStudio 抓包工具Profiler使用

AndroidStudio 抓包工具Profiler使用

date: 2020-08-19 10:16:00
updated: 2020-08-19 10:16:00

ThreadLocal

參考網址

以資料庫連線為例,如果多個執行緒共享一個連線,有可能一個執行緒在對資料庫進行操作,另一個執行緒呼叫了closeConnection操作;如果在每一個執行緒都new一個連線物件,如果開啟關閉資料庫操作頻繁,會影響到伺服器壓力,並且影響程式執行效能。
=> ThreadLocal 內部維護一個 ThreadLocalMap 類,儲存的是 Entry<Thread K, Object V> 陣列,K是執行緒,V是值,這樣每一個執行緒無論在哪裡呼叫,都會拿到自己執行緒的值

每個Thread物件中都持有一個ThreadLocalMap的成員變數。每個ThreadLocalMap內部又維護了N個Entry節點,也就是Entry陣列,每個Entry代表一個完整的物件,key是ThreadLocal本身,value是ThreadLocal的泛型值

ThreadLocalMap的引用是在Thread裡的,所以它裡面的Entry陣列存放的是一個執行緒裡new出來的多個ThreadLocal物件

Thread維護了ThreadLocalMap,而ThreadLocalMap裡維護了Entry,而Entry裡存的是以ThreadLocal為key,傳入的值為value的鍵值對。

// java.lang.Thread類裡持有ThreadLocalMap的引用
public class Thread implements Runnable {
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

// java.lang.ThreadLocal有內部靜態類ThreadLocalMap
public class ThreadLocal<T> {
    static class ThreadLocalMap {
        private Entry[] table;
        
        // ThreadLocalMap內部有Entry類,Entry的key是ThreadLocal本身,value是泛型值
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
    }
}

主要方法:

  • initialValue:初始化。在get方法裡懶載入的。

    • 通常,每個執行緒最多呼叫一次此方法。但是如果已經呼叫了remove(),然後再次呼叫get()的話,則可以再次觸發initialValue。
    • 如果要重寫的話一般建議採取匿名內部類的方式重寫此方法,否則預設返回的是null。
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal() {
    @Override
    protected SimpleDateFormat initialValue() {
        return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
    }
    };
    // Java8的高逼格寫法
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =
        ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));
    
  • get:得到這個執行緒對應的value。如果呼叫get之前沒set過,則get內部會執行initialValue方法進行初始化。

    /**
    * 獲取當前執行緒下的entry裡的value值。
    * 先獲取當前執行緒下的ThreadLocalMap,
    * 然後以當前ThreadLocal為key取出map中的value
    */
    public T get() {
        // 獲取當前執行緒
        Thread t = Thread.currentThread();
        // 獲取當前執行緒對應的ThreadLocalMap物件。
        ThreadLocalMap map = getMap(t);
        // 若獲取到了。則獲取此ThreadLocalMap下的entry物件,若entry也獲取到了,那麼直接獲取entry對應的value返回即可。
        if (map != null) {
            // 獲取此ThreadLocalMap下的entry物件
            ThreadLocalMap.Entry e = map.getEntry(this);
            // 若entry也獲取到了
            if (e != null) {
                @SuppressWarnings("unchecked")
                // 直接獲取entry對應的value返回。
                T result = (T)e.value;
                return result;
            }
        }
        // 若沒獲取到ThreadLocalMap或沒獲取到Entry,則設定初始值。 懶載入方式
        return setInitialValue();
    }
    
    private Entry getEntry(ThreadLocal<?> key) {
        int i = key.threadLocalHashCode & (table.length - 1);
        Entry e = table[i];
        if (e != null && e.get() == key)
            return e;
        else
            // 如果e=null,說明出現碰撞問題,通過開放定址方法來繼續尋找
            return getEntryAfterMiss(key, i, e);
    }
    
    // 通過布長+1或-1來尋找下一個相鄰的位置
    private static int nextIndex(int i, int len) {
        return ((i + 1 < len) ? i + 1 : 0);
    }
    private static int prevIndex(int i, int len) {
        return ((i - 1 >= 0) ? i - 1 : len - 1);
    }
    
    
  • set:為這個執行緒設定一個新值。

    /**
    * 設定當前執行緒的執行緒區域性變數的值
    * 實際上ThreadLocal的值是放入了當前執行緒的一個ThreadLocalMap例項中,所以只能在本執行緒中訪問。
    */
    public void set(T value) {
        // 獲取當前執行緒
        Thread t = Thread.currentThread();
        // 獲取當前執行緒對應的ThreadLocalMap例項,注意這裡是將t傳進去了,t是當前執行緒,就是說ThreadLocalMap是線上程裡持有的引用。
        ThreadLocalMap map = getMap(t);
        // 若當前執行緒有對應的ThreadLocalMap例項,則將當前ThreadLocal物件作為key,value做為值存到ThreadLocalMap的entry裡。
        if (map != null)
            map.set(this, value);
        else
            // 若當前執行緒沒有對應的ThreadLocalMap例項,則建立ThreadLocalMap,並將此執行緒與之繫結
            createMap(t, value);
    }
    
  • remove:ThreadLocalMap鍵為弱引用,刪除這個執行緒對應的值,防止記憶體洩露的最佳手段。

碰撞解決與神奇的 0x61c88647(十進位制:1640531527)

兩種碰撞型別

- 只有一個ThreadLocal例項的時候(上面推薦的做法),當向thread-local變數中設定多個值的時產生的碰撞,碰撞解決是通過開放定址法, 且是線性探測(linear-probe)
- 多個ThreadLocal例項的時候,最極端的是每個執行緒都new一個ThreadLocal例項,此時利用特殊的雜湊碼0x61c88647大大降低碰撞的機率, 同時利用開放定址法處理碰撞

顯然ThreadLocalMap採用線性探測的方式解決Hash衝突的效率很低,如果有大量不同的ThreadLocal物件放入map中時傳送衝突,或者發生二次衝突,則效率很低。

所以這裡引出的良好建議是:每個執行緒只存一個變數,這樣的話所有的執行緒存放到map中的Key都是相同的ThreadLocal,如果一個執行緒要儲存多個變數,就需要建立多個ThreadLocal,多個ThreadLocal放入Map中時會極大的增加Hash衝突的可能。

key.threadLocalHashCode ==> AtomicInteger.getAndAdd(HASH_INCREMENT) 其中 HASH_INCREMENT = 0x61c88647,這個值是 為了讓雜湊碼能均勻的分佈在2的N次方的數組裡

This number represents the golden ratio (sqrt(5)-1) times two to the power of 31 ((sqrt(5)-1) * (2^31)). The result is then a golden number, either 2654435769 or -1640531527.

魔數0x61c88647的與斐波那契雜湊有關,0x61c88647對應的十進位制為1640531527。斐波那契雜湊的乘數可以用(long) ((1L << 31) * (Math.sqrt(5) - 1))可以得到2654435769,如果把這個值給轉為帶符號的int,則會得到-1640531527。換句話說(1L << 32) - (long) ((1L << 31) * (Math.sqrt(5) - 1))得到的結果就是1640531527也就是0x61c88647。通過理論與實踐,當我們用0x61c88647作為魔數累加為每個ThreadLocal分配各自的ID也就是threadLocalHashCode再與2的冪取模,得到的結果分佈很均勻。