ThreadLocal原始碼解析

語言: CN / TW / HK

這是ThreadLocal系列的最後一篇文章。

前幾篇文章更多的是在使用層面去介紹ThreadLocal,並沒有深入去理解原理。

其實學任何技術都是這樣一個過程,我們最先接觸到的可能是一個框架的API,然後你可能就會開始使用它;再然後會看看別人是怎麼使用它的,有沒有值得借鑑之處,再然後就是深入原理,看看它的底層是如何實現的,對它做一個深入的瞭解。

下面我們進入正題,先分析一下ThreadLocal幾個重要的方法。

set

set方法其實很短,我們先看一下程式碼:

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

先拿到當前的執行緒,然後通過它去拿到一個Map,如果這個Map存在,就把value塞進去,否則就建立一個新的。

ThreadLocalMap是在ThreadLocal類裡面實現的一個Map,它的Entry是一個弱引用的實現。

static class Entry extends WeakReference<ThreadLocal<?>>
複製程式碼

每個執行緒對應一個自己執行緒私有的ThreadLocalMap,它被Thread物件持有:

// 類Thread裡面定義了ThreadLocalMap的引用
ThreadLocal.ThreadLocalMap threadLocals = null;
複製程式碼

從set方法的程式碼可以看到,最開始執行緒的threadLocals可能是空,這個時候就建立一個新的,賦值給當前執行緒物件:

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
複製程式碼

get

看完上面的分析後,get方法就很好理解了。仍然是先通過getMap方法拿到當前執行緒對應的Map,然後從裡面取出value。如果沒有value,就呼叫ThreadLocal提供的初始化方法,初始化一個值。

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();
}
複製程式碼

初始化

先來看在ThreadLocal定義的的初始化方法,看起來就是一個很簡單的protected方法:

protected T initialValue() {
    return null;
}
複製程式碼

而為了更方便使用者使用,ThreadLocal自己內部有一個ThreadLocal的實現類,它提供了一個函數語言程式設計的方式來讓客戶端更方便地使用:

static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {

    private final Supplier<? extends T> supplier;

    SuppliedThreadLocal(Supplier<? extends T> supplier) {
        this.supplier = Objects.requireNonNull(supplier);
    }

    @Override
    protected T initialValue() {
        return supplier.get();
    }
}
複製程式碼

我們甚至可以依樣畫葫蘆,自己新建一個FunctionedThreadLocal,實現更多的定製化。

remove

remove方法不得不提。首先我們思考一下,既然已經有了弱引用,按理說,如果執行緒沒有持有某個value的時候,會在GC的時候自動清理掉對應的Entry,為什麼會有remove方法存在?

因為我們在開發一個多執行緒的程式時,往往會使用執行緒池。而執行緒池的功能就是執行緒的複用。那如果執行緒池和ThreadLocal在一起就可能會造成一個問題:

  • job A和job B共用了同一個執行緒,
  • job A使用完ThreadLocal,ThreadLocal裡面還有job A儲存的值,而這個時候可能還沒有清理掉,
  • job B複用執行緒進來了,取出來是 job A的值,可能就會造成問題。

所以在有必要的時候,可以在使用完ThreadLocal的時候,顯式呼叫一下remove方法。remove方法的原始碼也比較簡單,就是呼叫對應的entry的clear方法。

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,想要拿到當前執行緒對應的ThreadLocalMap,可能就會用一個Map來存這個關係:

Map<Thread, ThreadLocalMap> threadLocalMaps = new ConcurrentHashMap<>();
複製程式碼

因為可能會有多個執行緒同時呼叫get/set方法,所以還需要對這個Map來做一些措施來保證執行緒安全,比如使用ConcurrentHashMap,甚至複雜的原子操作可能還需要上鎖。這樣其實對效能是不利的。

而JDK巧妙地把這個引用直接放到了Thread物件裡面,使得多個執行緒不需要同時操作同一個物件。所以我們在設計程式碼的時候,也要有這種思維的轉變。並不是說我想實現一個工具類,就一定要把所有的程式碼都寫在這個工具類裡面。要充分考慮怎樣設計更合理,效能更高。

弱引用

使用弱引用可以讓GC及時回收掉程式中不需要使用的物件。這個剛好適用於ThreadLocal的場景。因為很多時候執行緒執行完後,就銷燬了。如果讓我們顯示去呼叫一個方法,就會變得非常麻煩。而且一旦忘記回收,還有可能造撐滿記憶體。

所以這點ThreadLocal做得很好,利用了弱引用的特性,與Java的設計哲學一致:你只管用,回收的事情我幫你做了!

Tips: 這裡需要注意上面提到的與執行緒池一起使用可能存在的問題哦。

簡單設計

ThreadLocal中自己定義了一個很簡單的可以自動擴容的Map。它處理衝突的方式與HashMap不一樣,HashMap是陣列 + 連結串列/紅黑樹的方式來處理雜湊衝突,而ThreadLocal實現得更簡單,使用的是開放地址法,如果發生了衝突,就尋找下一個有空的位置。

開放地址法雖然效率不一定高,但勝在實現起來很簡單,用在這裡綽綽有餘。我們在設計資料結構和演算法的時候,甚至是在設計程式的時候,也有遵循夠用、簡單就行的原則,不用太過度設計。也就是我們常說的KISS原則:Keep it stupid and simple。

函數語言程式設計

使用函數語言程式設計可以讓客戶端更簡單地實現定製化。比如ThreadLocal中的初始化方法,如果沒有函數語言程式設計,我們首先得新建一個ThreadLocal的繼承類,然後複寫它的initialValue方法,用起來特別不方便。

我們在設計自己的工具類的時候,想要實現一定程度的靈活性和定製化,就可以考慮利用函數語言程式設計的便利。

巧用this

this其實我們平時用的還算比較多,最多的地方應該是POJO類了。但ThreadLocal進行了一個騷操作。

我們看ThreadLocalMap的原始碼可以發現,它的key型別就是ThreadLocal。我們在呼叫get/set方法的時候,就會使用this。

為什麼要這麼設計?你會發現Thread和ThreadLocal其實是多對多的關係。一個Thread可能會用到多個ThreadLocal,而一個ThreadLocal又同時給多個Thread用。那麼問題來了,我們的入口是ThreadLocal物件,那如何能夠快速地拿到當前Thread,當前ThreadLocal的value?

這就是this的關鍵之處了,我先拿到當前Thread,然後通過Thread裡面儲存的引用,拿到ThreadLocalMap,這個Map裡面儲存了此執行緒對應的所有ThreadLocal的物件,key就是這個物件本身,所以用this作為key,可以快速找到當前ThreadLocal對應的value。

假如我們要實現一個多對多的場景,比如一個學生有多個老師,一個老師有多個學生。通過學生類作為入口進去,如何能夠快速獲取一個學生指定老師的分數?我們寫個程式來模擬一下:

// 教師類
public class Teacher {
    // 每個教師儲存了自己每個學生的分數
    Map<Student, Integer> scores = new HashMap<>();

    public Map<Student, Integer> getScores() {
        return scores;
    }
}

// 學生類
public class Student {

    public int get(Teacher teacher) {
        Map<Student, Integer> scores = teacher.getScores();
        return scores.get(this);
    }

    public void set(Teacher teacher, int score) {
        teacher.getScores().put(this, score);
        Map<Student, Integer> scores = teacher.getScores();
    }
}
複製程式碼

當然了,這種場景其實並不多見。但ThreadLocal有它的特殊性,首先當前Thread物件是可以通過全域性直接獲取到的,然後我們的操作入口一般是ThreadLocal,使用而不是Thread。

試想一下,其實如果JDK開放許可權,通過Thread也能拿到最後的ThreadLocal,無非就是麻煩一些:大概長這樣:

Thread thread = Thread.currentThread();
// 如果jdk提供下面這個方法
ThreadLocalMap threadLocalMap = thread.getThreadLocals();
threadLocalMap.set(threadLocal, value); // set
Object value = threadLocalMap.get(threadLocal); // get
複製程式碼

但是這樣一看,顯然不如現在這樣設計得優雅:

threadLocal.set(value); //set
Object value = threadLocal.get(); // get
複製程式碼

所以這就是程式設計的哲學,大佬設計出來的東西,就是好用!JDK把ThreadLocal的引用放到了Thread裡面,讓它能夠避免多個執行緒爭用資源,再巧妙利用了this關鍵字,讓你可以很簡單地使用它。然後還考慮到了記憶體回收的問題,用弱引用幫你解決。

看完ThreadLocal原始碼不禁驚呼:只怪自己沒文化,一句臥槽走天下!

關於作者

我是Yasin,一個有顏有料又有趣的程式設計師。

微信公眾號:編了個程

個人網站:http://yasinshaw.com

關注我的公眾號,和我一起成長~

公眾號
公眾號