一、單線程改造為多線程也是個技術活
正如我們看到耗子叔叔博客裡寫的那樣,原來是單線程的應用程序,”後來,我們的程序性能有問題,所以需要變成多線程的,於是,變成多線程後到了線上,發現程序經常占了100%的CPU“。
考慮到是淘寶的工程師曝出來的問題,他們的技術基礎一般都很扎實,連他們都用錯了,所以把單線程改造為多線程並不是想象中的那麼簡單,我認為。
你可能很不服氣地反問,淘寶的工程師又怎麼了,單線程改為多線程有什麼難的?無非就是應用現有的多線程技術嘛,你看,我有非常強烈的線程安全意識,我知道同步、死鎖、競態條件,還知道lock free和線程安全容器,還知道各種線程安全同步構造……難道還寫不出線程安全的應用程序?
實際情況是,線程安全的應用程序並不一定因為你有扎實的線程安全基礎和開發經驗就能夠寫好的。
試著舉兩個例子:
1、使用線程安全容器通過索引取數據
很多人知道的線程安全容器,實際使用的時候並不一定不出現BUG,下面的(有隱患的)代碼就比較典型:
代碼如下:
static int GetFirstOrDefault(ThreadSafeList<int> list)
{
if (list.Count > 0)
{
return list[0];
}
return 0;
}
上面的函數參數list如果一開始傳入一個元素總數為1的列表,大家能分析出上面的代碼會有什麼問題嗎?
關於線程安全容器,之前我恰好也總結過一篇文章<深入線程安全容器的實現方法>。線程安全容器並不真正安全,上面有問題的代碼就是出自於這裡。
2、多線程操作郵件的失誤
還有就是多線程應用場景的分析可能不正確,曾經因為一個郵件收發程序的性能問題,我也大膽改造過應用程序,改來改去就出現了重大BUG,
大家可以看看我痛心疾首總結過的<基於一個應用程序多線程誤用的分析詳解>。
上面舉的這兩個例子,我只是想說明,多線程應用程序中,因為線程安全產生的BUG其實是很微妙的,一個考慮不周或者認識不夠深刻,出現問題的可能性簡直防不勝防。
二、ReHash的代價
上面第一點主要是閒談線程安全,接著我們也說說哈希表,深刻理解消耗成本很大的ReHash。
我們平常理解中的哈希表是“以空間換時間的一種數據結構”。這樣說的太久了,大家可能會有一種直觀上的錯覺,就是哈希表犧牲的是空間,爭取的是時間。
但是,ReHash的過程其實是空間和時間的雙重重大損失,因為分析源代碼,我們知道ReHash的過程其實就是一個動態擴容的過程,而哈希表的擴容是個空間和時間消耗都非常驚人的內部操作。
為什麼說ReHash是個空間和時間消耗都非常驚人的內部操作呢?
1、原來當我們對哈希結構的容器進行擴容時,散列表內部要重新new一個更大的數組,然後把原來數組的內容拷貝到新數組,並進行重新散列;
2、new出來的這個更大的新數組容量有多大也是一門學問,一般來說,新數組的大小會設置成原數組雙倍大小的相近的一個素數(.NET中這個素數的生成還有一定的技巧)。
從1和2這兩點可以看出,ReHash的代價確實非常高。在不久以前我碰巧寫過一篇關於.NET容器的動態擴容的文章<解析從源碼分析常見的基於Array的數據結構動態擴容機制的詳解>,其中也淺顯總結了.NET的HashTable的擴容機制,現在對照Java中的HashMap源碼,看到熟悉的ReHash函數命名,再看一遍.NET中的實現,果然有比較才能有提高。
至於我們平時所理解的“以空間換時間“,其實是指哈希具有O(1)復雜度的數據檢索效率,但它受填充因子影響,空間開銷通常很大,空間利用率不高。
所以我們常常說哈希表適用於讀操作頻繁,寫操作較少應用場景,比如把哈希表當做緩存容器,於我心有戚戚焉。
最後看到這句“有人把這個問題報給了Sun,不過Sun不認為這個是一個問題。因為HashMap本來就不支持並發。要並發就用ConcurrentHashmap…”
根據實際開發經驗,線程安全的容器並不真正線程安全,會用ConcurrentHashmap也只是進入初級階段,同時忍不住要感慨下當年如日中天風光無限的Sun。