前面的兩篇總結簡單的說明了同步的一些問題,在使用基礎的同步機制中還有兩個可以分享的技術: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++;
}
}
這個簡單的例子在讀的方法上沒有使用synchronized關鍵字,所以讀的操作幾乎沒有等待;而由於寫的操作是原子性的違反了使用條件 ,不能得到保證,所以使用synchronized同步得到寫的正確性保證,這個模型在多讀取少寫入的實際場景中應該要比都用synchronized的性 能有不小的提升。
另外還有一個使用volatile的好處,得自於其原理:內部禁止改變兩個volatile變量的賦值或者初始化順序,並且嚴格限制volatile變 量和其周圍非volatile變量的賦值或者初始化順序。
Java代碼
/**
* User: yanxuxin
* Date: Dec 12, 2009
* Time: 8:34:07 PM
*/
public class VolatileTest {
public static void main(String[] args) {
final VolatileSample sample = new VolatileSample();
new Thread(new Runnable(){
public void run() {
sample.finish();
}
}).start();
new Thread(new Runnable(){
public void run() {
sample.doSomething();
}
}).start();
}
}
class VolatileSample {
private volatile boolean finished;
private int lucky;
public void doSomething() {
if(finished) {
System.out.println("lucky: " + lucky);
}
}
public void finish() {
lucky = 7;
finished = true;
}
}
這裡首先線程A執行finish(),完成finished變量的賦值後,線程B進入方法doSomething()讀到了finish的值為 true,打印lucky的值, 預想狀態下為7,這樣完美的執行結束了。但是,事實是如果finished變量不是聲明了volatile的話,過程就有可能是這樣的:線程A執行 finish()先對finished賦值,與此同時線程B進入doSomething()得到finished的值為 true,打印lucky的值為0,鏡頭切回線程A,接著給 lucky賦值為7,可憐的是這個幸運數字不幸杯具了。因為這裡發生了扯淡的事情:JVM或許為了優化執行把兩者的賦值順序調換了。這個結 果在單線程的程序中簡直絕對一定肯定就是不可能,遺憾的是多線程存在這個隱患。
所以不說其它的知識,想用Java實現正確,高性能的並發程序是需要處處小心的。後面想說的ThreadLocal就是看慣了線程為了共享數據 而屢屢發生慘劇後,想把數據與線程死死綁定不共享的另一個技術。當然還想嘗試寫寫對atomic包的理解,對並發集合的理解,對線程池的 理解。所有的這些基礎有個清晰的認識,才能有自信寫寫正確的,性能稍好的並發程序。