ThreadLocal

ThreadLocal趣談 —— 楊過和他的四個冤家

Google+ Pinterest LinkedIn Tumblr

作為一篇趣談,這篇文章不打算太過深入的分析ThreadLocal內部機制。

只希望通過一種有趣的方式,讓大家瞭解ThreadLocal的兩大用途:

  • 實現執行緒安全;
  • 儲存執行緒上下文資訊

原始碼的事,後面再討論。

這篇趣談的主人公是楊過。我們將聊聊楊過是如何利用ThreadLocal打敗四大高手的。

一個一個上

一日醒來,楊過發現小龍女離家出走,於是外出尋找,不料碰上了金輪法王、李莫愁、裘千尺、公孫止四個冤家。

「哼,四個打我一個,算什麼英雄好漢,有本事的,一個一個上!」

按照楊過的說法,這個場景,寫成Java程式碼,大概就是這樣:

public class ThreadSafeSDFUsingSync {
    private SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd HHmm");

    public synchronized String formatIt(Date date) {
        return sdf.format(date);
    }
}

楊過就是這個執行緒不安全的SimpleDateFormat,一旦被多個執行緒同時操作(被多個高手同時進攻),就會出現異常(被打死),所以他選擇了加鎖,也就是synchronize,這樣就不會有執行緒安全問題了。

為什麼SimpleDateFormat是執行緒不安全的?這主要是因為,它內部使用了一個全域性的Calendar變數,來儲存date資訊。詳細解釋可以參考文末列出的文章。

瞬間分身術

「呵呵,可笑,誰說我們是英雄好漢了?」,李莫愁說道。

說罷,四大高手一齊使出看家本領,欲置楊過於死地。

楊過先前在百花谷學到了周伯通的左右互搏術,結合小時候看到的《火影忍者》裡的影分身術,領悟出了自己的一套瞬間分身法。

只要有人向他進攻,他就能瞬間分身,去抵擋住對方的攻勢。

寫成程式碼,就是把上面的SimpleDateFormat,換成天然執行緒安全的區域性變數,這樣就無需使用synchronize加鎖了:

public class ThreadSafeSDFUsingLocalVariable {
    public String formatIt(Date date) {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd HHmm");
        return sdf.format(date);
    }
}

分身大法

就這樣雙方僵持了兩個小時,楊過發現這樣打下去自己體力只會越來越差,因為每次四大高手中的任意一方發起進攻,自己都要花費內功產生一個分身(每次執行緒一呼叫,都需要去new一個物件)。

「能不能讓分身不用完就消失呢?」,楊過一邊應付攻勢,一邊思考著。

突然,他領悟出了一套可以持久分身的絕招,一下子分身出四個楊過,分別對付四個敵人。

寫成程式碼,那就是用一個Map,key是執行緒ID,value是SimpleDateFormat,要用的時候,根據當前執行緒ID獲取對應的SimpleDateFormat即可:

public class ThreadSafeSDFUsingMap {
    private Map<Long, SimpleDateFormat> sdfMap = new ConcurrentHashMap();

    public String formatIt(Date date) {
        Thread currentThread = Thread.currentThread();
        long threadId = currentThread.getId();

        SimpleDateFormat sdf = sdfMap.get(threadId);
        if (null == sdf) {
            sdf = new SimpleDateFormat("yyyyMMdd HHmm");
            sdfMap.put(threadId, sdf);
        }

        return sdf.format(date);
    }
}

當然,JDK早已經知道到我們會有這種需求,他們提供了 ThreadLocal來幫助我們實現把變數和執行緒進行繫結的功能 ,上面的程式碼,可以用ThreadLocal進行改寫:

public class ThreadSafeSDFUsingThreadLocal {
    private static final ThreadLocal<SimpleDateFormat> formatter = new ThreadLocal();

    static {
        formatter.set(new SimpleDateFormat("yyyyMMdd HHmm"));
    }

    public String formatIt(Date date) {
        SimpleDateFormat simpleDateFormat = formatter.get();
        return simpleDateFormat.format(date);
    }
}

使用ThreadLocal的靜態方法withInitial,可以讓上面這段程式碼更簡潔。

簡單看看ThreadLocal

ThreadLocal的實現思路,正如我們上面ThreadSafeSDFUsingMap所演示的,通過Map這樣的key-value結構來將變數繫結到執行緒。

只不過這個Map不是常見的HashMap結構,這個Map也不是儲存在ThreadLocal,並且Map的key也不是執行緒ID。

我們只需看一下ThreadLocal的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);
    }
    
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

set方法會先獲取到當前執行緒,然後獲取當前執行緒物件中,一個ThreadLocalMap型別的map,然後把自己,也就是threadLocal作為key,把要儲存的值作為value,塞入這個map。

這張圖很好的描述了Thread、ThreadLocal、ThreadLocalMap三者的關係:

ThreadLocal趣談 —— 楊過和他的四個冤家

為什麼JDK要把資料放在Thread物件?而不直接放到ThreadLocal?為什麼key值不是執行緒ID,而是ThreadLocal?思考題。後面再討論。

ThreadLocal的另一個用途

上面講的都是ThreadLocal在實現執行緒安全上的用途。

ThreadLocal還有另一個用途,那就是儲存執行緒上下文資訊。

這一點在很多框架乃至JDK類載入中都有用到。

比如Spring的事務管理,方法A裡頭呼叫了方法B,方法B如果失敗了,需要執行connection.rollback()來回滾事務。

那麼方法B怎麼知道connection是哪個?最簡單的就是方法A在呼叫方法B時,把connection物件傳進去,虛擬碼如下:

@Transactional
methodA(){
  methodB(connection);
}

顯然,這樣很挫,需要修改方法的定義。

不過你現在知道ThreadLocal了,只需把connection塞入threadLocal,methodB和methodA在一個執行緒中執行,那麼自然,methodB可以獲取到和methodA相同的connection。

具體可以參考Spring的TransactionSynchronizationManager類,至於Spring的事務管理原理,後面再討論。

總結

這篇文章帶大家初步看了看ThreadLocal,瞭解了ThreadLocal的兩大用途。

當然ThreadLocal肯定還有更多的用途,只要我們弄懂了它的原理,就知道如何靈活使用。

關於ThreadLocal的原始碼,比如:

  • 它和HashMap在key-value功能的實現上有何不同
  • 它為什麼使用了WeakReference
  • 使用了WeakReference就不會有記憶體溢位的風險了嗎?

咱們下回繼續討論。

參考

  • ThreadLocal怎麼用: Baeldung java-threadlocal
  • 什麼時候可以使用ThreadLocal: when-and-how-should-i-use-a-threadlocal-variable
  • SimpleDateFormat為什麼執行緒不安全
    • why-is-javas-simpledateformat-not-thread-safe
    • java-dateformat-is-not-threadsafe-what-does-this-leads-to
  • 《Spring揭祕》第五部分 事務管理
  • Previous

    管理精力,而非時間

Write A Comment