ThreadLocal常見面試題剖析

ThreadLocalMap 和HashMap區別

HashMap 的數據結構是數組+鏈表

ThreadLocalMap的數據結構僅僅是數組

HashMap 是通過鏈地址法解決hash 衝突的問題

ThreadLocalMap 是通過開放地址法來解決hash 衝突的問題

HashMap 裏面的Entry 內部類的引用都是強引用

ThreadLocalMap裏面的Entry 內部類中的key 是弱引用,value 是強引用

鏈地址法

這種方法的基本思想是將所有哈希地址爲i的元素構成一個稱爲同義詞鏈的單鏈表,並將單鏈表的頭指針存在哈希表的第i個單元中,因而查找、插入和刪除主要在同義詞鏈中進行。列如對於關鍵字集合{12,67,56,16,25,37, 22,29,15,47,48,34},我們用前面同樣的12爲除數,進行除留餘數法:

在這裏插入圖片描述

開放地址法

這種方法的基本思想是一旦發生了衝突,就去尋找下一個空的散列地址(這非常重要,源碼都是根據這個特性,必須理解這裏才能往下走),只要散列表足夠大,空的散列地址總能找到,並將記錄存入。

比如說,我們的關鍵字集合爲{12,33,4,5,15,25},表長爲10。 我們用散列函數f(key) = key mod l0。 當計算前S個數{12,33,4,5}時,都是沒有衝突的散列地址,直接存入(藍色代表爲空的,可以存放數據):

在這裏插入圖片描述

計算key = 15時,發現f(15) = 5,此時就與5所在的位置衝突。於是我們應用上面的公式f(15) = (f(15)+1) mod 10 =6。於是將15存入下標爲6的位置。這其實就是房子被人買了於是買下一間的作法:

在這裏插入圖片描述

鏈地址法和開放地址法的優缺點

開放地址法:

容易產生堆積問題,不適於大規模的數據存儲。

散列函數的設計對沖突會有很大的影響,插入時可能會出現多次衝突的現象。

刪除的元素是多個衝突元素中的一個,需要對後面的元素作處理,實現較複雜。

鏈地址法:

處理衝突簡單,且無堆積現象,平均查找長度短。

鏈表中的結點是動態申請的,適合構造表不能確定長度的情況。

刪除結點的操作易於實現。只要簡單地刪去鏈表上相應的結點即可。

指針需要額外的空間,故當結點規模較小時,開放定址法較爲節省空間。

ThreadLocalMap 採用開放地址法原因

ThreadLocal 中看到一個屬性 HASH_INCREMENT = 0x61c88647 ,0x61c88647 是一個神奇的數字,讓哈希碼能均勻的分佈在2的N次方的數組裏, 即 Entry[] table

通過HASH_INCREMENT 可以看到,ThreadLocal 中使用了斐波那契散列法,來保證哈希表的離散度。而它選用的乘數值即是2^32 * 黃金分割比

什麼是散列?

散列(Hash)也稱爲哈希,就是把任意長度的輸入,通過散列算法,變換成固定長度的輸出,這個輸出值就是散列值。

ThreadLocal 往往存放的數據量不會特別大(而且key 是弱引用又會被垃圾回收,及時讓數據量更小),這個時候開放地址法簡單的結構會顯得更省空間,同時數組的查詢效率也是非常高,加上第一點的保障,衝突概率也低.

解決哈希衝突

ThreadLocal中的hash code非常簡單,就是調用AtomicInteger的getAndAdd方法,參數是個固定值0x61c88647。

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

上面說過ThreadLocalMap的結構非常簡單隻用一個數組存儲,並沒有鏈表結構,當出現Hash衝突時採用線性查找的方式,所謂線性查找,就是根據初始key的hashcode值確定元素在table數組中的位置,如果發現這個位置上已經有其他key值的元素被佔用,則利用固定的算法尋找一定步長的下個位置,依次判斷,直至找到能夠存放的位置。如果產生多次hash衝突,處理起來就沒有HashMap的效率高,爲了避免哈希衝突,使用盡量少的threadlocal變量

內存泄漏問題

在JAVA裏面,存在強引用、弱引用、軟引用、虛引用。這裏主要談一下強引用和弱引用。

強引用,就不必說了,類似於:

A a = new A();

B b = new B();

考慮這樣的情況:

C c = new C(b);

b = null;

考慮下GC的情況。要知道b被置爲null,那麼是否意味着一段時間後GC工作可以回收b所分配的內存空間呢?答案是否定的,因爲即便b被置爲null,但是c仍然持有對b的引用,而且還是強引用,所以GC不會回收b原先所分配的空間!既不能回收利用,又不能使用,這就造成了內存泄露

那麼如何處理呢?

可以c = null;也可以使用弱引用!(WeakReference w = new WeakReference(b);)

ThreadLocal使用到了弱引用,是否意味着不會存在內存泄露呢?

把ThreadLocal置爲null,那麼意味着Heap中的ThreadLocal實例不在有強引用指向,只有弱引用存在,因此GC是可以回收這部分空間的,也就是key是可以回收的。但是value卻存在一條從Current Thread過來的強引用鏈。因此只有當Current Thread銷燬時,value才能得到釋放。

只要這個線程對象被gc回收,就不會出現內存泄露,但在threadLocal設爲null和線程結束這段時間內不會被回收的,就發生了我們認爲的內存泄露。最要命的是線程對象不被回收的情況,比如使用線程池的時候,線程結束是不會銷燬的,再次使用的,就可能出現內存泄露。

那麼如何有效的避免呢?

在ThreadLocalMap中的set/getEntry方法中,會對key爲null(也即是ThreadLocal爲null)進行判斷,如果爲null的話,那麼是會對value置爲null的。我們也可以通過調用ThreadLocal的remove方法進行釋放!也就是每次使用完ThreadLocal,都調用它的remove()方法,清除數據。

ThreadLocal使用

ThreadLocal使用的一般步驟:

1、在多線程的類(如ThreadDemo類)中。創建一個ThreadLocal對象threadXxx,用來保存線程間須要隔離處理的對象xxx。
2、在ThreadDemo類中。創建一個獲取要隔離訪問的數據的方法getXxx(),在方法中推斷,若ThreadLocal對象爲null時候,應該new()一個隔離訪問類型的對象,並強制轉換爲要應用的類型。
3、在ThreadDemo類的run()方法中。通過getXxx()方法獲取要操作的數據。這樣能夠保證每一個線程相應一個數據對象,在不論什麼時刻都操作的是這個對象。

使用示例:

private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {

        new Thread(() -> {
            try {
                for (int i = 0; i < 10; i++) {
                    threadLocal.set(i);
                    System.out.println(Thread.currentThread().getName() + " = " + threadLocal.get());
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            } finally {
                threadLocal.remove();
            }
        }, "threadLocal test 1").start();


        new Thread(() -> {
            try {
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName() + " = " + threadLocal.get());
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            } finally {
                threadLocal.remove();
            }
        }, "threadLocal test 2").start();
    }

輸出

threadLocal test 1 = 0
threadLocal test 2 = null
threadLocal test 2 = null
threadLocal test 1 = 1
threadLocal test 2 = null
threadLocal test 1 = 2
threadLocal test 2 = null
threadLocal test 1 = 3
threadLocal test 2 = null
threadLocal test 1 = 4
threadLocal test 2 = null
threadLocal test 1 = 5
threadLocal test 2 = null
threadLocal test 1 = 6
threadLocal test 2 = null
threadLocal test 1 = 7
threadLocal test 2 = null
threadLocal test 1 = 8
threadLocal test 2 = null
threadLocal test 1 = 9

與Synchonized的對照:

ThreadLocal和Synchonized都用於解決多線程併發訪問。可是ThreadLocal與synchronized有本質的差別。synchronized是利用鎖的機制,使變量或代碼塊在某一時該僅僅能被一個線程訪問。而ThreadLocal爲每個線程都提供了變量的副本,使得每個線程在某一時間訪問到的並非同一個對象,這樣就隔離了多個線程對數據的數據共享。而Synchronized卻正好相反,它用於在多個線程間通信時可以獲得數據共享。

Synchronized用於線程間的數據共享,而ThreadLocal則用於線程間的數據隔離。

線程隔離特性

線程隔離特性,只有在線程內才能獲取到對應的值,線程外不能訪問。

(1)Synchronized是通過線程等待,犧牲時間來解決訪問衝突

(1)ThreadLocal是通過每個線程單獨一份存儲空間,犧牲空間來解決衝突

需要了解ThreadLocal的源碼解析: 點此瞭解

如果大家對java架構相關感興趣,可以關注下面公衆號,會持續更新java基礎面試題, netty, spring boot,spring cloud等系列文章,一系列乾貨隨時送達, 超神之路從此展開, BTAJ不再是夢想!

架構殿堂

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