分類
發燒車訊

ThreadLocal源碼解析-Java8,ThreadLocal的使用場景分析,利用線性探測法解決hash衝突,Java-強引用、軟引用、弱引用、虛引用,利用線性探測法解決hash衝突,分析ThreadLocal的弱引用與內存泄漏問題-Java8

目錄

一.ThreadLocal介紹

  1.1 ThreadLocal的功能

  1.2 ThreadLocal使用示例

二.源碼分析-ThreadLocal

  2.1 ThreadLocal的類層級關係

  2.2 ThreadLocal的屬性字段

  2.3 創建ThreadLocal對象

  2.4 ThreadLocal-set操作

  2.5 ThreadLocal-get操作

  2.6 ThreadLocal-remove操作

三.ThreadLocalMap類

  3.0 線性探測算法解決hash衝突

  3.1 Entry內部類

  3.2 ThreadLocalMap的常量介紹

  3.3 實例化ThreadLocalMap

  3.4 ThreadLocalMap的set操作

  3.5 清理陳舊Entry和rehash

四.總結 

 

一.介紹ThreadLocal

1.1ThreadLocal的功能

  我們知道,變量從作用域範圍進行分類,可以分為“全局變量”、“局部變量”兩種:

  1.全局變量(global variable),比如類的靜態屬性(加static關鍵字),在類的整個生命周期都有效;

  2.局部變量(local variable),比如在一個方法中定義的變量,作用域只是在當前方法內,方法執行完畢后,變量就銷毀(釋放)了;

  使用全局變量,當多個線程同時修改靜態屬性,就容易出現併發問題,導致臟數據;而局部變量一般來說不會出現併發問題(在方法中開啟多線程併發修改局部變量,仍可能引起併發問題);

  再看ThreadLocal,可以用來保存局部變量,只不過這個“局部”是指“線程”作用域,也就是說,該變量在該線程的整個生命周期中有效。

  關於ThreadLocal的使用場景,可以查看ThreadLocal的使用場景分析。

 

1.2ThreadLocal的使用示例

  ThreadLocal使用非常簡單。

package cn.ganlixin;

import org.junit.Test;

import java.util.Arrays;
import java.util.List;

public class TestThreadLocal {

    private static class Goods {
        public Integer id;
        public List<String> tags;
    }

    @Test
    public void testReference() {
        Goods goods1 = new Goods();
        goods1.id = 10;
        goods1.tags = Arrays.asList("healthy", "cheap");

        ThreadLocal<Goods> threadLocal = new ThreadLocal<>();
        threadLocal.set(goods1);

        Goods goods2 = threadLocal.get();
        System.out.println(goods1); // cn.ganlixin.TestThreadLocal$Goods@1c655221
        System.out.println(goods2); // cn.ganlixin.TestThreadLocal$Goods@1c655221

        goods2.id = 100;
        System.out.println(goods1.id);  // 100
        System.out.println(goods2.id);  // 100

        threadLocal.remove();
        System.out.println(threadLocal.get()); // null
    }

    @Test
    public void test2() {
        // 一個線程中,可以創建多個ThreadLocal對象,多個ThreadLoca對象互不影響
        ThreadLocal<String> threadLocal1 = new ThreadLocal<>();
        ThreadLocal<String> threadLocal2 = new ThreadLocal<>();
        // ThreadLocal存的值默認為null

        System.out.println(threadLocal1.get()); // null

        threadLocal1.set("this is value1");
        threadLocal2.set("this is value2");
        System.out.println(threadLocal1.get()); // this is value1
        System.out.println(threadLocal2.get());  // this is value2

        // 可以重寫initialValue進行設置初始值
        ThreadLocal<String> threadLocal3 = new ThreadLocal<String>() {
            @Override
            protected String initialValue() {
                return "this is initial value";
            }
        };
        System.out.println(threadLocal3.get()); // this is initial value
    }
}

  

二.源碼分析-ThreadLocal

2.1ThreadLocal類層級關係

  

  ThreadLocal類中有一個內部類ThreadLocalMap,這個類特別重要,ThreadLocal的各種操作基本都是圍繞ThreadLocalMap進行的

  對於ThreadLocalMap有來說,它內部定義了一個Entry內部類,有一個table屬性,是一個Entry數組,和HashMap有一些相似的地方,但是ThreadLocalMap和HashMap並沒有什麼關係。

  先大概看一下內存關係圖,不理解也沒關係,看了後面的代碼應該就能理解了:

   

  大概解釋一下,棧中的Thread ref(引用)堆中的Thread對象,Thread對象有一個屬性threadlocals(ThreadLocalMap類型),這個Map中每一項(Entry)的value是ThreadLocal.set()的值,而Map的key則是ThreadLocal對象。

  下面在介紹源碼的時候,會從兩部分進行介紹,先介紹ThreadLocal的常用api,然後再介紹ThreadLocalMap,因為ThreadLocal的api內部其實都是在操作ThreadLocalMap,所以看源碼時一定要知道他們倆之間的關係

 

2.2ThreadLocal的屬性

  ThreadLocal有3個屬性,主要的功能就是生成ThreadLocal的hash值。

// threadLocalHashCode用來表示當前ThreadLocal對象的hashCode,通過計算獲得
private final int threadLocalHashCode = nextHashCode();

// 一個AtomicInteger類型的屬性,功能就是計數,各種操作都是原子性的,在併發時不會出現問題
private static AtomicInteger nextHashCode = new AtomicInteger();

// hash值的增量,不是隨便指定的,被稱為“黃金分割數”,能讓hash結果均衡分佈
private static final int HASH_INCREMENT = 0x61c88647;

/**
 * 通過計算,為當前ThreadLocal對象生成一個HashCode
 */
private static int nextHashCode() {
    // 獲取當前nextHashCode,然後遞增HASH_INCREMENT
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

  

2.3創建ThreadLocal對象

  ThreadLocal類,只有一個無參構造器,如果需要是指默認值,則可以重寫initialValue方法:

public ThreadLocal() {}

/**
 * 初始值默認為null,要設置初始值,只需要設置為方法返回值即可
 *
 * @return ThreadLocal的初始值
 */
protected T initialValue() {
    return null;
}

  需要注意的是initialValue方法並不會在創建ThreadLocal對象的時候設置初始值,而是延遲執行:當ThreadLocal直接調用get時才會觸發initialValue執行(get之前沒有調用set來設置過值),initialValue方法在後面還會介紹。 

 

2.4ThreadLocal-set操作

  下面這段代碼只給出了ThreadLocal的set代碼:

public void set(T value) {
    // 獲取當前線程
    Thread t = Thread.currentThread();

    // 獲取當前線程的ThreadLocalMap屬性,ThreadLocal有一個threadLocals屬性(ThreadLocalMap類型)
    ThreadLocalMap map = getMap(t);

    if (map != null) {
        // 如果當前線程有關聯的ThreadLocalMap對象,則調用ThreadLocalMap的set方法進行設置
        map.set(this, value);
    } else {
        // 創建一個與當前線程關聯的ThreadLocalMap對象,並設置對應的value
        createMap(t, value);
    }
}

/**
 * 獲取線程關聯的ThreadLocalMap對象
 */
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

/**
 * 創建ThreadLocalMap
 * @param t          key為當前線程
 * @param firstValue value為ThreadLocal.set的值
 */
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

  如果想立即了解ThreadLocalMap的set方法,則可點此跳轉!

 

2.5ThreadLocal-get操作

  前面說過“重寫ThreadLocal的initialValue方法來設置ThreadLocal的默認值,並不是在創建ThreadLocal的時候執行的,而是在直接get的時候執行的”,看了下面的代碼,就知道這句話的具體含義了,感覺設計很巧妙:

public T get() {
    // 獲取當前線程
    Thread t = Thread.currentThread();

    // 獲取當前線程對象的threadLocals屬性
    ThreadLocalMap map = getMap(t);

    // 若當前線程對象的threadLocals屬性不為空(map不為空)
    if (map != null) {
        // 當前ThreadLocal對象作為key,獲取ThreadLocalMap中對應的Entry
        ThreadLocalMap.Entry e = map.getEntry(this);

        // 如果找到對應的Entry,則證明該線程的該ThreadLocal有值,返回值即可
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T) e.value;
            return result;
        }
    }

    // 1.當前線程對象的threadLocals屬性為空(map為空)
    // 2.或者map不為空,但是未在map中查詢到以該ThreadLocal對象為key對應的entry
    // 這兩種情況,都會進行設置初始值,並將初始值返回
    return setInitialValue();
}

/**
 * 設置ThreadLocal初始值
 *
 * @return 初始值
 */
private T setInitialValue() {
    // 調用initialValue方法,該方法可以在創建ThreadLocal的時候重寫
    T value = initialValue();
    Thread t = Thread.currentThread();

    // 獲取當前線程的threadLocals屬性(map)
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // threadLocals屬性值不為空,則進行調用ThreadLocalMap的set方法
        map.set(this, value);
    } else {
        // 沒有關聯的threadLocals,則創建ThreadLocalMap,並在map中新增一個Entry
        createMap(t, value);
    }

    // 返回初始值
    return value;
}

/**
 * 初始值默認為null,要設置初始值,只需要設置為方法返回值即可
 * 創建ThreadLocal設置默認值,可以覆蓋initialValue方法,initialValue方法不是在創建ThreadLocal時執行,而是這個時候執行
 *
 * @return ThreadLocal的初始值
 */
protected T initialValue() {
    return null;
}

     

2.6ThreadLocal-remove操作

  一般是在ThreadLocal對象使用完后,調用ThreadLocal的remove方法,在一定程度上,可以避免內存泄露;

 

/**
 * 刪除當前線程中threadLocals屬性(map)中的Entry(以當前ThreadLocal為key的)
 */
public void remove() {
    // 獲取當前線程的threadLocals屬性(ThreadLocalMap)
    ThreadLocalMap m = getMap(Thread.currentThread());

    if (m != null) {
        // 調用ThreadLocalMap的remove方法,刪除map中以當前ThreadLocal為key的entry
        m.remove(this);
    }
}

 

三.ThreadLocalMap內部類

3.0 線性探測算法解決hash衝突

  在介紹ThreadLocalMap的之前,強烈建議先了解一下線性探測算法,這是一種解決Hash衝突的方案,如果不了解這個算法就去看ThreadLocalMap的源碼就會非常吃力,會感到莫名其妙。

  鏈接在此:利用線性探測法解決hash衝突

 

3.1Entry內部類

  ThreadLocalMap是ThreadLocal的內部類,ThreadLocalMap底層使用數組實現,每一個數組的元素都是Entry類型(在ThreadLocalMap中定義的),源碼如下:

/**
 * ThreadLocalMap中存放的元素類型,繼承了弱引用類
 */
static class Entry extends WeakReference<ThreadLocal<?>> {
    // key對應的value,注意key是ThreadLocal類型
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

  ThreadLocalMap和HashMap類似,比較一下:

  a:底層都是使用數組實現,數組元素類型都是內部定義,Java8中,HashMap的元素是Node類型(或者TreeNode類型),ThreadLocalMap中的元素類型是Entry類型;

  b.都是通過計算得到一個值,將這個值與數組的長度(容量)進行與操作,確定Entry應該放到哪個位置;

  c.都有初始容量、負載因子,超過擴容閾值將會觸發擴容;但是HashMap的初始容量、負載因子是可以更改的,而ThreadLocalMap的初始容量和負載因子不可修改;

  注意Entry繼承自WeakReference類,在實例化Entry時,將接收的key傳給父類構造器(也就是WeakReference的構造器),WeakReference構造器又將key傳給它的父類構造器(Reference):

// 創建Reference對象,接受一個引用
Reference(T referent) {
    this(referent, null);
}

// 設置引用
Reference(T referent, ReferenceQueue<? super T> queue) {
    this.referent = referent;
    this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}

  關於Java的各種引用,可以參考:Java-強引用、軟引用、弱引用、虛引用

 

3.2ThreadLocalMap的常量介紹

// ThreadLocalMap的初始容量
private static final int INITIAL_CAPACITY = 16;

// ThreadLocalMap底層存數據的數組
private Entry[] table;

// ThreadLocalMap中元素的個數
private int size = 0;

// 擴容閾值,當size達到閾值時會觸發擴容(loadFactor=2/3;newCapacity=2*oldCapacity)
private int threshold; // Default to 0

  

3.3創建ThreadLocalMap對象

  創建ThreadLocalMap,是在第一次調用ThreadLocal的set或者get方法時執行,其中第一次未set值,直接調用get時,就會利用ThreadLocal的初始值來創建ThreadLocalMap。

  ThreadLocalMap內部類的源碼如下:

/**
 * 初始化一個ThreadLocalMap對象(第一次調用ThreadLocal的set方法時創建),傳入ThreadLocal對象和對應的value
 */
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    // 創建一個Entry數組,容量為16(默認)
    table = new Entry[INITIAL_CAPACITY];

    // 計算新增的元素,應該放到數組的哪個位置,根據ThreadLocal的hash值與初始容量進行"與"操作
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);

    // 創建一個Entry,設置key和value,注意Entry中沒有key屬性,key屬性是傳給Entry的父類WeakReference
    table[i] = new Entry(firstKey, firstValue);

    // 初始容量為1
    size = 1;

    // 設置擴容閾值
    setThreshold(INITIAL_CAPACITY);
}

/**
 * 設置擴容閾值,接收容量值,負載因子固定為2/3
 */
private void setThreshold(int len) {
    threshold = len * 2 / 3;
}

 

3.4 ThreadLocalMap的set操作

  ThreadLocal的set方法,其實核心就是調用ThreadLocalMap的set方法,set方法的流程比較長

/**
 * 為當前ThreadLocal對象設置value
 */
private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;

    // 計算新元素應該放到哪個位置(這個位置不一定是最終存放的位置,因為可能會出現hash衝突)
    int i = key.threadLocalHashCode & (len - 1);

    // 判斷計算出來的位置是否被佔用,如果被佔用,則需要找出應該存放的位置
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        // 獲取Entry中key,也就是弱引用的對象
        ThreadLocal<?> k = e.get();

        // 判斷key是否相等(判斷弱引用的是否為同一個ThreadLocal對象)如果是,則進行覆蓋
        if (k == key) {
            e.value = value;
            return;
        }

        // k為null,也就是Entry的key已經被回收了,當前的Entry是一個陳舊的元素(stale entry)
        if (k == null) {
            // 用新元素替換掉陳舊元素,同時也會清理其他陳舊元素,防止內存泄露
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    // map中沒有ThreadLocal對應的key,或者說沒有找到陳舊的Entry,則創建一個新的Entry,放入數組中
    tab[i] = new Entry(key, value);
    // ThreadLocalMap的元素數量加1
    int sz = ++size;

    // 先清理map中key為null的Entry元素,該Entry也應該被回收掉,防止內存泄露
    // 如果清理出陳舊的Entry,那麼就判斷是否需要擴容,如果需要的話,則進行rehash
    if (!cleanSomeSlots(i, sz) && sz >= threshold) {
        rehash();
    }
}

  上面最後幾行代碼涉及到清理陳舊Entry和rehash,這兩塊的代碼在下面。

 

3.5清理陳舊Entry和rehash

  陳舊的Entry,是指Entry的key為null,這種情況下,該Entry是不可訪問的,但是卻不會被回收,為了避免出現內存泄漏,所以需要在每次get、set、replace時,進行清理陳舊的Entry,下面只給出一部分代碼:

/**
 * 清理map中key為null的Entry元素,該Entry也應該被回收掉,防止內存泄露
 *
 * @param i 新Entry插入的位置
 * @param n 數組中元素的數量
 * @return 是否有陳舊的entry的清除
 */
private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    do {
        i = nextIndex(i, len);
        Entry e = tab[i];
        if (e != null && e.get() == null) {
            n = len;
            removed = true;
            i = expungeStaleEntry(i);
        }
    } while ((n >>>= 1) != 0);
    return removed;
}

private void rehash() {
    // 清除底層數組中所有陳舊的(stale)的Entry,也就是key為null的Entry
    // 同時每清除一個Entry,就對其後面的Entry重新計算hash,獲取新位置,使用線性探測法,重新確定最終位置
    expungeStaleEntries();

    // 清理完陳舊Entry后,判斷是否需要擴容
    if (size >= threshold - threshold / 4) {
        // 擴容時,容量變為舊容量的2倍,再進行rehash,並使用線性探測發確定Entry的新位置
        resize();
    }
}

  在rehash的時候,涉及到“線性探測法”,是一種用來解決hash衝突的方案,可以查看利用線性探測法解決hash衝突了解詳情。

 

3.6ThreadLocalMap-remove操作

  remove操作,是調用ThreadLocal.remove()方法時,刪除當前線程的ThreadLocalMap中該ThreadLocal為key的Entry。

/**
 * 移除當前線程的threadLocals屬性中key為ThreadLocal的Entry
 *
 * @param key 要移除的Entry的key(ThreadLocal對象)
 */
private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;

    // 計算出該ThreadLocal對應的key應該存放的位置
    int i = key.threadLocalHashCode & (len - 1);

    // 找到指定位置,開始按照線性探測算法進行查找到該Thread的Entry
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {

        // 如果Entry的key相同
        if (e.get() == key) {
            // 調用WeakReference的clear方法,Entry的key是弱引用,指向ThreadLocal,現在將key指向null
            // 則該ThreadLocal對象在會在下一次gc時,被垃圾收集器回收
            e.clear();

            // 將該位置的Entry中的value置為null,於是value引用的對象也會被垃圾收集器回收(不會造成內存泄漏)
            // 同時內部會調整Entry的順序(開放探測算法的特點,刪除元素後會重新調整順序)
            expungeStaleEntry(i);

            return;
        }
    }
}

 

四.總結

  在學習ThreadLocal類源碼的過程還是受益頗多的:

  1.ThreadLocal的使用場景;

  2.initialValue的延遲執行;

  3.HashMap使用鏈表+紅黑樹解決hash衝突,ThreadLocalMap使用線性探測算法(開放尋址)解決hash衝突

  另外,ThreadLocal還有一部分內容,是關於弱引用和內存泄漏的問題,可以參考:分析ThreadLocal的弱引用與內存泄漏問題-Java8。

 

  原文地址:https://www.cnblogs.com/-beyond/p/13093032.html

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?