鎖是用來作併發最簡單的方式,固然其代價也是最高的。內核態的鎖的時候須要操做系統進行一次上下文切換,加鎖、釋放鎖會致使比較多的上下文切換和調度延時,等待鎖的線程會被掛起直至鎖釋放。在上下文切換的時候,cpu以前緩存的指令和數據都將失效,對性能有很大的損失。操做系統對多線程的鎖進行判斷就像兩姐妹在爲一個玩具在爭吵,而後操做系統就是能決定他們誰能拿到玩具的父母,這是很慢的。用戶態的鎖雖然避免了這些問題,可是其實它們只是在沒有真實的競爭時纔有效。html
Java在JDK1.5以前都是靠synchronized關鍵字保證同步的,這種經過使用一致的鎖定協議來協調對共享狀態的訪問,能夠確保不管哪一個線程持有守護變量的鎖,都採用獨佔的方式來訪問這些變量,若是出現多個線程同時訪問鎖,那第一些線線程將被掛起,當線程恢復執行時,必須等待其它線程執行完他們的時間片之後才能被調度執行,在掛起和恢復執行過程當中存在着很大的開銷。鎖還存在着其它一些缺點,當一個線程正在等待鎖時,它不能作任何事。若是一個線程在持有鎖的狀況下被延遲執行,那麼全部須要這個鎖的線程都沒法執行下去。若是被阻塞的線程優先級高,而持有鎖的線程優先級低,將會致使優先級反轉(Priority Inversion)。java
獨佔鎖是一種悲觀鎖,synchronized就是一種獨佔鎖,它假設最壞的狀況,而且只有在確保其它線程不會形成干擾的狀況下執行,會致使其它全部須要鎖的線程掛起,等待持有鎖的線程釋放鎖。而另外一個更加有效的鎖就是樂觀鎖。所謂樂觀鎖就是,每次不加鎖而是假設沒有衝突而去完成某項操做,若是由於衝突失敗就重試,直到成功爲止。node
與鎖相比,volatile變量是一和更輕量級的同步機制,由於在使用這些變量時不會發生上下文切換和線程調度等操做,可是volatile變量也存在一些侷限:不能用於構建原子的複合操做,所以當一個變量依賴舊值時就不能使用volatile變量。(參考:談談volatiile)nginx
volatile只能保證變量對各個線程的可見性,但不能保證原子性。爲何?見個人另一篇文章:《爲何volatile不能保證原子性而Atomic能夠?》git
原子操做指的是在一步以內就完成並且不能被中斷。原子操做在多線程環境中是線程安全的,無需考慮同步的問題。在java中,下列操做是原子操做:github
問題來了,爲何long型賦值不是原子操做呢?例如:算法
1
|
long
foo = 65465498L;
|
實時上java會分兩步寫入這個long變量,先寫32位,再寫後32位。這樣就線程不安全了。若是改爲下面的就線程安全了:數據庫
1
|
private
volatile
long
foo;
|
由於volatile內部已經作了synchronized.編程
要實現無鎖(lock-free)的非阻塞算法有多種實現方法,其中CAS(比較與交換,Compare and swap)是一種有名的無鎖算法。CAS, CPU指令,在大多數處理器架構,包括IA3二、Space中採用的都是CAS指令,CAS的語義是「我認爲V的值應該爲A,若是是,那麼將V的值更新爲B,不然不修改並告訴V的值實際爲多少」,CAS是項樂觀鎖技術,當多個線程嘗試使用CAS同時更新同一個變量時,只有其中一個線程能更新變量的值,而其它線程都失敗,失敗的線程並不會被掛起,而是被告知此次競爭中失敗,並能夠再次嘗試。CAS有3個操做數,內存值V,舊的預期值A,要修改的新值B。當且僅當預期值A和內存值V相同時,將內存值V修改成B,不然什麼都不作。CAS無鎖算法的C實現以下:api
1
2
3
4
5
6
7
8
9
|
int
compare_and_swap (
int
* reg,
int
oldval,
int
newval)
{
ATOMIC();
int
old_reg_val = *reg;
if
(old_reg_val == oldval)
*reg = newval;
END_ATOMIC();
return
old_reg_val;
}
|
CAS比較與交換的僞代碼能夠表示爲:
do{
備份舊數據;
基於舊數據構造新數據;
}while(!CAS( 內存地址,備份的舊數據,新數據 ))
(上圖的解釋:CPU去更新一個值,但若是想改的值再也不是原來的值,操做就失敗,由於很明顯,有其它操做先改變了這個值。)
就是指當二者進行比較時,若是相等,則證實共享數據沒有被修改,替換成新值,而後繼續往下運行;若是不相等,說明共享數據已經被修改,放棄已經所作的操做,而後從新執行剛纔的操做。容易看出 CAS 操做是基於共享數據不會被修改的假設,採用了相似於數據庫的 commit-retry 的模式。當同步衝突出現的機會不多時,這種假設能帶來較大的性能提高。
前面說過了,CAS(比較並交換)是CPU指令級的操做,只有一步原子操做,因此很是快。並且CAS避免了請求操做系統來裁定鎖的問題,不用麻煩操做系統,直接在CPU內部就搞定了。但CAS就沒有開銷了嗎?不!有cache miss的狀況。這個問題比較複雜,首先須要瞭解CPU的硬件體系結構:
上圖能夠看到一個8核CPU計算機系統,每一個CPU有cache(CPU內部的高速緩存,寄存器),管芯內還帶有一個互聯模塊,使管芯內的兩個核能夠互相通訊。在圖中央的系統互聯模塊可讓四個管芯相互通訊,而且將管芯與主存鏈接起來。數據以「緩存線」爲單位在系統中傳輸,「緩存線」對應於內存中一個 2 的冪大小的字節塊,大小一般爲 32 到 256 字節之間。當 CPU 從內存中讀取一個變量到它的寄存器中時,必須首先將包含了該變量的緩存線讀取到 CPU 高速緩存。一樣地,CPU 將寄存器中的一個值存儲到內存時,不只必須將包含了該值的緩存線讀到 CPU 高速緩存,還必須確保沒有其餘 CPU 擁有該緩存線的拷貝。
好比,若是 CPU0 在對一個變量執行「比較並交換」(CAS)操做,而該變量所在的緩存線在 CPU7 的高速緩存中,就會發生如下通過簡化的事件序列:
以上是刷新不一樣CPU緩存的開銷。最好狀況下的 CAS 操做消耗大概 40 納秒,超過 60 個時鐘週期。這裏的「最好狀況」是指對某一個變量執行 CAS 操做的 CPU 正好是最後一個操做該變量的CPU,因此對應的緩存線已經在 CPU 的高速緩存中了,相似地,最好狀況下的鎖操做(一個「round trip 對」包括獲取鎖和隨後的釋放鎖)消耗超過 60 納秒,超過 100 個時鐘週期。這裏的「最好狀況」意味着用於表示鎖的數據結構已經在獲取和釋放鎖的 CPU 所屬的高速緩存中了。鎖操做比 CAS 操做更加耗時,是因深刻理解並行編程
爲鎖操做的數據結構中須要兩個原子操做。緩存未命中消耗大概 140 納秒,超過 200 個時鐘週期。須要在存儲新值時查詢變量的舊值的 CAS 操做,消耗大概 300 納秒,超過 500 個時鐘週期。想一想這個,在執行一次 CAS 操做的時間裏,CPU 能夠執行 500 條普通指令。這代表了細粒度鎖的侷限性。
如下是cache miss cas 和lock的性能對比:
在JDK1.5以前,若是不編寫明確的代碼就沒法執行CAS操做,在JDK1.5中引入了底層的支持,在int、long和對象的引用等類型上都公開了CAS的操做,而且JVM把它們編譯爲底層硬件提供的最有效的方法,在運行CAS的平臺上,運行時把它們編譯爲相應的機器指令,若是處理器/CPU不支持CAS指令,那麼JVM將使用自旋鎖。所以,值得注意的是,CAS解決方案與平臺/編譯器緊密相關(好比x86架構下其對應的彙編指令是lock cmpxchg,若是想要64Bit的交換,則應使用lock cmpxchg8b。在.NET中咱們可使用Interlocked.CompareExchange函數)。
在原子類變量中,如java.util.concurrent.atomic中的AtomicXXX,都使用了這些底層的JVM支持爲數字類型的引用類型提供一種高效的CAS操做,而在java.util.concurrent中的大多數類在實現時都直接或間接的使用了這些原子變量類。
Java 1.6中AtomicLong.incrementAndGet()的實現源碼爲:
因而可知,AtomicLong.incrementAndGet的實現用了樂觀鎖技術,調用了sun.misc.Unsafe類庫裏面的 CAS算法,用CPU指令來實現無鎖自增。因此,AtomicLong.incrementAndGet的自增比用synchronized的鎖效率倍增。
1
2
3
4
5
6
7
8
9
10
11
12
|
public
final
int
getAndIncrement() {
for
(;;) {
int
current = get();
int
next = current +
1
;
if
(compareAndSet(current, next))
return
current;
}
}
public
final
boolean
compareAndSet(
int
expect,
int
update) {
return
unsafe.compareAndSwapInt(
this
, valueOffset, expect, update);
}
|
下面是測試代碼:能夠看到用AtomicLong.incrementAndGet的性能比用synchronized高出幾倍。
下面是比非阻塞自增稍微複雜一點的CAS的例子:非阻塞堆棧/ConcurrentStack
。ConcurrentStack
中的 push()
和 pop()
操做在結構上與NonblockingCounter
上類似,只是作的工做有些冒險,但願在 「提交」 工做的時候,底層假設沒有失效。push()
方法觀察當前最頂的節點,構建一個新節點放在堆棧上,而後,若是最頂端的節點在初始觀察以後沒有變化,那麼就安裝新節點。若是 CAS 失敗,意味着另外一個線程已經修改了堆棧,那麼過程就會從新開始。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
public
class
ConcurrentStack<E> {
AtomicReference<Node<E>> head =
new
AtomicReference<Node<E>>();
public
void
push(E item) {
Node<E> newHead =
new
Node<E>(item);
Node<E> oldHead;
do
{
oldHead = head.get();
newHead.next = oldHead;
}
while
(!head.compareAndSet(oldHead, newHead));
}
public
E pop() {
Node<E> oldHead;
Node<E> newHead;
do
{
oldHead = head.get();
if
(oldHead ==
null
)
return
null
;
newHead = oldHead.next;
}
while
(!head.compareAndSet(oldHead,newHead));
return
oldHead.item;
}
static
class
Node<E> {
final
E item;
Node<E> next;
public
Node(E item) {
this
.item = item; }
}
}
|
在輕度到中度的爭用狀況下,非阻塞算法的性能會超越阻塞算法,由於 CAS 的多數時間都在第一次嘗試時就成功,而發生爭用時的開銷也不涉及線程掛起和上下文切換,只多了幾個循環迭代。沒有爭用的 CAS 要比沒有爭用的鎖便宜得多(這句話確定是真的,由於沒有爭用的鎖涉及 CAS 加上額外的處理),而爭用的 CAS 比爭用的鎖獲取涉及更短的延遲。
在高度爭用的狀況下(即有多個線程不斷爭用一個內存位置的時候),基於鎖的算法開始提供比非阻塞算法更好的吞吐率,由於當線程阻塞時,它就會中止爭用,耐心地等候輪到本身,從而避免了進一步爭用。可是,這麼高的爭用程度並不常見,由於多數時候,線程會把線程本地的計算與爭用共享數據的操做分開,從而給其餘線程使用共享數據的機會。
以上的示例(自增計數器和堆棧)都是很是簡單的非阻塞算法,一旦掌握了在循環中使用 CAS,就能夠容易地模仿它們。對於更復雜的數據結構,非阻塞算法要比這些簡單示例複雜得多,由於修改鏈表、樹或哈希表可能涉及對多個指針的更新。CAS 支持對單一指針的原子性條件更新,可是不支持兩個以上的指針。因此,要構建一個非阻塞的鏈表、樹或哈希表,須要找到一種方式,能夠用 CAS 更新多個指針,同時不會讓數據結構處於不一致的狀態。
在鏈表的尾部插入元素,一般涉及對兩個指針的更新:「尾」 指針老是指向列表中的最後一個元素,「下一個」 指針從過去的最後一個元素指向新插入的元素。由於須要更新兩個指針,因此須要兩個 CAS。在獨立的 CAS 中更新兩個指針帶來了兩個須要考慮的潛在問題:若是第一個 CAS 成功,而第二個 CAS 失敗,會發生什麼?若是其餘線程在第一個和第二個 CAS 之間企圖訪問鏈表,會發生什麼?
對於非複雜數據結構,構建非阻塞算法的 「技巧」 是確保數據結構總處於一致的狀態(甚至包括在線程開始修改數據結構和它完成修改之間),還要確保其餘線程不只可以判斷出第一個線程已經完成了更新仍是處在更新的中途,還可以判斷出若是第一個線程走向 AWOL,完成更新還須要什麼操做。若是線程發現了處在更新中途的數據結構,它就能夠 「幫助」 正在執行更新的線程完成更新,而後再進行本身的操做。當第一個線程回來試圖完成本身的更新時,會發現再也不須要了,返回便可,由於 CAS 會檢測到幫助線程的干預(在這種狀況下,是建設性的干預)。
這種 「幫助鄰居」 的要求,對於讓數據結構免受單個線程失敗的影響,是必需的。若是線程發現數據結構正處在被其餘線程更新的中途,而後就等候其餘線程完成更新,那麼若是其餘線程在操做中途失敗,這個線程就可能永遠等候下去。即便不出現故障,這種方式也會提供糟糕的性能,由於新到達的線程必須放棄處理器,致使上下文切換,或者等到本身的時間片過時(而這更糟)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
public
class
LinkedQueue <E> {
private
static
class
Node <E> {
final
E item;
final
AtomicReference<Node<E>> next;
Node(E item, Node<E> next) {
this
.item = item;
this
.next =
new
AtomicReference<Node<E>>(next);
}
}
private
AtomicReference<Node<E>> head
=
new
AtomicReference<Node<E>>(
new
Node<E>(
null
,
null
));
private
AtomicReference<Node<E>> tail = head;
public
boolean
put(E item) {
Node<E> newNode =
new
Node<E>(item,
null
);
while
(
true
) {
Node<E> curTail = tail.get();
Node<E> residue = curTail.next.get();
if
(curTail == tail.get()) {
if
(residue ==
null
)
/* A */
{
if
(curTail.next.compareAndSet(
null
, newNode))
/* C */
{
tail.compareAndSet(curTail, newNode)
/* D */
;
return
true
;
}
}
else
{
tail.compareAndSet(curTail, residue)
/* B */
;
}
}
}
}
}
|
具體算法相見IBM Developerworks
Java5中的ConcurrentHashMap,線程安全,設計巧妙,用桶粒度的鎖,避免了put和get中對整個map的鎖定,尤爲在get中,只對一個HashEntry作鎖定操做,性能提高是顯而易見的。
具體實現中使用了鎖分離機制,在這個帖子中有很是詳細的討論。這裏有關於Java內存模型結合ConcurrentHashMap的分析。如下是JDK6的ConcurrentHashMap的源碼:
ConcurrentLinkedQueue也是一樣使用了CAS指令,但其性能並不高由於太多CAS操做。其源碼以下:
服務端編程的3大性能殺手:一、大量線程致使的線程切換開銷。二、鎖。三、非必要的內存拷貝。在高併發下,對於純內存操做來講,單線程是要比多線程快的, 能夠比較一下多線程程序在壓力測試下cpu的sy和ni百分比。高併發環境下要實現高吞吐量和線程安全,兩個思路:一個是用優化的鎖實現,一個是lock-free的無鎖結構。但非阻塞算法要比基於鎖的算法複雜得多。開發非阻塞算法是至關專業的訓練,並且要證實算法的正確也極爲困難,不只和具體的目標機器平臺和編譯器相關,並且須要複雜的技巧和嚴格的測試。雖然Lock-Free編程很是困難,可是它一般能夠帶來比基於鎖編程更高的吞吐量。因此Lock-Free編程是大有前途的技術。它在線程停止、優先級倒置以及信號安全等方面都有着良好的表現。
另外,在設計思路上除了儘可能減小資源爭用之外,還能夠借鑑nginx/node.js等單線程大循環的機制,用單線程或CPU數相同的線程開闢大的隊列,併發的時候任務壓入隊列,線程輪詢而後一個個順序執行。因爲每一個都採用異步I/O,沒有阻塞線程。這個大隊列可使用RabbitMQueue,或是JDK的同步隊列(性能稍差),或是使用Disruptor無鎖隊列(Java)。任務處理能夠所有放在內存(多級緩存、讀寫分離、ConcurrentHashMap、甚至分佈式緩存Redis)中進行增刪改查。最後用Quarz維護定時把緩存數據同步到DB中。固然,這只是中小型系統的思路,若是是大型分佈式系統會很是複雜,須要分而治理,用SOA的思路,參考這篇文章的圖。(注:Redis是單線程的純內存數據庫,單線程無需鎖,而Memcache是多線程的帶CAS算法,二者都使用epoll,no-blocking io)
若是深刻 JVM 和操做系統,會發現非阻塞算法無處不在。垃圾收集器使用非阻塞算法加快併發和平行的垃圾蒐集;調度器使用非阻塞算法有效地調度線程和進程,實現內在鎖。在 Mustang(Java 6.0)中,基於鎖的 SynchronousQueue
算法被新的非阻塞版本代替。不多有開發人員會直接使用 SynchronousQueue
,可是經過 Executors.newCachedThreadPool()
工廠構建的線程池用它做爲工做隊列。比較緩存線程池性能的對比測試顯示,新的非阻塞同步隊列實現提供了幾乎是當前實現 3 倍的速度。在 Mustang 的後續版本(代碼名稱爲 Dolphin)中,已經規劃了進一步的改進。