話說在《操作系統原理》這門課里面,我們學到了很多概念:進程、線程、鎖、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這個類增加了兩個方法:getValue和setValue,他們分別從當前線程中取出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();