前面的兩篇總結簡單的說明了同步的一些問題,在使用基礎的同步機制中還有兩個可以分享的技術:volatile關鍵字和ThreadLocal。合 理的根據情境利用這些技術,可以有效提高並發的效能,下面嘗試結合自己的理解敘述這部分的內容,應該會有理解的偏差,我也會盡量 的在完善自己理解的同時同步更新文章的錯誤。
或許在知道synchronized配和對象內部鎖的機制以後,可以提高寫出正確同步的並發程式成功率,但是這時候會遇到另一個大問題:性 能!是的,對於 synchronized帶來的可能龐大的效能成本,開發人員們總結出不同的優秀的最佳化方案:常見的是鎖的分解和鎖持有時間的最 小化。有效降低鎖持有的時間對競爭線程激烈的調用會大大的提高效能,所以不要輕易的在方法上聲明synchronized,應該在需要保護的 代碼塊上添加 synchronized。另一個方案是拆分鎖的競爭顆粒的大小,與其幾百個線程競爭一個對象的鎖,不如幾個或者幾十個線程競爭 多個對象的鎖,常見的應用是ConcurrentHashMap的實現,其內部有類似的鎖對象數組維護每段表內的線程競爭,預設16個對象鎖,當然提 供參數可調。這對於儲存了成千上萬個執行個體的map效能提升不言而喻,線程的競爭被分散到多段的小競爭,再也不用全部的堆在門口傻等了 。
但是synchronized同步和類似的機制帶來的效能成本,還是使得開發人員不能不研究無鎖和低成本的同步機制來保證並發的效能。 volatile就是被認為“輕量級的synchronized”,但是使用其雖然可以簡化同步的編碼,並且運行開銷相對於JVM沒有最佳化的競爭線程同步 低,但是濫用將不能保證程式的正確性。鎖的兩個特性是:互斥和可見。互斥保證了同時只有一個線程持有對象鎖進行共用資料的操作,從 而保證了資料操作的原子性,而可見則保證共用資料的修改在下一個線程獲得鎖後看到更新後的資料。volatile僅僅保證了無鎖的可見度, 但是不提供原子性操作的保證!這是因為volatile關鍵字作用的設計是JVM阻止volatile變數的值放入處理器的寄存器,在寫入值以後會被 從處理器的cache中flush掉,寫到記憶體中去。這樣讀的時候限制處理器的cache是無效的,只能從記憶體讀取值,保證了可見度。從這個實現 可以看出volatile的使用情境:多線程大量的讀取,極少量或者一次性的寫入,並且還有其他限制。
由於其無法保證“讀-修改-寫”這樣操作的原子性(當然java.util.concurrent.atomic包內的實現滿足這些操作,主要是通過 CAS-- 比較交換的機制,後續會嘗試寫寫。),所以像++,--,+=,-=這樣的變數操作,即使聲明volatile也不會保證正確性。圍繞這個原理的主題 ,我們可以大致的整理一下volatile代替synchronized的條件:對變數的寫操作不依賴自身的狀態。所以除了剛剛介紹的操作外,例如:
Java代碼
private volatile boolean flag;
if(!flag) {
flag == true;
}
類似這樣的操作也是違反volatile使用條件的,很可能造成程式的問題。所以使用volatile的簡單情境是一次性的寫入之後,大量線程 的讀取並且不再改變變數的值(如果這樣的話,都不是並發了)。這個關鍵字的優勢還是在於多線程的讀取,既保證了讀取的低開銷(與單 線程程式變數差不多),又能保證讀到的是最新的值。所以利用這個優勢我們可以結合synchronized使用實現低開銷讀寫鎖:
Java代碼
/**
* User: yanxuxin
* Date: Dec 12, 2009
* Time: 8:28:29 PM
*/
public class AnotherSyncSample {
private volatile int counter;
public int getCounter() {
return counter;
}
public synchronized void add() {
counter++;
}
}