揭祕ThreadLocal

大閒人柴毛毛發表於2019-03-04

ThreadLocal是開發中最常用的技術之一,也是面試重要的考點。本文將由淺入深,介紹ThreadLocal的使用方式、實現原理、記憶體洩漏問題以及使用場景。

揭祕ThreadLocal

ThreadLocal作用

在併發程式設計中時常有這樣一種需求:每條執行緒都需要存取一個同名變數,但每條執行緒中該變數的值均不相同。

如果是你,該如何實現上述功能?常規的思路如下:
使用一個執行緒共享的Map<Thread,Object>,Map中的key為執行緒物件,value即為需要儲存的值。那麼,我們只需要通過map.get(Thread.currentThread())即可獲取本執行緒中該變數的值。

這種方式確實可以實現我們的需求,但它有何缺點呢?——答案就是:需要同步,效率低!

由於這個map物件需要被所有執行緒共享,因此需要加鎖來保證執行緒安全性。當然我們可以使用java.util.concurrent.*包下的ConcurrentHashMap提高併發效率,但這種方法只能降低鎖的粒度,不能從根本上避免同步鎖。而JDK提供的ThreadLocal就能很好地解決這一問題。下面來看看ThreadLocal是如何高效地實現這一需求的。

如何使用ThreadLocal

在介紹ThreadLocal原理之前,首先簡單介紹一下它的使用方法。

public class Main{
    private ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
    
    public void start() {
        for (int i=0; i<10; i++) {
            new Thread(new Runnable(){
                @override
                public void run(){
                    threadLocal.set(i);
                    threadLocal.get();
                    threadLocal.remove();
                }
            }).start();
        }
    }
}
複製程式碼
  • 首先我們需要建立一個執行緒共享的ThreadLocal物件,該物件用於儲存Integer型別的值;
  • 然後在每條執行緒中可以通過如下方法操作ThreadLocal:
    • set(obj):向當前執行緒中儲存資料
    • get():獲取當前執行緒中的資料
    • remove():刪除當前執行緒中的資料

ThreadLocal的使用方法非常簡單,關鍵在於它背後的實現原理。回到上面的問題:ThreadLocal究竟是如何避免同步鎖,從而保證讀寫的高效?

ThreadLocal實現原理

ThreadLocal的內部結構如下圖所示:

title

ThreadLocal並不維護ThreadLocalMap,並不是一個儲存資料的容器,它只是相當於一個工具包,提供了操作該容器的方法,如get、set、remove等。而ThreadLocal內部類ThreadLocalMap才是儲存資料的容器,並且該容器由Thread維護。

每一個Thread物件均含有一個ThreadLocalMap型別的成員變數threadLocals,它儲存本執行緒中所有ThreadLocal物件及其對應的值。

ThreadLocalMap由一個個Entry物件構成,Entry的程式碼如下:

static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}
複製程式碼

Entry繼承自WeakReference<ThreadLocal<?>>,一個EntryThreadLocal物件和Object構成。由此可見,Entry的key是ThreadLocal物件,並且是一個弱引用。當沒指向key的強引用後,該key就會被垃圾收集器回收。

那麼,ThreadLocal是如何工作的呢?下面來看set和get方法。

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
複製程式碼

當執行set方法時,ThreadLocal首先會獲取當前執行緒物件,然後獲取當前執行緒的ThreadLocalMap物件。再以當前ThreadLocal物件為key,將值儲存進ThreadLocalMap物件中。

get方法執行過程類似。ThreadLocal首先會獲取當前執行緒物件,然後獲取當前執行緒的ThreadLocalMap物件。再以當前ThreadLocal物件為key,獲取對應的value。

由於每一條執行緒均含有各自私有的ThreadLocalMap容器,這些容器相互獨立互不影響,因此不會存線上程安全性問題,從而也無需使用同步機制來保證多條執行緒訪問容器的互斥性。

為何要使用弱引用?

對弱引用不了解的同學可以參考筆者的另一篇文章:http://blog.csdn.net/u010425776/article/details/50760053。

Java設計之初的一大宗旨就是——弱化指標。
Java設計者希望通過合理的設計簡化程式設計,讓程式設計師無需處理複雜的指標操作。然而指標是客觀存在的,在目前的Java開發中也不可避免涉及到“指標操作”。如:

Object a = new Object();
複製程式碼

上述程式碼建立了一個強引用a,只要強引用存在,垃圾收集器是不會回收該物件的。如果該物件非常龐大,那麼為了節約記憶體空間,在該物件使用完成後,我們需要手動拆除該強引用,如下面程式碼所示:

a = null;
複製程式碼

此時,指向該物件的強引用消除了,垃圾收集器便可以回收該物件。但在這個過程中,仍然需要程式設計師處理指標。為了弱化指標這一概念,弱引用便出現了,如下程式碼建立了一個Person型別的弱引用:

WeakReference<Person> wr = new WeakReference<Person>(new Person()); 
複製程式碼

此時程式設計師不用再關注指標,只要沒有強引用指向Person物件,垃圾收集器每次執行都會自動將該物件釋放。

那麼,ThreadLocalMap中的key使用弱引用的原因也是如此。當一條執行緒中的ThreadLocal物件使用完畢,沒有強引用指向它的時候,垃圾收集器就會自動回收這個Key,從而達到節約記憶體的目的。

那麼,問題又來了——這會導致記憶體洩漏問題!

ThreadLocal的記憶體洩漏問題

在ThreadLocalMap中,只有key是弱引用,value仍然是一個強引用。當某一條執行緒中的ThreadLocal使用完畢,沒有強引用指向它的時候,這個key指向的物件就會被垃圾收集器回收,從而這個key就變成了null;然而,此時value和value指向的物件之間仍然是強引用關係,只要這種關係不解除,value指向的物件永遠不會被垃圾收集器回收,從而導致記憶體洩漏!

不過不用擔心,ThreadLocal提供了這個問題的解決方案。

每次操作set、get、remove操作時,ThreadLocal都會將key為null的Entry刪除,從而避免記憶體洩漏。

那麼問題又來了,如果一個執行緒執行週期較長,而且將一個大物件放入LocalThreadMap後便不再呼叫set、get、remove方法,此時該仍然可能會導致記憶體洩漏。

這個問題確實存在,沒辦法通過ThreadLocal解決,而是需要程式設計師在完成ThreadLocal的使用後要養成手動呼叫remove的習慣,從而避免記憶體洩漏。

ThreadLocal的使用場景

Web系統Session的儲存就是ThreadLocal一個典型的應用場景。

Web容器採用執行緒隔離的多執行緒模型,也就是每一個請求都會對應一條執行緒,執行緒之間相互隔離,沒有共享資料。這樣能夠簡化程式設計模型,程式設計師可以用單執行緒的思維開發這種多執行緒應用。

當請求到來時,可以將當前Session資訊儲存在ThreadLocal中,在請求處理過程中可以隨時使用Session資訊,每個請求之間的Session資訊互不影響。當請求處理完成後通過remove方法將當前Session資訊清除即可。

揭祕ThreadLocal

相關文章