再有人問你什麼是ThreadLocal,就把這篇文章甩給他

ThreadLocal是JDK1.2提供的一個工具,它爲解決多線程程序的併發問題提供了一種新的思路。使用這個工具類可以很簡潔地編寫出優美的多線程程序,解決共享參數的頻繁傳遞與線程安全等問題。如果開發者掌握了ThreadLocal用法與原理,那麼使用起來將得心應手,那麼請跟隨本文的節奏,撥開迷霧,探究本質吧!

本文將帶領讀者深入理解ThreadLocal,爲了保證閱讀質量,我們可以先一起來簡單理解一下什麼是ThreadLocal?

如果你從字面上來理解,很容易將ThreadLocal理解爲『本地線程』,那麼你就大錯特錯了。

首先,ThreadLocal不是線程,更不是本地線程,而是Thread的局部變量,也許把它命名爲ThreadLocalVariable更容易讓人理解一些。

它是每個線程獨享的本地變量,每個線程都有自己的ThreadLocal,它們是線程隔離的。接下來,我們通過一個生活案例來開始理解ThreadLocal。

一、問題場景引入

假如語文老師有一本書,但是班上有30名學生,老師將這本書送給學生們去閱讀,30名學生都想閱讀這本書。

爲保證每個學生都能閱讀到書籍,那麼基本可以有兩種方案,一是按照某種排序(例如姓名首字母排序),讓每個學生依次閱讀。

二是讓30名學生同時爭搶,誰搶到誰就去閱讀,讀完放回原處,剩下的29名學生再次爭搶。

顯然第一種方案,基本表現爲串行閱讀,時間成本較大,第二種方案爲多個學生爭搶,容易發生安全問題(學生髮生衝突或者書籍在爭搶過程中被毀壞)。

爲了解決這兩個問題,那麼有沒有更加好的方案呢?當然有,老師可以將書籍複印30本,每個學生都發一本,這樣既大大提高了閱讀效率,節約了閱讀時間,還能保證每個學生都能有自己的書籍,這樣就不會發生爭搶,避免了安全問題。

其實閱讀到這裏,讀者應該有點感覺了,因爲生動的例子能幫助讀者迅速理解關鍵點,在本例中,書籍作爲共享變量,那麼很多學生去爭搶,學生可以理解爲線程,同時去爭搶(併發執行)有很大可能會引起安全問題(線程安全問題),這往往是老師不願意看到的後果。

我們在結合Java Demo來演示類似的案例。假如我們有一個需求,那就是在多線程環境下,去格式化時間爲指定格式yyyy-MM-dd HH:mm:ss,假設一開始只有兩個線程需要這麼做,代碼如下:

public class ThreadLocalUsage01 {

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                String date = new ThreadLocalUsage01().date(10);
                System.out.println(date);
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                String date = new ThreadLocalUsage01().date(1000);
                System.out.println(date);
            }
        }).start();
    }

    private String date(int seconds) {
        // 參數的單位是毫秒,從1970.1.1 00:00:00 GMT計時
        Date date = new Date(1000 * seconds);
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return dateFormat.format(date);
    }

}

在線程少的情況下是沒有問題的,我們在每個線程裏調用date方法,也就是在每個線程裏都執行了創建SimpleDateFormat對象,每個對象在各自的線程裏面執行格式化時間

但是我們是否會思考到,假如有1000個線程需要格式化時間,那麼需要調用1000次date方法,也就是需要創建1000個作用一樣的SimpleDateFormat對象,這樣是不是太浪費內存了?也給GC帶來壓力?

於是我們聯想到,1000個線程來共享一個SimpleDateFormat對象,這樣SimpleDateFormat對象只需要創建一次即可,代碼如下:

public class ThreadLocalUsage02 {

    public static ExecutorService THREAD_POOL = Executors.newFixedThreadPool(10);
    static SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            THREAD_POOL.submit(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalUsage02().date(finalI);
                    System.out.println(date);
                }
            });
        }
        // 關閉線程池,此種關閉方式不再接受新的任務提交,等待現有隊列中的任務全部執行完畢之後關閉
        THREAD_POOL.shutdown();
    }

    private String date(int seconds) {
        // 參數的單位是毫秒,從1970.1.1 00:00:00 GMT計時
        Date date = new Date(1000 * seconds);
        return DATE_FORMAT.format(date);
    }

}

上述代碼我們使用到了固定線程數的線程池來執行時間格式化任務,我們來執行一下,看看結果:

再有人問你什麼是ThreadLocal,就把這篇文章甩給他

 

截取了部分執行結果,發現執行結果中有很多重複的時間格式化內容,這是爲什麼呢?

這是因爲SimpleDateFormat是一個線程不安全的類,其實例對象在多線程環境下作爲共享數據,會發生線程不安全問題。

說到這裏,很多讀者肯定會說,我們可以嘗試一下使用鎖機制,我們將date方法內的格式化代碼使用synchronized關鍵字概括起來,保證同一時刻只能有一個線程來訪問SimpleDateFormat的format方法,代碼如下所示:

private String date(int seconds) {
    // 參數的單位是毫秒,從1970.1.1 00:00:00 GMT計時
    Date date = new Date(1000 * seconds);
    String format;
    synchronized (ThreadLocalUsage02.class) {
        format = DATE_FORMAT.format(date);
    }
    return format;
}

有了鎖的保證,那麼這次執行後就不會再出現重複的時間格式化結果了,這也就保證了線程安全。

使用鎖機制確實可以解決問題,但是多數情況下,我們不大願意使用鎖,因爲鎖的使用會帶來性能的下降(比如10個線程重複排隊執行DATE_FORMAT.format(date)代碼),那麼有沒有其他方法來解決這個問題呢?答案當然是有,那就是本文的主角——ThreadLocal。

二、理解ThreadLocal的用法

這裏還是使用固定線程數的線程池來執行格式化時間的任務。

我們的基本思想是,使用ThreadLocal來給線程池中每個線程賦予一個SimpleDateFormat對象副本,該副本只能被當前線程使用,是當前線程獨享的成員變量,當SimpleDateFormat對象不存在多線程共同訪問的時候,也就不會產生線程安全問題了,基本原理圖如下所示:

再有人問你什麼是ThreadLocal,就把這篇文章甩給他

 

我們使用ThreadLocal的目的是爲了避免創建1000個SimpleDateFormat對象,且在不使用鎖的情況下保證線程安全,那麼如何實現只創建一個SimpleDateFormat對象且能被多個線程同時使用呢?改造後的案例代碼如下所示:

public class ThreadLocalUsage04 {

    public static ExecutorService THREAD_POOL = Executors.newFixedThreadPool(10);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            THREAD_POOL.submit(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalUsage04().date(finalI);
                    System.out.println(date);
                }
            });
        }
        THREAD_POOL.shutdown();
    }

    private String date(int seconds) {
        // 參數的單位是毫秒,從1970.1.1 00:00:00 GMT計時
        Date date = new Date(1000 * seconds);
        SimpleDateFormat simpleDateFormat = ThreadSafeDateFormatter.dateFormatThreadLocal.get();
        return simpleDateFormat.format(date);
    }

}

class ThreadSafeDateFormatter {

    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };

}

上面的代碼使用到了ThreadLocal,將SimpleDateFormat對象用ThreadLocal包裝了一層,使得多個線程內部都有一個SimpleDateFormat對象副本,每個線程使用自己的SimpleDateFormat,這樣就不會產生線程安全問題了。

那麼以上介紹的是ThreadLocal的第一大場景的使用,也就是利用到了ThreadLocal的initialValue()方法,使得每個線程內都具備了一個SimpleDateFormat副本。

接下來我們一起來看看ThreadLocal的第二大使用場景,在使用之前,我們先把兩個場景總結如下:

  • 場景1:每個線程需要一個獨享的對象,通常是工具類,比如典型的SimpleDateFormat和Random等。
  • 場景2:每個線程內需要保存線程內的全局變量,這樣線程在執行多個方法的時候,可以在多個方法中獲取這個線程內的全局變量,避免了過度參數傳遞的問題。

那麼如何理解第二個問題呢?我們還是使用一個Demo來理解:假設有一個學生類,類成員變量包括姓名,性別,成績,我們需要定義三個方法來分別獲取學生的姓名、性別和成績,那麼我們傳統的做法是:

public class ThreadLocalUsage05 {

    public static void main(String[] args) {
        Student student = init();
        new NameService().getName(student);
        new SexService().getSex(student);
        new ScoreService().getScore(student);
    }

    private static Student init() {
        Student student = new Student();
        student.name = "Lemon";
        student.sex = "female";
        student.score = "100";
        return student;
    }

}

class Student {

    /**
     * 姓名、性別、成績
     */
    String name;
    String sex;
    String score;

}

class NameService {

    public void getName(Student student) {
        System.out.println(student.name);
    }

}

class SexService {

    public void getSex(Student student) {
        System.out.println(student.sex);
    }

}

class ScoreService {

    public void getScore(Student student) {
        System.out.println(student.score);
    }

}

從上面的代碼中可以看出,每個類的方法都需要傳遞學生的信息纔可以獲取到正確的信息,這樣做能達到目的

但是每個方法都需要學生信息作爲入參,這樣未免有點繁瑣,且在實際使用中通常在每個方法裏面還需要對每個學生信息進行判空,這樣的代碼顯得十分冗餘,不利於維護。

也許有人會說,我們可以將學生信息存入到一個共享的Map中,需要學生信息的時候直接去Map中取,如下圖所示:

再有人問你什麼是ThreadLocal,就把這篇文章甩給他

 

其實這也是一種思路,但是在併發環境下,如果要使用Map,那麼就需要使用同步的Map,比如ConcurrentHashMap或者
Collections.SynchronizedMap(),前者底層用的是CAS和鎖機制,後者直接使用的是synchronized,性能也不盡人意。

其實,我們可以將學生信息存入到ThreadLocal中,在同一個線程中,那麼直接從ThreadLocal中獲取需要的信息即可!案例代碼如下所示:

public class ThreadLocalUsage05 {

    public static void main(String[] args) {
        init();
        new NameService().getName();
        new SexService().getSex();
        new ScoreService().getScore();
    }

    private static void init() {
        Student student = new Student();
        student.name = "Lemon";
        student.sex = "female";
        student.score = "100";
        ThreadLocalProcessor.studentThreadLocal.set(student);
    }

}

class ThreadLocalProcessor {

    public static ThreadLocal<Student> studentThreadLocal = new ThreadLocal<>();

}

class Student {

    /**
     * 姓名、性別、成績
     */
    String name;
    String sex;
    String score;

}

class NameService {

    public void getName() {
        System.out.println(ThreadLocalProcessor.studentThreadLocal.get().name);
    }

}

class SexService {

    public void getSex() {
        System.out.println(ThreadLocalProcessor.studentThreadLocal.get().sex);
    }

}

class ScoreService {

    public void getScore() {
        System.out.println(ThreadLocalProcessor.studentThreadLocal.get().score);
    }

}

上面的代碼就省去了頻繁的傳遞參數,也沒有使用到鎖機制,同樣滿足了需求,思想其實和上面將學生信息存儲到Map中的思想差不多,只不過這裏不是將學生信息存儲到Map中,而是存儲到了ThreadLocal中,原理圖如下所示:

再有人問你什麼是ThreadLocal,就把這篇文章甩給他

 

那麼總結這兩種用法,通常分別用在不同的場景裏:

  • 場景一:通常多線程之間需要擁有同一個對象的副本,那麼通常就採用initialValue()方法進行初始化,直接將需要擁有的對象存儲到ThreadLocal中。
  • 場景二:如果多個線程中存儲不同的信息,爲了方便在其他方法裏面獲取到信息,那麼這種場景適合使用set()方法。例如,在攔截器生成的用戶信息,用ThreadLocal.set直接放入到ThreadLocal中去,以便在後續的方法中取出來使用。

三、理解ThreadLocal原理

3.1 理解ThreadLocalMap數據結構

通過本文的第二小節的介紹,相信大家基本上可以掌握ThreadLocal的基本使用方法,接下來,我們來一起閱讀ThreadLocal源碼,從源碼角度來真正理解ThreadLocal。

在閱讀源碼之前,我們一起來看看一張圖片:

再有人問你什麼是ThreadLocal,就把這篇文章甩給他

 

上圖中基本描述出了Thread、ThreadLocalMap以及ThreadLocal三者之間的包含關係。Thread類對象中維護了ThreadLocalMap成員變量,而ThreadLocalMap維護了以ThreadLocal爲key,需要存儲的數據爲value的Entry數組。這是它們三者之間的基本包含關係,我們需要進一步到源碼中尋找蹤跡。

查看Thread類,內部維護了兩個變量,threadLocals和inheritableThreadLocals,它們的默認值是null,它們的類型是
ThreadLocal.ThreadLocalMap,也就是ThreadLocal類的一個靜態內部類ThreadLocalMap。

在靜態內部類ThreadLocalMap維護一個數據結構類型爲Entry的數組,節點類型如下代碼所示:

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

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

從源碼中我們可以看到,Entry結構實際上是繼承了一個ThreadLocal類型的弱引用並將其作爲key,value爲Object類型。這裏使用弱引用是否會產生問題,我們這裏暫時不討論,在文章結束的時候一起討論一下,暫且可以理解key就是ThreadLocal對象。對於ThreadLocalMap,我們一起來了解一下其內部的變量:

// 默認的數組初始化容量
private static final int INITIAL_CAPACITY = 16;
// Entry數組,大小必須爲2的冪
private Entry[] table;
// 數組內部元素個數
private int size = 0;
// 數組擴容閾值,默認爲0,創建了ThreadLocalMap對象後會被重新設置
private int threshold;

這幾個變量和HashMap中的變量十分類似,功能也類似。

ThreadLocalMap的構造方法如下所示:

/**
 * Construct a new map initially containing (firstKey, firstValue).
 * ThreadLocalMaps are constructed lazily, so we only create
 * one when we have at least one entry to put in it.
 */
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    // 初始化Entry數組,大小 16
    table = new Entry[INITIAL_CAPACITY];
    // 用第一個鍵的哈希值對初始大小取模得到索引,和HashMap的位運算代替取模原理一樣
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    // 將Entry對象存入數組指定位置
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    // 初始化擴容閾值,第一次設置爲10
    setThreshold(INITIAL_CAPACITY);
}

從構造方法的註釋中可以瞭解到,該構造方法是懶加載的,只有當我們創建一個Entry對象並需要放入到Entry數組的時候纔會去初始化Entry數組。

分析到這裏,也許我們都有一個疑問,平常使用ThreadLocal功能都是藉助ThreadLocal對象來操作的,比如set、get、remove等,使用上都屏蔽了ThreadLocalMap的API,那麼到底是如何做到的呢?我們一起繼續看下面的代碼。

3.2 理解ThreadLocal類set方法

試想我們一個請求對應一個線程,我們可能需要在請求到達攔截器之後,可能需要校驗當前請求的用戶信息,那麼校驗通過的用戶信息通常都放入到ThreadLocalMap中,以方便在後續的方法中直接從ThreadLocalMap中獲取

但是我們並沒有直接操作ThreadLocalMap來存取數據,而是通過一個靜態的ThreadLocal變量來操作,我們從上面的圖可以看出,ThreadLocalMap中存儲的鍵其實就是ThreadLocal的弱引用所關聯的對象,那麼鍵是如何操作類似HashMap的值的呢?我們一起來分析一下set方法:

public void set(T value) {
    // 首先獲取調用此方法的線程
    Thread t = Thread.currentThread();
    // 將線程傳遞到getMap方法中來獲取ThreadLocalMap,其實就是獲取到當前線程的成員變量threadLocals所指向的ThreadLocalMap對象
    ThreadLocalMap map = getMap(t);
    // 判斷Map是否爲空
    if (map != null)
        // 如果Map爲不空,說明當前線程內部已經有ThreadLocalMap對象了,那麼直接將本ThreadLocal對象作爲鍵,存入的value作爲值存儲到ThreadLocalMap中
        map.set(this, value);
    else
        // 創建一個ThreadLocalMap對象並將值存入到該對象中,並賦值給當前線程的threadLocals成員變量
        createMap(t, value);
}

// 獲取到當前線程的成員變量threadLocals所指向的ThreadLocalMap對象
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

// 創建一個ThreadLocalMap對象並將值存入到該對象中,並賦值給當前線程的threadLocals成員變量
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

上面的set方法是ThreadLocal的set方法,就是爲了將指定的值存入到指定線程的threadLocals成員變量所指向的ThreadLocalMap對象中,那麼具體是如何存取的,其實調用的還是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;
    // 計算當前ThreadLocal對象作爲鍵在Entry數組中的下標索引
    int i = key.threadLocalHashCode & (len-1);

    // 線性遍歷,首先獲取到指定下標的Entry對象,如果不爲空,則進入到for循環體內,
    // 判斷當前的ThreadLocal對象是否是同一個對象,如果是,那麼直接進行值替換,並結束方法,
    // 如果不是,再判斷當前Entry的key是否失效,如果失效,則直接將失效的key和值進行替換。
    // 這兩點都不滿足的話,那麼就調用nextIndex方法進行搜尋下一個合適的位置,進行同樣的操作,
    // 直到找到某個位置,內部數據爲空,也就是Entry爲null,那麼就直接將鍵值對設置到這個位置上。
    // 最後判斷是否達到了擴容的條件,如果達到了,那麼就進行擴容。
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) {
            e.value = value;
            return;
        }

        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

這裏的代碼核心的地方就是for循環這一塊,代碼上面加了詳細的註釋,這裏在複述一遍:

線性遍歷,首先獲取到指定下標的Entry對象,如果不爲空,則進入到for循環體內,判斷當前的ThreadLocal對象是否是同一個對象

如果是,那麼直接進行值替換,並結束方法。如果不是,再判斷當前Entry的key是否失效,如果失效,則直接將失效的key和值進行替換。

這兩點都不滿足的話,那麼就調用nextIndex方法進行搜尋下一個合適的位置,進行同樣的操作,直到找到某個位置,內部數據爲空,也就是Entry爲null,那麼就直接將鍵值對設置到這個位置上。最後判斷是否達到了擴容的條件,如果達到了,那麼就進行擴容。

這裏有兩點需要注意:一是nextIndex方法,二是key失效,這裏先解釋第一個注意點,第二個注意點涉及到弱引用JVM GC問題,文章最後做出解釋。

nextIndex方法的具體代碼如下所示:

private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

其實就是尋找下一個合適位置,找到最後一個後還不合適的話,那麼從數組頭部重新開始找,且一定可以找到,因爲存在擴容閾值,數組必定有冗餘的位置存放當前鍵值對所對應的Entry對象。其實nextIndex方法就是大名鼎鼎的『開放尋址法』的應用。

這一點和HashMap不一樣,HashMap存儲HashEntry對象發生哈希衝突的時候採用的是鏈表方式進行存儲,而這裏是去尋找下一個合適的位置,思想就是『開放尋址法』。

3.3 理解ThreadLocal類get方法

在實際的開發中,我們往往需要在代碼中調用ThreadLocal對象的get方法來獲取存儲在ThreadLocalMap中的數據,具體的源碼如下所示:

public T get() {
    // 獲取當前線程的ThreadLocalMap對象
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 如果map不爲空,那麼嘗試獲取Entry數組中以當前ThreadLocal對象爲鍵的Entry對象
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            // 如果找到,那麼直接返回value
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // 如果Map爲空或者在Entry數組中沒有找到以當前ThreadLocal對象爲鍵的Entry對象,
    // 那麼就在這裏進行值初始化,值初始化的過程是將null作爲值,當前ThreadLocal對象作爲鍵,
    // 存入到當前線程的ThreadLocalMap對象中
    return setInitialValue();
}

// 值初始化過程
private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

值初始化過程是這樣的一個過程,如果調用新的ThreadLocal對象的get方法,那麼在當前線程的成員變量threadLocals中必定不存在key爲當前ThreadLocal對象的Entry對象,那麼這裏值初始話就將此ThreadLocal對象作爲key,null作爲值存儲到ThreadLocalMap的Entry數組中。

3.4 理解ThreadLocal的remove方法

使用ThreadLocal這個工具的時候,一般提倡使用完後及時清理存儲在ThreadLocalMap中的值,防止內存泄露。這裏一起來看下ThreadLocal的remove方法。

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

// 具體的刪除指定的值,也是通過遍歷尋找,找到就刪除,找不到就算了
private void remove(ThreadLocal<?> key) {
    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)]) {
        if (e.get() == key) {
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}

看了這麼多ThreadLocal的源碼實現,其實原理還是很簡單的,基本上可以說是一看就懂,理解ThreadLocal原理,其實就是需要理清Thread、ThreadLocal、ThreadLocalMap三者之間的關係

這裏加以總結:線程類Thread內部持有ThreadLocalMap的成員變量,而ThreadLocalMap是ThreadLocal的內部類,ThreadLocal操作了ThreadLocalMap對象內部的數據,對外暴露的都是ThreadLocal的方法API,隱藏了ThreadLocalMap的具體實現,理清了這一點,ThreadLocal就很容易理解了。

四、理解ThreadLocalMap內存泄露問題

這裏所說的ThreadLocal的內存泄露問題,其實都是從ThreadLocalMap中的一段代碼說起的,這段代碼就是Entry的構造方法:

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

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

這裏簡單介紹一下Java內的四大引用:

  • 強引用:Java中默認的引用類型,一個對象如果具有強引用那麼只要這種引用還存在就不會被回收。比如String str = new String("Hello ThreadLocal");,其中str就是一個強引用,當然,一旦強引用出了其作用域,那麼強引用隨着方法彈出線程棧,那麼它所指向的對象將在合適的時機被JVM垃圾收集器回收。
  • 軟引用:如果一個對象具有軟引用,在JVM發生內存溢出之前(即內存充足夠使用),是不會GC這個對象的;只有到JVM內存不足的時候纔會調用垃圾回收期回收掉這個對象。軟引用和一個引用隊列聯合使用,如果軟引用所引用的對象被回收之後,該引用就會加入到與之關聯的引用隊列中。
  • 弱引用:這裏討論ThreadLocalMap中的Entry類的重點,如果一個對象只具有弱引用,那麼這個對象就會被垃圾回收器回收掉(被弱引用所引用的對象只能生存到下一次GC之前,當發生GC時候,無論當前內存是否足夠,弱引用所引用的對象都會被回收掉)。弱引用也是和一個引用隊列聯合使用,如果弱引用的對象被垃圾回收期回收掉,JVM會將這個引用加入到與之關聯的引用隊列中。若引用的對象可以通過弱引用的get方法得到,當引用的對象被回收掉之後,再調用get方法就會返回null。
  • 虛引用:虛引用是所有引用中最弱的一種引用,其存在就是爲了將關聯虛引用的對象在被GC掉之後收到一個通知。

我們從ThreadLocal的內部靜態類Entry的代碼設計可知,ThreadLocal的引用k通過構造方法傳遞給了Entry類的父類WeakReference的構造方法,從這個層面來說,可以理解ThreadLocalMap中的鍵是ThreadLocal的所引用。

當一個線程調用ThreadLocal的set方法設置變量的時候,當前線程的ThreadLocalMap就會存放一個記錄,這個記錄的鍵爲ThreadLocal的弱引用,value就是通過set設置的值,這個value值被強引用。

如果當前線程一直存在且沒有調用該ThreadLocal的remove方法,如果這個時候別的地方還有對ThreadLocal的引用,那麼當前線程中的ThreadLocalMap中會存在對ThreadLocal變量的引用和value對象的引用,是不會釋放的,就會造成內存泄漏。

考慮這個ThreadLocal變量沒有其他強依賴,如果當前線程還存在,由於線程的ThreadLocalMap裏面的key是弱引用,所以當前線程的ThreadLocalMap裏面的ThreadLocal變量的弱引用在垃圾回收的時候就被回收,但是對應的value還是存在的這就可能造成內存泄漏(因爲這個時候ThreadLocalMap會存在key爲null但是value不爲null的entry項)。

總結:ThreadLocalMap中的Entry的key使用的是ThreadLocal對象的弱引用,在沒有其他地方對ThreadLocal依賴,ThreadLocalMap中的ThreadLocal對象就會被回收掉,但是對應的值不會被回收,這個時候Map中就可能存在key爲null但是值不爲null的項,所以在使用ThreadLocal的時候要養成及時remove的習慣。

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