ThreadLocal 線程的私有內存


話說在《操作系統原理》這門課里面,我們學到了很多概念:進程、線程、鎖、PV操作、讀寫者問題等等,大家還記得么?(估計有些概念早已忘記了吧,哈哈哈~) 其中關於進程、線程和鎖的東西是我們平時工作中用到最多的:服務器接收到用戶請求,需要用一個進程或者一個線程去處理,然后操作內存、文件或者數據庫的時候,可能需要對他們進行加鎖操作。這一切都看起來順理成章,正常的不能再正常,對吧。

 

不過作為一個有追求的程序員,我們有些時候會不滿足於此,會想方設法的去追求卓越和ZhuangBility,這些也是老王所追求的^_^ 。於是,我們開始對線程、鎖開始了漫漫的優化之路。其中有一種情況是非常值得優化的:假定我們現在有一個web服務,我們的程序一般都會為每一個到來的請求創建一個上下文環境(比如:請求的URL、瀏覽器型號、訪問的token等等),然后這個上下文貫穿了這一次請求,任何處理操作都可以拿到這個上下文。那我們實現的時候,就可以這樣來做:

 

 

我們設計一個ContextManager,用來管理所有的context。他可能是一個單例(singleton)模式,也可能提供靜態(static)方法,反正不管如何,全局都可以訪問,我們這里為了說明簡單,就假定是用static方法。然后請求的處理線程在接收到任務的時候,就調用這個ContextManager的getContext()方法,提供給這個線程一個context,然后線程對這個context做初始化等等操作,供后續使用。

 

我們來考慮一下ContextManager.getContext()的實現:為了保證多個線程訪問時候的安全,我們在絕大多數情況下,會對這個函數加鎖(或者部分代碼塊加鎖),對吧。這正如我們之前所說,是再正常不過的操作了。不過,如果訪問量很大的話,這個加鎖就會導致性能下降(特別是線程很多的時候,這個情況就越發的明顯),很多線程會在鎖上進行等待(linux下可以用strace,java可以用jstack等工具來查看),其造成的結果就是處理速度變慢,並發能力下降。那有沒有好的解決方案呢?答案是肯定的(不然老王怎么往下講^_^)

 

第一次改進:全局鎖 -> 線程內部數據

大家想,對於context而言,只要有用戶請求進來,他的這一段青春就已經獻給了這次請求,換句話說,實際上就是把自己獻給了這個線程,你中有我,我中有你。那既然這樣,我們是不是就可以把context的歸屬權讓給thread,而不是ContextManager呢?這樣,context不光是一段青春給了thread,更是把一生都獻給了thread~

 

 

偽代碼大體上就寫成了這樣:

 1 class Thread
 2 
 3 {
 4 
 5         private Context context;
 6 
 7  
 8 
 9         public Context getContext()
10 
11         {  
12 
13                 return context;
14 
15         }  
16 
17 }

如果一個請求過來了,我們的工作線程直接就初始化自己的context環境,然后供后續邏輯處理使用,再也不用去求別人加個鎖分配個context了。是不是一個很棒的方案呢?

 

只要少去了鎖,效率確實就極大的提升,看起來我們就不用往下講了,因為之前說的問題都解決了,是嗎?

 

其實不完全,比方說,除了context,我還要放點其他的東西,如:日志文件的fd、數據庫的連接…… 這怎么辦呢?一種辦法是,我們在Thread這個類里面,再繼續填充各種東西,比如:

 1 class Thread
 2 
 3 {
 4 
 5         private Context context;
 6 
 7         private int logFd;
 8 
 9         private MysqlConnection conn;
10 
11 }

那如果再加東西呢?按照這種方式,不但寫起來麻煩,而且擴展能力相當差。我們怎么改進呢?

 

第二次改進:線程內成員歸一到Map

其實也很簡單,我們不是有一種神奇的數據結構叫做map(有紅黑樹、Hash等實現版本)么?他就能幫我們來管理這些爛七八糟的東東啊。

 

 

於是乎,我們原來的那些代碼,就可以這樣的修改啦:

 1 class Thread
 2 
 3 {
 4 
 5         private Map<String, Object> threadMap;
 6 
 7  
 8 
 9         public Map<String, Object> getThreadMap()
10 
11         {
12 
13                 return threadMap;
14 
15         }
16 
17 }

線程創建並初始化的時候,執行以下代碼:

1 Threadthread = Thread.getCurrentThread();
2 
3 Map<String,Object> map = thread.getThreadMap();
4 
5 map.put("context", new Context());
6 
7 map.put("logFd", (new Log()).getFd());
8 
9 map.put("mysqlConnection", ConnectionPool.getConnection());

這樣,我們的代碼就有非常好的一個擴展性了,想放啥放啥,對吧。而且請求來了以后,也不加鎖,程序跑的飛快!如果你的程序要放些啥進去,也沒啥問題。不過,就是唯一有點問題,你的程序要記住各種字符串組成的key,比如:"context"、"logFd"等等。雖然也不是什么問題吧,不過也有些不是太完美。而且如果代碼是多個人寫的話,還有可能出現key的命名沖突,對吧(誰把誰覆蓋了都不知道,然后各種debug,最后發現了問題,只能說一句:我擦!)。

 

那我們又該怎么樣來解決這個問題呢?

 

第三次改進:對象地址取代命名

其實也不難,既然字符串容易造成混亂,我們把字符串換掉,換成一個不重復的東西不就結了嘛?那什么東西不會重復呢?很簡單,內存地址啊!於是,我們就可以把代碼改成這樣:

class ThreadMapKey

{}

 

class Thread

{

        private Map<ThreadMapKey, Object> threadMap;

 

        public Map<ThreadMapKey, Object> getThreadMap()

        {        

                return threadMap;

        }

}

 

全局建立幾個對象:

static ThreadMapKey context = newThreadMapKey();

static ThreadMapKey logFd = newThreadMapKey();

static ThreadMapKey mysqlConnection = newThreadMapKey();

 

線程初始化的時候調用:

Thread thread = Thread.getCurrentThread();

Map<ThreadMapKey, Object> map = thread.getThreadMap();

map.put(context, new Context());

map.put(logFd, (new Log()).getFd());

map.put(mysqlConnection, ConnectionPool.getConnection());

我們定義一個叫做ThreadMapKey的類,這個類啥事兒不干,他就是一個擺設。當全局初始化的時候,我們新建幾個他的實例,比如:context、logFd、mysqlConnection等,然后把他們當做ThreadMap的Key。這樣,不同的開發者再也不用擔心自己起的名字會沖突了,因為只要對象不一樣,他們的內存地址就是不一樣的,我們用他做的Key就是不一樣的。

 

好了,這樣看起來似乎已經很完美了。不過呢,對於追求極致美的程序員而言,他們不甘心,覺得還有瑕疵:每次要從這個線程取線程局部數據的時候,代碼寫起來都麻煩的很。具體看如下:

Thread thread = Thread.getCurrentThread();

Map<ThreadMapKey,Object> map = thread.getThreadMap();

Object obj = map.get(context);

Context value = (Context)obj;

這樣的代碼看起來似乎太不優美了,要寫這么多行代碼…… 我們如何優化呢?

 

第四次改進:包裝

如果我們把上述代碼包裝起來,是不是就不用每次都寫了呢?那怎么包裝呢?我們的ThreadMapKey就是一個很好的東東,我們就讓他提供一個函數,用來包裝。說干就干,看看代碼:

class ThreadMapKey <T>

{

        public T getValue()

        {

                Thread thread = Thread.getCurrentThread();

                Map<ThreadMapKey, Object>map = thread.getThreadMap();

                Object obj = map.get(this);

                T value = (T)obj;

                return value;

        }

 

        public void setValue(T value)

        {

                Thread thread =Thread.getCurrentThread();

                Map<ThreadMapKey, Object>map = thread.getThreadMap();

                map.put(this, value);

        }

}

 

class Thread

{

        private Map<ThreadMapKey, Object> threadMap;

 

        public Map<ThreadMapKey, Object> getThreadMap()

        {   

                return threadMap;

        }  

}

 

static ThreadMapKey context = new ThreadMapKey();

static ThreadMapKey logFd = new ThreadMapKey();

static ThreadMapKey mysqlConnection = new ThreadMapKey();

 

// init

context.setValue(new Context());

logFd.setValue((new Log()).getFd());

mysqlConnection.setValue(ConnectionPool.getConnection());

 

// get per query

Context value = context.getValue();

我們將ThreadMapKey這個類增加了兩個方法:getValuesetValue,他們分別從當前線程中取出ThreadMap,然后根據this關鍵字來獲取和設置value。而且用到了泛型,這樣取出來的值就可以直接賦值,而不用再自己寫代碼來做類型強轉。這下再看代碼,是不是簡介了很多很多。

 

=== 進入最終的話題 ===

 

上面講了這么多(都是老王YY的,沒有經過任何組織的認證,如果講的不對,大家就包含着哈,關鍵是大家看懂了就好 ^_^),其實這些都是java的一個叫做ThreadLocal類引出來的事兒。老王最初在學校里看ThreadLocal的代碼就看的有點繞,一會兒是ThreadLocal,一會兒又是Map,一會兒又是Thread。然后代碼跳來跳去,可能當時記住了,后來久了不看又忘了。用的時候也是容易產生錯誤的用法(百度里去搜索,大部分都是雷同的,完全沒把這個事情講清楚)。

 

后來去百度工作了,在實際的項目中,用到了線程數據這樣的東東,才發現:哦,原來線程這玩意兒可以私藏數據,然后還不用加鎖,真是好用!(在百度用的是c/c++,原理是一樣的)。用多了,再去看ThreadLocal的代碼,就自己琢磨和猜測他的演化過程了。

 

好了,我們拿ThreadLocal的代碼和我們上面的代碼做一個對應吧:

 

我們把

ThreadMapKey ->ThreadLocal

ThreadMap -> ThreadLocalMap

做了這樣的一個轉換以后,再來看Java的代碼:

public class ThreadLocal<T> {

    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();

    }

 

    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 class Thread implements Runnable {

    ThreadLocal.ThreadLocalMap threadLocals = null;

}

 

看看,是不是和我們的實現很像啊。我們的應用代碼怎么寫呢?

private static final ThreadLocal<Session> sessions = new ThreadLocal<>();

   

public static Session getThreadSession()

{

    Session session = sessions.get();

    if (session == null)

    {

       session = new Session();

       sessions.set(session);

    }

    return session;

}

 

這樣的使用是不是感覺很有B格呢?

 

好了,原理性的東西說完了,還有一點需要補充一下,就是在ThreadLocalMap的實現代碼里寫的很贊的一個點:

 

static class ThreadLocalMap {

 

    /**

     * The entries in this hash map extend WeakReference, using

     * its main ref field as the key (which is always a

     * ThreadLocal object).  Note that null keys (i.e. entry.get()

     * == null) mean that the key is no longer referenced, so the

     *entry can be expunged from table.  Such entries are referred to

     * as "stale entries" in the code that follows.

     */

    static class Entry extends WeakReference<ThreadLocal<?>> {

        /** Thevalue associated with this ThreadLocal. */

        Object value;

 

        Entry(ThreadLocal<?> k, Object v){

            super(k);

            value = v;

        }

    }

}

ThreadLocalMap的實現,沒有用標准的Map,而是自己實現了一個HashMap(有興趣的同學可以去讀讀源代碼,就是我們教科書上講的經典hash實現)。這個Map里的Entry類中的Key ,采用了弱引用的方式(而不是強引用),這樣的好處,就是如果有對象不再使用的時候,就會被系統回收。

 

總結一下:在了解了實現原理以后,如果你有跟線程相關的數據而又可以不加鎖的,就盡管使用ThreadLocal吧,真的很好用。他可以讓你的程序有更高的效率,更好的代碼整潔度。(而且還可以ZhuangBility!)

class ThreadMapKey <T>

{

        public T getValue()

        {

                Thread thread = Thread.getCurrentThread();

                Map<ThreadMapKey, Object>map = thread.getThreadMap();

                Object obj = map.get(this);

                T value = (T)obj;

                return value;

        }

 

        public void setValue(T value)

        {

                Thread thread =Thread.getCurrentThread();

                Map<ThreadMapKey, Object>map = thread.getThreadMap();

                map.put(this, value);

        }

}

 

class Thread

{

        private Map<ThreadMapKey, Object> threadMap;

 

        public Map<ThreadMapKey, Object> getThreadMap()

        {   

                return threadMap;

        }  

}

 

static ThreadMapKey context = new ThreadMapKey();

static ThreadMapKey logFd = new ThreadMapKey();

static ThreadMapKey mysqlConnection = new ThreadMapKey();

 

// init

context.setValue(new Context());

logFd.setValue((new Log()).getFd());

mysqlConnection.setValue(ConnectionPool.getConnection());

 

// get per query

Context value = context.getValue();


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM