無內鬼,來點ThreadLocal!

ThreadLocal介紹

官方介紹

從java官方文檔中的描述: ThreadLocal類用來提供線程內部的局部變量。這種變量在多線程環境下訪問(通過get和set方法訪問)時能保證各個線程的變量相對獨立於其他線程內的變量。ThreadLocal實例通常來說都是private static類型的,用於關聯線程和線程上下文。
我們可以得知ThreadLocal的作用是:提供線程內的局部變量,不同的線程之間不會相互干擾,這種變量在線程的生命週期內起作用,減少同一一個線程內多個函數或組件之間一些公共變量傳遞的複雜度。

總結:
1. 線程併發: 在多線程的場景下
2. 傳遞數據: 我們可以通過ThreadLocal在同一線程,不同組件中傳遞公共變量
3. 線程隔離: 每個線程的變量都是獨立的,不會互相影響

基本使用

常用方法

image-20200523161328998

使用案例

不適用ThreadLocal的情況:

/**
 * 需求:  線程隔離
 * 在多線程併發的場景下,每個線程中的變量都是互相獨立的
 * 線程A: 設置(變量1)    獲取(變量1)
 * 線程B: 設置(變量2)    獲取(變量2)
 */
public class MyDemo {
    private String content;

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public static void main(String[] args) {
        MyDemo demo = new MyDemo();
        for (int i = 0; i < 5; i++) {
            /**
             * 每個線程: 存一個變量,過一會兒取出這個變量
             */
            new Thread(() -> {
                demo.setContent(Thread.currentThread().getName() + "的數據");
                System.out.println("--------------");
                System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent());
            },"線程"+i).start();
        }
    }
}

可以看到,出現了線程不安全的情況(顯而易見)

image-20200523162556357

我們使用ThreadLocal試試:

image-20200523163118349

不管運行幾次都不會出現問題:

image-20200523163203437

ThreadLocal與synchronized關鍵字

synchronized同步方式

new Thread(() -> {
    synchronized(MyDemo.class){
        demo.setContent(Thread.currentThread().getName() + "的數據");
        System.out.println("--------------");
        System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent());
    }   
},"線程"+i).start();

這種方式當然也可以實現線程安全,但是對比TrheadLocal來說效率會降低. 即,把並行的線程改爲了串行,吞吐量降低,於是效率降低

ThreadLocal與synchronized的區別

雖然ThreadLocal模式與synchronized關鍵字都用於處理多線程併發訪問變量的問題,不過兩者處理問題的角度和思路不同。

image-20200523163900368

總結

在剛剛的案例中,雖然使用ThreadLocal和synchronized都能解決問題,但是使用Threadlocal更爲合適,因爲這樣可以使程序擁有更高的併發性

運用場景_事務案例

ThreadLocal具體能用在什麼地方?

轉賬案例

場景構建

這裏我們先構建一個簡單的轉賬場景:有一個數據表account ,裏面有兩個用戶Jack和Rose ,用戶Jack給用戶Rose轉賬。
案例的實現主要用mysq|數據庫, JDBC和C3P0框架。

image-20200523164414687

  1. 項目結構

image-20200523164501891

  1. dao

image-20200523164549704

  1. config.xml

    image-20200523164645278

  2. jdbc工具類

    image-20200523164716037

  3. service

image-20200523164747424

  1. web

image-20200523164842617

引入事務

案例中的轉賬涉及兩個DML操作: 一個轉出, 一個轉入。這些操作是需要具備原子性的,不可分割。不然就有可能出現數據修改異常情況。

模擬一個異常

image-20200523165043373

所以這裏就需要操作事務,來保證轉出和轉入操作具備原子性,要麼同時成功,要麼同時失敗。

  1. jdbc中關於事務操作的api

    image-20200523165235984

  2. 開啓事務的注意點

  • 爲了保證所有的操作在一個事務中,案例中使用的連接必須是同一個: service層開啓事務的connection需要跟dao層訪問數據庫的connection保持一致
  • 線程併發情況下,每個線程只能操作各自的connection

image-20200523165543499

image-20200523165552657

image-20200523165644116

image-20200523165902561

常規解決方案

常規解決方案的實現

傳參 加鎖

image-20200523170312557

常規解決方案的弊端

  • 增加了代碼的耦合度 (傳參)
  • 降低了程序的性能 (加鎖)

使用ThreadLocal來解決上述事務問題

image-20200523170620303

代碼修改

image-20200523170859169

獲取連接修改

原本: 直接從連接池中獲取連接

現在:

1. 直接獲取當前線程綁定的連接對象
2. 如果連接對象是空的
  	1. 再去連接池中獲取連接
  	2. 將此連接對象與當前線程綁定

image-20200523171248352

使用完成後將connection與threadlocal解綁

爲什麼要解綁是爲了防止內存泄漏,往下看

image-20200523171512934

ThreadLocal方案的好處

從上述的案例中我們可以看到,在一些特定場景下 , ThreadLocal方案有兩個突出的優勢:

  1. 傳遞數據: 保存每個線程綁定的數據,在需要的地方可以直接獲取,避免參數直接傳遞帶來的代碼耦合問題
  2. 線程隔離: 各線程之間的數據相互隔離卻又具備併發性,避免同步方式帶來的性能損失

ThreadLocal的內部結構

通過以上的學習,我們對ThreadLocal的作用有了-定的認識。現在我們一起來看一下Threadlocal的內部結構,探究它能夠實現線程數據隔離的原理。

常見的誤解(早期的設計)

通常,如果我們不去看源代碼的話,我猜ThreadlLocal是這樣子設計的:每個ThreadLocal類都創建一個Map ,然後用線程的ID threadID 作爲Mapkey , 要存儲的局部變量作爲Mapvalue , 這樣就能達到各個線程的局部變量隔離的效果。這是最簡單的設計方法, JDK最早期的ThreadLocal就是這樣設計的。

image-20200523172119511

現在的設計

但是, JDK後面優化了設計方案,在JDK8中ThreadLocal的設計是:每個Thread維護一個ThreadlocalMap , 這個MapkeyThreadLocal實例本身,value 纔是真正要存儲的值object。具體的過程是這樣的:

  1. 每個Thread線程內部都有一個Map (ThreadLocalMap)
  2. Map裏面存儲ThreadLocal對象( key )和線程的變量副本( value )
  3. Thread內部的Map是由ThreadLocal維護的,由ThreadLocal負責向map獲取和設置線程的變量值。
  4. 對於不同的線程,每次獲取副本值時,別的線程並不能獲取到當前線程的副本值,形成了副本的隔離,互不干擾。

image-20200523172554819

這樣設計的好處

兩個好處:

  1. 每個Map存儲的Entry數量變少 (並不是所有的Thread都會使用ThreadLocal)
  2. 當Thread銷燬的時候,ThreadLocalMap也會隨之銷燬,減少內存的使用

ThreadLocal的核心源碼方法

基於ThreadLocal的內部結構,我們繼續分析它的核心方法源碼,更深入的瞭解其操作原理。除了構造方法之外. ThreadLocal對外暴露的方法有以下4個:

image-20200523173134413

以下是這4個方法的詳細源碼分析

set方法

源碼片段:

/**
     * 設置當前線程對應的ThreadLocal的值
     * @param value 將要保存在當前線程對應的ThreadLocal的值
     */
    public void set(T value) {
        //獲取當前線程對象
        Thread t = Thread.currentThread();
        //獲取此線程對象中維護的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        //判斷map是否存在
        if (map != null) {
            //存在調用map.set()設置這個實體entry
            map.set(this, value);
        } else {
            //1. 當前線程Thread不存在ThreadlocalMap對象
			//2. 則調用createMap進行ThreadLocalMap對象的初始化
			//3. 並將t(當前線程)和value(t對應的值)作爲第一個entry存放至ThreadLocalMap中
            createMap(t, value);
        }
    }

/**
     * 獲取當前線程Thread對應維護的ThreadLocalMap
     *
     * @param  t 	the current thread 
     * @return 		the map 
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

/**
     * 創建當前線程Thread對應維護的ThreadLocalMap
     * @param t				 the current thread
     * @param firstValue	 存放到map中第一個entry的值
     */
    void createMap(Thread t, T firstValue) {
        //這裏的this是調用此方法的threadlocal
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

代碼執行流程

​ A. 首先獲取當前線程,並根據當前線程獲取一個Map
​ B. 如果獲取的Map不爲空,則將參數設置到Map中(當前ThreadLocal的引用作爲key )
​ C. 如果Map爲空,則給該線程創建Map ,並設置初始值

get方法

源碼片段

/**
     * 返回當前線程中保存ThreadLoca1的值
	 * 言如果當前線程沒有此ThreadLoca1變量,
	 * 則它會通過調用{@link #initialvalue} 方法進行初始化值
	 * @return 返回當前線程對應此ThreadLocal的值

     */
    public T get() {
        //獲取當前線程對象
        Thread t = Thread.currentThread();
        //獲取此線程對象中維護的Thr eadL ocalMap對象
        ThreadLocalMap map = getMap(t);
        //如果此map存在
        if (map != null) {
            //以當前的ThreadLocal爲key,調用getEntry獲取對應的存儲實體e
            ThreadLocalMap.Entry e = map.getEntry(this);
            //對e進行判空
            if (e != null) {
                @SuppressWarnings("unchecked")
                //獲取存儲實體e對應的value值
				//即爲我們想要的當前線程對應此Threadlocal的值
                T result = (T)e.value;
                return result; 
            }
        }
        /*
			初始化:有兩種情況有執行當前代碼
			第一種情況: map不存在,表示此線程沒有維護的ThreadLocalMap對象
			第二種情況: map存在,但是沒有與當前ThreadLocal關聯的entry
		*/
        return setInitialValue();
    }

/**
     * 初始化
     * @return		 the initial value 初始化後的值
     */
    private T setInitialValue() {
        //調用initialvalue獲取初始化的值
		//此方法可以被子類重寫,如果不重寫默認返回nu11
        T value = initialValue();
        //獲取當前線程對象
        Thread t = Thread.currentThread();
        //獲取此線程對象中維護的Thr eadLoca lMap對象
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //判斷map是否存在,存在則調用map.set設置此實體entry
            map.set(this, value);
        } else {
            //1. 當前線程Thread不存在ThreadlocalMap對象
			//2. 則調用createMap進行ThreadLocalMap對象的初始化
			//3. 並將t(當前線程)和value(t對應的值)作爲第一個entry存放至ThreadLocalMap中
            createMap(t, value);
        }
        if (this instanceof TerminatingThreadLocal) {
            TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
        }
        //
        return value;
    }

代碼執行流程

​ A. 首先獲取當前線程,根據當前線程獲取一個Map
​ B. 如果獲取的Map不爲空,則在Map中以ThreadLocal的引用作爲key來在Map中獲取對應的Entry e ,否則轉到D
​ C. 如果e不爲null ,則返回e.value ,否則轉到D
​ D. Map爲空或者e爲空,則通過initialValue函數獲取初始值value ,然後用ThreadLocal的引用和value作爲firstKey和firstValue創建一個新的Map

總結: 先獲取當前線程的ThreadLocalMap變量,如果存在則返回值.不存在則創建並返回初始值

remove方法

源碼

/**
   	 * 刪除當前線程中保存的ThreadLoca1對應的實體entry
     */
     public void remove() {
         //獲取當前線程對象中維護的ThreadLocalMap對象
         ThreadLocalMap m = getMap(Thread.currentThread());
         //如果map存在
         if (m != null) {
             //存在則調用map.remove
			 //以當前ThreadLoca1爲key刪除對應的實體entry
             m.remove(this);
         }
     }

代碼執行流程

​ A. 首先獲取當前線程,並根據當前線程獲取一個Map
​ B. 如果獲取的Map不爲空,則移除當前ThreadLocal對象對應的entry

initialValue方法

源碼

/**
 * 返回當前線程對應的ThreadLocal的初始值
 
 * 此方法的第一次調用發生在 ,當線程通過get方法訪問此線程的ThreadLocal值時
 * 除非線程先調用了set方法,在這種情況下,initialvalue纔不會被這個線程調用。
 * 通常情況下,每個線程最多調用一次這個方法。

 * method, in which case the {@code initialValue} method will not
 * be invoked for the thread.  Normally, this method is invoked at
 * most once per thread, but it may be invoked again in case of
 * subsequent invocations of {@link #remove} followed by {@link #get}.
 *
 * <p>這個方法僅僅簡單的返回nu11 {@code nu11};
 * 如果程序員想ThreadLocal線程局部變量有一個除nu11以外的初始值,
 * 必須通過子類繼承{@code ThreadLocal} 的方式去重寫此方法
 * 通常,可以通過匿名內部類的方式實現
 *
 * @return 當前ThreadLocal的初始值
 */
protected T initialValue() {
    return null;
}

此方法的作用是返回該線程局部變量的初始值。
(1) 這個方法是一個延遲調用方法 , 以上面的代碼我們得知,在set方法還未調用而先調用了get方法時才執行,並且僅執行1次。
(2) 這個方法缺省實現直接返回一個nu11。
(3) 如果想要一個除null之外的初始值,可以重寫此方法。( 備註:該訪法是一個protected的方法,顯然是爲了讓子類覆蓋而設計的)

ThreadLocalMap源碼分析

在分析ThreadLocal方法的時候,我們瞭解到ThreadLocal的操作實際上是圍繞ThreadLocalMap展開的。
ThreadLocalMap的源碼相對比較複雜我們從以下三個方面進行討論。

基本結構

ThreadLocalMap是ThreadLocal的內部類,沒有實現Map接口,用獨立的方式實現了Map的功能,其內部的Entry也是獨立實現。

image-20200523182326022

成員變量

		/**
         * 初始容量,必須是2的整數次冪
         */
        private static final int INITIAL_CAPACITY = 16;

        /**
         * 存放數據的table, Entry類在下面分析
         * 同樣,數組長度必須是2的整數次冪
         */
        private Entry[] table;

        /**
         * 數組裏面entrys的個數,可以用於判斷table當前使用量是否超過閾值
         */
        private int size = 0;

        /**
         * 進行擴容的閾值,表使用量大於它的時候進行擴容
         */
        private int threshold; // Default to 0

跟HashMap類似, INITIAL CAPACITY代表這個Map的初始容量;

table是一個Entry 類型的數組.用於存儲數據;

size代表表中的存儲數目;

threshold 代表需要擴容時對應size的閾值。

存儲結構-Entry

/**
* Entry繼承WeakReference ,並且用ThreadLocal作爲key. 
* 如果key爲null(entry.get() == null) , 意味着key不再被引用,
* 因此這時候entry也可以從tab1e中清除。
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

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

在ThreadLocalMap中,也是用Entry來保存K-V結構數據的。不過Entry中的key只能是ThreadLocal對象,這點在構造方法中已經限定死了。
另外, Entry繼承WeakReference ,也就是key ( ThreadLocal )是弱引用,其目的是將ThreadLocal對象的生命週期和線程生命週期解綁。

弱引用和內存泄漏

有些程序員在使用ThreadLocal的過程中會發現有內存泄漏的情況發生,就猜測這個內存泄漏跟Entry中使用了弱引用的key有關係。這個理解其實是不對的。
我們先來回顧這個問題中涉及的幾個名詞概念,再來分析問題。

內存泄漏

  • Memory overflow: 內存溢出,沒有足夠的內存提供申請者使用。
  • Memory leak: 內存泄漏是指程序中己動態分配的堆內存由於某種原因程序未釋放或無法釋放,造成系統內存的浪費,導致程序運行速度減慢甚至系統崩潰等嚴重後果。內存泄漏的堆積終將導致內存溢出。

弱引用

Java中的引用有4種類型:強、軟、弱、虛。當前這個問題主要涉及到強引用和弱引用:
強引用( “Strong” Reference) , 就是我們最常見的普通對象引用,只要還有強引用指向一個對象,就能表明對象還“活着”,垃圾回收器就不會回收這種對象。
弱引用( WeakReference) ,垃圾回收器一旦發現了只具有弱引用的對象,不管當前內存空間足夠與否,都會回收它的內存。

如果key使用強引用

假設ThreadLocalMap中的key使用了強引用,那麼會出現內存泄漏嗎?

此時ThreadLocal的內存圖(實線表示強引用)如下:

image-20200523184058818

  1. 假設在業務代碼中使用完ThreadLocal , threadLocal Ref被回收了。

  2. 但是因爲threadLocalMap的Entry強引用了threadLocal ,造成threadLocal無法被回收。

  3. 在沒有手動刪除這個Entry以及CurrentThread依然運行的前提下.始終有強引用鏈threadRef-> currentThread-> threadLocalMap->entry ,Entry就不會被回收(Entry中包括了ThreadLocal實例和value),導致Entry內存泄漏。

    也就是說, ThreadLocalMap中的key使用了強引用,是無法完全避免內存泄漏的。

假設ThreadLocalMap中的key使用了弱引用,那麼會出現內存泄漏嗎?

image-20200523184649472

  1. 同樣假設在業務代碼中使用完ThreadLocal , threadLocal Ref被回收了。

  2. 於ThreadLocalMap只持有ThreadLocal的弱引用.沒有任何強引用指向threadlocal實例,所以threadlocal就可以順利被gc回收.此時Entry中的key=null.

  3. 但是在沒有手動刪除這個Entry以及CurrentThread依然運行的前提下,也存在有強引用鏈threadRef->currentThread->threadLocalMap-> entry ->value ,value不會被回收,而這塊value永遠不會被訪問到了.導致value內存泄漏。

    也就是說, ThreadLocalMap中的key使用了弱引用,也有可能內存泄漏。

出現內存泄漏的真實原因?

比較以上兩種情況,我們就會發現,內存泄漏的發生跟ThreadLocalMap中的key是否使用弱引用是沒有關係的。
那麼內存泄漏的的真正原因是什麼呢?

細心的同學會發現,在以上兩種內存泄漏的情況中.都有兩個前提:

  1. 沒有手動刪除這個Entry
  2. CurrentThread依然運行

第一點很好理解,只要在使用完ThreadLocal ,調其remove方法刪除對應的Entry ,就能避免內存泄漏。
第二點稍微複雜一點,由於ThreadLocalMap是Thread的一個屬性,被當前線程所引用,所以它的生命週期跟Thread一樣長。那麼在使用完ThreadLocal的使用,如果當前Thread也隨之執行結束, ThreadLocalMap自然也會被gc回收,從根源上避免了內存泄漏。

綜上, ThreadLocal內存泄漏的根源是:由於ThreadLocalMap的生命週期跟Thread一樣長.如果沒有手動刪除對應key就會導致內存泄漏。

那爲什麼要用弱引用

根據剛纔的分析,我們知道了:無論使用ThreadLocalMap中的key使用哪種類型引用都無法完全避免內存泄漏,跟使用弱引用沒有關係。

要避免內存泄漏有兩種方式:

  1. 使用完ThreadLocal ,調用其remove方法刪除對應的Entry
  2. 使用完ThreadLocal ,當前Thread也隨之運行結束

相對第一種方式,第二種方式顯然更不好控制,特別是使用線程池的時候,線程結束是不會銷燬的。

也就是說,只要記得在使用完ThreadLocal及時的調用remove ,無論key是強弓|用還是弱引用都不會有問題。那麼爲什麼key要用弱引用呢?

事實上,在ThreadLocalMap中的set/getEntry方法中,會對key爲null (也即是ThreadLocal爲null )進行判斷,如果爲null的話,那麼是會對value置爲null的。

這就意味着使用完Thread ocal , CurrentThread依然運行的前提下,就算忘記調用remove方法,弱引用比強引可以多一層保障: 弱引用的ThreadLocal會被回收,對應

的value在下一次ThreadLocalMap調用set,get,remove中的任一方法的時候會被清除,從而避免內存泄漏。

hash衝突的解決

hash衝突的解決是Map中的一個重要內容。我們以hash衝突的解決爲線索,來研究一下ThreadLocalMap的核心源碼。

首先從ThreadLocal的set()方法入手

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //調用了ThreadLocalMap的set方法
            map.set(this, value);
        } else {
            createMap(t, value);
        }
    }


    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    void createMap(Thread t, T firstValue) {
        //調用了ThreadLocalMap的構造方法
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

這個方法我們剛纔分析過,其作用是設置當前線程綁定的局部變量:
A. 首先獲取當前線程,並根據當前線程獲取一個Map

​ B. 如果獲取的Map不爲空,則將參數設置到Map中(當前ThreadLocal的引用作爲key)

(這裏調用了ThreadLocalMap的set方法)

​ C. 如果Map爲空,則給該線程創建Map,並設置初始值

(這裏調用了ThreadLocalMap的構造方法)

這段代碼有兩個地方分別涉及到ThreadLocalMap的兩個方法我們接着分析這兩個方法。

構造方法 ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue)

/**
* firstKey: 本ThreadLocal實例
* firstValue: 要保存的線程本地變量
* one when we have at least one entry to put in it.
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    //初始化table
    table = new Entry[INITIAL_CAPACITY];
    //計算索引(重點)
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    //設置值
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    //設置閾值
    setThreshold(INITIAL_CAPACITY);
}

構造函數首先創建一個長度爲16的Entry數組,然後計算出firstKey對應的索引, 然後存儲到table中,並設置size和threshold

重點分析:int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);

關於firstKey.threadLocalHashCode

private final int threadLocalHashCode = nextHashCode();

private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
	}

//原子Integer類,線程安全的方式進行加減,適合高併發情況下使用
private static AtomicInteger nextHashCode =
        new AtomicInteger();

//特殊的hash值
private static final int HASH_INCREMENT = 0x61c88647; //十六進制

這裏定義了一個AtomicInteger類型,每次獲取當前值並加上HASH_ JINCREMENTHASH INCREMENT =0x61c88647,這個值跟斐波那契數列(黃金分割數)有關,其主要目的就是爲了讓哈希碼能均勻的分佈在2的n次方的數組裏也就是Entry[] table中,這樣做可以儘量避免hash衝突。

關於& (INITIAL_CAPACITY - 1)

就和hashmap中計算下標的方式一樣,保證每次都是2的n次方容量大小就可以通過與運算得到下標,理解爲優化版的%運算就可

ThreadLocalMap中的set()方法

private void set(ThreadLocal<?> key, Object value) {

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

    Entry[] tab = table;
    int len = tab.length;
    //計算索引
    int i = key.threadLocalHashCode & (len-1);

    /**
     * 使用線性探測法查找元素(重點)
     */
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();
		//ThreadLocal對應的key存在,直接覆蓋之前的值
        if (k == key) {
            e.value = value;
            return;
        }
		//key爲null時,但是值不爲null,說明之前的ThreadLocal對象已經被回收了
        //當前數組中的Entry是一個陳舊的(stable)的元素
        if (k == null) {
            //用新元素替換陳舊的元素,這個方法進行了不少的垃圾清理動作,防止內存泄漏
            replaceStaleEntry(key, value, i);
            return;
        }
    }
	//ThreadL oca1對應的key不存在並且沒有找到陳舊的元素,則在空元素的位置創建一個新的Entry.
    tab[i] = new Entry(key, value);
    int sz = ++size;
    /**
     * cleanSomeSlots用於清除那些e.get()==nu11的元表,
	 * 這種數據key關聯的對象已經被回收,所以這個Entry(table[index])可以被置nu11.
	 * 如果沒有清除任何entry,並且當前使用量達到了負載因子所定義(長度的2/3),那麼進行
	 * rehash (執行一次全表的掃描清理工作)
     */
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

/**
 * 獲取環形數組的下一個索引
 */
private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

代碼執行流程

  1. 首先還是根據key計算出索引i,然後查找i位置上的Entry
  2. 若是Entry已經存在並且key等於傳入的key,那麼這時候直接給這個Entry賦新的value值
  3. 若是Entry存在,但是key爲null,則調用replaceStaleEntry來更換這個key爲空的Entry
  4. 不斷循環檢測,直到遇到爲null的地方,這時候要是還沒在循環過程中return,那麼就在這個null的位置新建一
    個Entry ,並且插入,同時size增加1。

最後調用cleanSomeSlots,清理key爲null的Entry,最後返回是否清理了Entry,接下來再判斷sz是否>=threshold達到了rehash的條件,達到的話就會調用rehash函數執行一次全表的掃描清理。

重點分析 ThreadLocalMap使用線性探測法來解決哈希衝突的。

該方法一次探測下一個地址,直到有空的地址後插入,若整個空間都找不到空餘的地址,則產生溢出。

舉個例子,假設當前table長度爲16 ,也就是說如果計算出來key的hash值爲14 ,如果table[14]上已經有值,並且其key與當前key不一致,那麼就發生了hash衝突,這個時候將14加1得到15 ,取table[15]進行判斷,這個時候如果還是衝突會回到0 ,取table[0],以此類推,直到可以插入。

(是不是很熟悉?其實就是操作系統課上講的解決hash衝突的一種方法)

按照上面的描述,可以把Entry[] table看成一個環形數組。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章