Java併發程式設計 -- 再論鎖的問題 -- 無鎖與鎖優化
在前面JUC原始碼分析和Disruptor分析序列中,我們已經反覆討論了鎖與無鎖的問題。
眾所周知,在多執行緒程式中,鎖是效能殺手。因此“鎖優化”一直是多執行緒中被頻繁探討的一個問題。
本文將從“鎖優化”這個應用層面,把前面的諸多東西串起來,探討一下鎖優化的一系列策略。
策略1:業務和設計層面 – 單執行緒或去共享資源
我們知道,至所以要加鎖,是因為多執行緒 + 共享資源。
如果我們可以根據具體業務場景,或者從頂層設計層面,不用多執行緒,或者去掉共享資源,那不就可以無鎖了嗎?
這個策略呢,不是一個純粹的Java的技術問題,沒有標準答案,也沒有放之四海皆準的一個原則。所以最難掌握,但往往是最關鍵的。
下面舉幾個這樣的例子:
單執行緒
我們知道,Redis就是單執行緒模型,與之相反的是Memcached的多執行緒模型。個人沒看過Redis的原始碼,但從這個一般描述可以看出,Redis肯定比Memcached所面對的鎖的問題要簡化很多。
ThreadLocal
ThreadLocal就是一個典型的去掉共享資源的辦法。本來是多個執行緒,共享一個物件,必然要加鎖。
那如果我為每個執行緒準備一個物件的拷貝,各訪問各的,不就可以無鎖了!
這個的典型例子就是SimpleDateFormat,我們知道這個類是非執行緒安全的,要在多執行緒中訪問,一個辦法就是加ThreadLocal。關於這個的程式碼,網上很多,此處就不列舉了。
策略2: single-writer principle – volatile
在前面的序列文章中,我們已經提到過Linux核心的kfifo佇列,Disruptor中的RingBuffer都是完全無鎖的。
至所以他們可以做到,有一個前提條件,就是:單執行緒寫。
只要是單執行緒寫,1寫1讀,或者1寫多讀,不是多寫多讀。那就可以不加鎖,通過volatile變數,實現記憶體的可見性。
策略3: 樂觀鎖 – CAS
相對悲觀鎖,樂觀鎖通過CAS + 自旋實現,而不呼叫底層pthread的mutex物件,更加輕量。比直接使用悲觀鎖要更優。
關於CAS + 自旋,其實不管是synchronized關鍵字的實現,還是JUC中lock的實現,都用了此種技術。
當然,在這個前面,還有一個策略,就是“偏向鎖”,專門處理單執行緒多次呼叫加鎖程式碼的優化。
策略4:鎖細化
鎖細化,也就是降低鎖的粒度,提供併發讀。
ConcurrentHashMap就是鎖細化的典型例子;Mysql中InnoDB的行鎖,相對MyIsam的表鎖,也是一個典型例子。
策略5:鎖粗化
與鎖細化剛好相反,有時候,我們需要提高鎖的粒度。
比如在一個for迴圈裡面,不斷加鎖/解鎖,那還不如把鎖拿到for迴圈外面,只加1次/解鎖1次;
再比如一個函式裡面,有2段程式碼,分別對同1把鎖,加鎖/解鎖,那可能還不如把2段程式碼放到1個裡面。
策略6: 鎖分離
也就是前面說的讀寫鎖,同1把鎖分離成讀鎖 + 寫鎖。此處不再詳述。
策略7:鎖消除與逃逸分析
鎖消除不是應用層程式碼做的事情,而是編譯器做的事情。
所謂逃逸分析,就是編譯器發現一個內部有鎖的物件,比如StringBuffer。它只在某1個函式內部使用,函式外部沒有其他地方用它,那就是這個物件沒有逃獄到函式外面,意味著這個物件只可能單執行緒訪問,那就可以去掉鎖。
對應的引數是
-xx:+DoEscapeAnalysis -xx:+EliminateLocks