Java并發(fā)編程(二)同步

如果你的java基礎較弱肠槽,或者不大了解java多線程請先看這篇文章java多線程(一)線程定義、狀態(tài)和屬性

同步一直是java多線程的難點陷遮,在我們做android開發(fā)時也很少應用,但這并不是我們不熟悉同步的理由。希望這篇文章能使更多的人能夠了解并且應用java的同步慧域。
在多線程的應用中,兩個或者兩個以上的線程需要共享對同一個數(shù)據(jù)的存取浪读。如果兩個線程存取相同的對象昔榴,并且每一個線程都調用了修改該對象的方法辛藻,這種情況通常成為競爭條件。
競爭條件最容易理解的例子就是:比如火車賣票互订,火車票是一定的吱肌,但賣火車票的窗口到處都有,每個窗口就相當于一個線程仰禽,這么多的線程共用所有的火車票這個資源氮墨。并且無法保證其原子性,如果在一個時間點上吐葵,兩個線程同時使用這個資源规揪,那他們取出的火車票是一樣的(座位號一樣),這樣就會給乘客造成麻煩温峭。解決方法為猛铅,當一個線程要使用火車票這個資源時,我們就交給它一把鎖凤藏,等它把事情做完后在把鎖給另一個要用這個資源的線程奸忽。這樣就不會出現(xiàn)上述情況。

1. 鎖對象
synchronized關鍵字自動提供了鎖以及相關的條件揖庄,大多數(shù)需要顯式鎖的情況使用synchronized非常的方便栗菜,但是等我們了解ReentrantLock類和條件對象時,我們能更好的理解synchronized關鍵字抠艾。ReentrantLock是JAVA SE 5.0引入的苛萎, 用ReentrantLock保護代碼塊的結構如下:

mLock.lock();
try{
...
}
finally{
mLock.unlock();
}

這一結構確保任何時刻只有一個線程進入臨界區(qū),一旦一個線程封鎖了鎖對象检号,其他任何線程都無法通過lock語句腌歉。當其他線程調用lock時,它們則被阻塞直到第一個線程釋放鎖對象齐苛。把解鎖的操作放在finally中是十分必要的翘盖,如果在臨界區(qū)發(fā)生了異常,鎖是必須要釋放的凹蜂,否則其他線程將會永遠阻塞馍驯。

2. 條件對象
進入臨界區(qū)時,卻發(fā)現(xiàn)在某一個條件滿足之后玛痊,它才能執(zhí)行汰瘫。要使用一個條件對象來管理那些已經(jīng)獲得了一個鎖但是卻不能做有用工作的線程,條件對象又稱作條件變量擂煞。
我們來看看下面的例子來看看為何需要條件對象

假設一個場景我們需要用銀行轉賬混弥,我們首先寫了銀行的類,它的構造函數(shù)需要傳入賬戶數(shù)量和賬戶金額

public class Bank {
private double[] accounts;
    private Lock bankLock;
    public Bank(int n,double initialBalance){
        accounts=new double[n];
        bankLock=new ReentrantLock();
        for (int i=0;i<accounts.length;i++){
            accounts[i]=initialBalance;
        }
    }
    }

接下來我們要提款对省,寫一個提款的方法蝗拿,from是轉賬方晾捏,to是接收方,amount轉賬金額哀托,結果我們發(fā)現(xiàn)轉賬方余額不足惦辛,如果有其他線程給這個轉賬方再存足夠的錢就可以轉賬成功了,但是這個線程已經(jīng)獲取了鎖仓手,它具有排他性胖齐,別的線程也無法獲取鎖來進行存款操作,這就是我們需要引入條件對象的原因俗或。

   public void transfer(int from,int to,int amount){
        bankLock.lock();
        try{
            while (accounts[from]<amount){
                //wait
            }
        }finally {
            bankLock.unlock();
        }
    }

一個鎖對象擁有多個相關的條件對象市怎,可以用newCondition方法獲得一個條件對象,我們得到條件對象后調用await方法辛慰,當前線程就被阻塞了并放棄了鎖

public class Bank {
private double[] accounts;
    private Lock bankLock;
    private Condition condition;
    public Bank(int n,double initialBalance){
        accounts=new double[n];
        bankLock=new ReentrantLock();
        //得到條件對象
        condition=bankLock.newCondition();
        for (int i=0;i<accounts.length;i++){
            accounts[i]=initialBalance;
        }
    }
    public void transfer(int from,int to,int amount) throws InterruptedException {
        bankLock.lock();
        try{
            while (accounts[from]<amount){
                //阻塞當前線程区匠,并放棄鎖
                condition.await();
            }
        }finally {
            bankLock.unlock();
        }
    }
}

等待獲得鎖的線程和調用await方法的線程本質上是不同的,一旦一個線程調用的await方法帅腌,他就會進入該條件的等待集驰弄。當鎖可用時,該線程不能馬上解鎖速客,相反他處于阻塞狀態(tài)戚篙,直到另一個線程調用了同一個條件上的signalAll方法時為止。當另一個線程準備轉賬給我們此前的轉賬方時溺职,只要調用condition.signalAll()岔擂;該調用會重新激活因為這一條件而等待的所有線程。
當一個線程調用了await方法他沒法重新激活自身浪耘,并寄希望于其他線程來調用signalAll方法來激活自身乱灵,如果沒有其他線程來激活等待的線程,那么就會產(chǎn)生死鎖現(xiàn)象七冲,如果所有的其他線程都被阻塞痛倚,最后一個活動線程在解除其他線程阻塞狀態(tài)前調用await,那么它也被阻塞,就沒有任何線程可以解除其他線程的阻塞澜躺,程序就被掛起了蝉稳。
那何時調用signalAll呢?正常來說應該是有利于等待線程的方向改變時來調用signalAll掘鄙。在這個例子里就是耘戚,當一個賬戶余額發(fā)生變化時,等待的線程應該有機會檢查余額操漠。

 public void transfer(int from,int to,int amount) throws InterruptedException {
        bankLock.lock();
        try{
            while (accounts[from]<amount){
                //阻塞當前線程收津,并放棄鎖
                condition.await();
            }
            //轉賬的操作
            ...
            condition.signalAll();
        }finally {
            bankLock.unlock();
        }
    }

當調用signalAll方法時并不是立即激活一個等待線程,它僅僅解除了等待線程的阻塞,以便這些線程能夠在當前線程退出同步方法后朋截,通過競爭實現(xiàn)對對象的訪問。還有一個方法是signal吧黄,它則是隨機解除某個線程的阻塞部服,如果該線程仍然不能運行,那么則再次被阻塞拗慨,如果沒有其他線程再次調用signal廓八,那么系統(tǒng)就死鎖了。

3. Synchronized關鍵字
Lock和Condition接口為程序設計人員提供了高度的鎖定控制赵抢,然而大多數(shù)情況下剧蹂,并不需要那樣的控制,并且可以使用一種嵌入到java語言內部的機制烦却。從Java1.0版開始宠叼,Java中的每一個對象都有一個內部鎖。如果一個方法用synchronized關鍵字聲明其爵,那么對象的鎖將保護整個方法冒冬。也就是說,要調用該方法摩渺,線程必須獲得內部的對象鎖简烤。
換句話說,

public synchronized void method(){

}

等價于

public void method(){
this.lock.lock();
try{

}finally{
this.lock.unlock();
}

上面銀行的例子摇幻,我們可以將Bank類的transfer方法聲明為synchronized,而不是使用一個顯示的鎖横侦。
內部對象鎖只有一個相關條件,wait放大添加到一個線程到等待集中绰姻,notifyAll或者notify方法解除等待線程的阻塞狀態(tài)枉侧。也就是說wait相當于調用condition.await(),notifyAll等價于condition.signalAll();

我們上面的例子transfer方法也可以這樣寫:

    public synchronized void transfer(int from,int to,int amount)throws InterruptedException{
        while (accounts[from]<amount) {
            wait();
        }
        //轉賬的操作
        ...
        notifyAll();   
        }

可以看到使用synchronized關鍵字來編寫代碼要簡潔很多龙宏,當然要理解這一代碼棵逊,你必須要了解每一個對象有一個內部鎖,并且該鎖有一個內部條件银酗。由鎖來管理那些試圖進入synchronized方法的線程辆影,由條件來管理那些調用wait的線程。

4. 同步阻塞
上面我們說過黍特,每一個Java對象都有一個鎖蛙讥,線程可以調用同步方法來獲得鎖,還有另一種機制可以獲得鎖灭衷,通過進入一個同步阻塞次慢,當線程進入如下形式的阻塞:

synchronized(obj){

}

于是他獲得了obj的鎖。再來看看Bank類

public class Bank {
private double[] accounts;
private Object lock=new Object();
   public Bank(int n,double initialBalance){
        accounts=new double[n];
        for (int i=0;i<accounts.length;i++){
            accounts[i]=initialBalance;
        }
    }
    public void transfer(int from,int to,int amount){
        synchronized(lock){
          //轉賬的操作
            ...
        }
    }
}

在此,lock對象創(chuàng)建僅僅是用來使用每個Java對象持有的鎖迫像。有時開發(fā)人員使用一個對象的鎖來實現(xiàn)額外的原子操作劈愚,稱為客戶端鎖定。例如Vector類闻妓,它的方法是同步的【穑現(xiàn)在假設在Vector中存儲銀行余額

  public void transfer(Vector<Double>accounts,int from,int to,int amount){
  accounts.set(from,accounts.get(from)-amount);
  accounts.set(to,accounts.get(to)+amount;
}

Vecror類的get和set方法是同步的,但是這并未對我們有所幫助由缆。在第一次對get調用完成以后注祖,一個線程完全可能在transfer方法中被被剝奪運行權,于是另一個線程可能在相同的存儲位置存入了不同的值均唉,但是是晨,我們可以截獲這個鎖

  public void transfer(Vector<Double>accounts,int from,int to,int amount){
  synchronized(accounts){
  accounts.set(from,accounts.get(from)-amount);
  accounts.set(to,accounts.get(to)+amount;
  }
}

客戶端鎖定(同步代碼塊)是非常脆弱的,通常不推薦使用舔箭,一般實現(xiàn)同步最好用java.util.concurrent包下提供的類罩缴,比如阻塞隊列。如果同步方法適合你的程序层扶,那么請盡量的使用同步方法靴庆,他可以減少編寫代碼的數(shù)量,減少出錯的幾率怒医,如果特別需要使用Lock/Condition結構提供的獨有特性時炉抒,才使用Lock/Condition。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末稚叹,一起剝皮案震驚了整個濱河市焰薄,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌扒袖,老刑警劉巖塞茅,帶你破解...
    沈念sama閱讀 221,576評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異季率,居然都是意外死亡野瘦,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,515評論 3 399
  • 文/潘曉璐 我一進店門飒泻,熙熙樓的掌柜王于貴愁眉苦臉地迎上來鞭光,“玉大人,你說我怎么就攤上這事泞遗《栊恚” “怎么了?”我有些...
    開封第一講書人閱讀 168,017評論 0 360
  • 文/不壞的土叔 我叫張陵史辙,是天一觀的道長汹买。 經(jīng)常有香客問我佩伤,道長,這世上最難降的妖魔是什么晦毙? 我笑而不...
    開封第一講書人閱讀 59,626評論 1 296
  • 正文 為了忘掉前任生巡,我火速辦了婚禮,結果婚禮上见妒,老公的妹妹穿的比我還像新娘障斋。我一直安慰自己,他們只是感情好徐鹤,可當我...
    茶點故事閱讀 68,625評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著邀层,像睡著了一般返敬。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上寥院,一...
    開封第一講書人閱讀 52,255評論 1 308
  • 那天劲赠,我揣著相機與錄音,去河邊找鬼秸谢。 笑死凛澎,一個胖子當著我的面吹牛,可吹牛的內容都是我干的估蹄。 我是一名探鬼主播塑煎,決...
    沈念sama閱讀 40,825評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼臭蚁!你這毒婦竟也來了最铁?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,729評論 0 276
  • 序言:老撾萬榮一對情侶失蹤垮兑,失蹤者是張志新(化名)和其女友劉穎冷尉,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體系枪,經(jīng)...
    沈念sama閱讀 46,271評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡雀哨,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,363評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了私爷。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片雾棺。...
    茶點故事閱讀 40,498評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖衬浑,靈堂內的尸體忽然破棺而出垢村,到底是詐尸還是另有隱情,我是刑警寧澤嚎卫,帶...
    沈念sama閱讀 36,183評論 5 350
  • 正文 年R本政府宣布嘉栓,位于F島的核電站宏榕,受9級特大地震影響,放射性物質發(fā)生泄漏侵佃。R本人自食惡果不足惜麻昼,卻給世界環(huán)境...
    茶點故事閱讀 41,867評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望馋辈。 院中可真熱鬧抚芦,春花似錦、人聲如沸迈螟。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,338評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽答毫。三九已至褥民,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間洗搂,已是汗流浹背消返。 一陣腳步聲響...
    開封第一講書人閱讀 33,458評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留耘拇,地道東北人撵颊。 一個月前我還...
    沈念sama閱讀 48,906評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像惫叛,于是被迫代替她去往敵國和親倡勇。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,507評論 2 359

推薦閱讀更多精彩內容