Java 線程同步 鎖 條件變量

1. 死鎖的產(chǎn)生條件

計算機(jī)系統(tǒng)中同時具備下面四個必要條件時谴麦,那么會發(fā)生死鎖</br>

  1. 互斥條件缀皱。即某個資源在一段時間內(nèi)只能由一個進(jìn)程占有,不能同時被兩個或兩個以上的進(jìn)程占有。這種獨占資源如CD-ROM驅(qū)動器洲拇,打印機(jī)等等奈揍,必須在占有該資源的進(jìn)程主動釋放它之后,其它進(jìn)程才能占有該資源赋续。這是由資源本身的屬性所決定的打月。</br>
  2. 不可搶占條件。進(jìn)程所獲得的資源在未使用完畢之前蚕捉,資源申請者不能強(qiáng)行地從資源占有者手中奪取資源奏篙,而只能由該資源的占有者進(jìn)程自行釋放。</br>
  3. 占有且申請條件迫淹。進(jìn)程至少已經(jīng)占有一個資源秘通,但又申請新的資源;由于該資源已被另外進(jìn)程占有敛熬,此時該進(jìn)程阻塞肺稀;但是,它在等待新資源之時应民,仍繼續(xù)占用已占有的資源话原。</br>
  4. 循環(huán)等待條件。存在一個進(jìn)程等待序列{P1诲锹,P2繁仁,...,Pn}归园,其中P1等待P2所占有的某一資源黄虱,P2等待P3所占有的某一源,......庸诱,而Pn等待P1所占有的的某一資源捻浦,形成一個進(jìn)程循環(huán)等待環(huán)。</br>

當(dāng)程序存在競爭條件時桥爽,需要同步朱灿,避免出現(xiàn)不合預(yù)期的運行結(jié)果。同步實現(xiàn)的兩個工具:鎖和條件狀態(tài)钠四。</br>
以銀行存取款為例盗扒,如果沒有采取同步操作</br>

Code1
public class Bank {
    private final double[] accounts;

    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 , double amount){
        if (accounts[from] < amount ) {
            return;
        }
        System.out.println(Thread.currentThread());
        accounts[from] -= amount;
        System.out.printf(" 10.2f from %d to %d", amount, from, to);
        accounts[to]  += amount;
        System.out.printf(" Total Balance: %10.2f%n", getTotalBalance();
    }

    public double getTotalBalance(){
        double sum = 0;
        for (double a : accounts){
                sum += a;
            }
            return sum;
        } 
    }

    public int size(){
        return accounts.length;
    }
}

如果存在兩個線程同時執(zhí)行指令

accounts[to]  += amount;

由于指令不是原子操作,該指令可能被處理為

  1. 將accounts[to]加載到寄存器</br>
  2. 增加amount </br>
  3. 將結(jié)果寫回account[to]</br>
    在線程1執(zhí)行完步驟1形导,2還沒有執(zhí)行步驟3的時候环疼,即只是在寄存器中增加了amount,線程1被剝奪了運行權(quán)限朵耕,處理器將運行權(quán)限交給了線程2,線程2執(zhí)行步驟1淋叶,2阎曹,還沒有執(zhí)行步驟3,即線程2獲取和線程1擁有一樣的初始值,并且只是在寄存器中增加了amount值处嫌,這時候處理器又將時間片給了線程1栅贴,線程1將計算后的值寫入內(nèi)存,而當(dāng)時間片繼續(xù)轉(zhuǎn)給線程2的時候熏迹,仍然是在和線程一樣的初始值上增加amount檐薯,這種情況下,則擦去了線程2所做的更新注暗。

2. ReentrantLock可重入鎖

可重入鎖:是一種特殊的互斥鎖坛缕,可以被同一個線程多次獲取,而不會產(chǎn)生死鎖捆昏。具有兩個特點:</br>
1.是互斥的赚楚,任意時刻,只有一個線程鎖骗卜,假設(shè)A線程已經(jīng)獲取了鎖宠页,在A線程釋放這個鎖之前,B線程無法獲取到寇仓。</br>
2.它可以被同一線程多次持有举户,即假設(shè)A線程已經(jīng)獲取了這個鎖,如果A線程在釋放這個鎖前又一次請求獲取這個鎖遍烦,能夠獲取成功</br>
鎖持有一個計數(shù)器敛摘,來跟蹤lock方法的嵌套調(diào)用。如下代碼乳愉,transfer調(diào)用getTotalBalance方法兄淫,也會封鎖bankLock對象,此時bankLock對象的持有計數(shù)為2蔓姚。當(dāng)getTotalBalance方法退出時捕虽,持有計數(shù)變回1。當(dāng)transfer退出時坡脐,持有計數(shù)變?yōu)?泄私。線程鎖釋放。</br>

Code2
public class Bank {
    private final double[] accounts;
    private Lock bankLock = new ReentrantLock();

    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 , double amount){
        bankLock.lock();
        try {
             if (accounts[from] < amount ) {
                    return;
            }
            System.out.println(Thread.currentThread());
            accounts[from] -= amount;
            System.out.printf(" 10.2f from %d to %d", amount, from, to);
            accounts[to]  += amount;
            System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
        } finally {
            bankLock.unlock();
        }
    }

    public double getTotalBalance(){
        bankLock.lock();
        try {
            double sum = 0;

            for (double a : accounts){
                sum += a;
            }
            return sum;
        } finally {
            bankLock.unlock();
        }
    }

    public int size(){
        return accounts.length;
    }
}
package java.util.concurrent.locks.Lock
//獲取這個鎖备闲,如果鎖同時被另一個線程擁有則發(fā)生阻塞
void lock():
//釋放這個鎖
void unlock();
package java.util.concurrent.locks.ReentrantLock
//構(gòu)建一個可以被用來保護(hù)臨界區(qū)的可重入鎖
ReentrantLock();
//構(gòu)建一個帶有公平策略的鎖晌端。一個公平鎖偏愛等待時間最長的線程。但是恬砂,這一公平的保證將大大降低性能咧纠,所以默認(rèn)情況下,鎖沒有被強(qiáng)制為公平的泻骤。
ReentrantLock(boolean fair);

3 條件對象(條件變量)

使用場景:線程進(jìn)入臨界區(qū)漆羔,卻發(fā)現(xiàn)在某一條件滿足之后它才能執(zhí)行梧奢。要使用一個條件對象對象來管理那些已經(jīng)獲得了一個鎖卻不能做有用工作的線程。</br>
銀行賬戶需要轉(zhuǎn)賬演痒,賬戶內(nèi)只有500元卻需要轉(zhuǎn)600元亲轨,即賬戶中沒有足夠的余額,應(yīng)該怎么辦呢鸟顺?現(xiàn)實情況下惦蚊,銀行柜員會告訴你賬戶余額不足,無法辦理讯嫂,直接退出蹦锋。或者端姚,我們可以等待另一個線程賬戶注入100元及以上的金額晕粪。</br>
當(dāng)transfer方法寫成如下

Code3
public void transfer(int from, int to, int amount){
    banklock.lock();
    try{
        while(accounts[from] < amount){
            //wait... 這里采取等待,而不是立即返回
        }
        //transfer funds...
    }finally{
        banklock.unlock();
    }
}

可以看出這個線程剛剛獲得了對banklock的排他性訪問渐裸,因此別的線程沒有進(jìn)行存取操作的機(jī)會巫湘。所以這是需要條件對象的原因。</br>
一個鎖對象可以有一個或多個相關(guān)的條件對象昏鹃,可以用newCondition方法獲得一個條件對象尚氛。習(xí)慣上給每一個條件對象命名為可以反映它所表達(dá)的條件的名字。</br>

Code4
public class Bank {
    private final double[] accounts;
    private Lock bankLock;
    private Condition sufficientFunds;

    public Bank(int n, double initialBalance) {
        accounts = new double[n];
        for (int i = 0; i < accounts.length; i++) {
            accounts[i] = initialBalance;
        }
        //一個bank對象擁有一個ReentrantLock
        bankLock = new ReentrantLock();
        sufficientFunds = bankLock.newCondition();
    }

    public void transfer(int from, int to, double amount) {
        bankLock.lock();

        try {
            while (accounts[from] < amount) {
                //當(dāng)前線程被阻塞洞渤,并且放棄了鎖阅嘶,并且該線程進(jìn)入該條件的等待集
                sufficientFunds.await();
            }
            System.out.println(Thread.currentThread());
            accounts[from] -= amount;
            System.out.printf(" 10.2f from %d to %d", amount, from, to);
            accounts[to] += amount;
            System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
            //重新激活因為這一條件而等待的所有線程。這些線程中等待集中移出時载迄,它們再次成為可運行的讯柔,調(diào)度器將再次激活它們
            //同時,它們將試圖重新進(jìn)入該對象
            //一旦鎖成為可用的护昧,它們中的某一個將從await調(diào)用返回魂迄,獲得該鎖并且從被阻塞的地方繼續(xù)執(zhí)行
            //采用循環(huán)while表明此時線程應(yīng)該再次檢測該條件。由于無法確保該條件被滿足惋耙,signalAll方法僅僅是通知正在等待的線程
            //siganlAll語義可以理解為:此時有可能已經(jīng)滿足條件捣炬,值得再次去檢測條件
            sufficientFunds.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            bankLock.unlock();
        }

    }

    public double getTotalBalance() {
        bankLock.lock();
        try {
            double sum = 0;
            for (double a : accounts) {
                sum += a;
            }
            return sum;
        } finally {
            bankLock.unlock();
        }
    }

    public int size() {
        return accounts.length;
    }
}

Condition.signal():是隨機(jī)解除等待集中某個線程的阻塞狀態(tài)。這比解除所有線程的等待狀態(tài)效率要高绽榛,但是存在危險湿酸。如果隨機(jī)選擇的線程發(fā)現(xiàn)自己仍然不能運行,那么它再次被阻塞灭美。如果沒有其他線程再次調(diào)用signal推溃,那么系統(tǒng)就死鎖了。

package java.util.concurrent.locks.Lock
//返回一個與該鎖相關(guān)的條件對象
Condition new Condition();
e.g. Condition sufficientFunds = bankLock.newCondition();
package java.util.concurrent.locks.Condition
//將線程放到條件的等待集合中
void await();
//解除該條件的等待集中的所有線程的阻塞狀態(tài)
void signalAll();
//從該條件的等待集中隨機(jī)地選擇一個線程冲粤,解除其阻塞狀態(tài)
void Signal();

4 鎖與條件對象的關(guān)鍵之處

*. 鎖可以用來保護(hù)代碼片段美莫,任何時刻只能有一個線程執(zhí)行被保護(hù)的代碼页眯。</br>
*. 鎖可以管理試圖進(jìn)入被保護(hù)代碼段的線程
*. 鎖可以擁有一個或多個相關(guān)的條件對象</br>
*. 每個條件對象管理那些已經(jīng)進(jìn)入被保護(hù)的代碼段但還不能運行的線程

5 synchronized 關(guān)鍵字

每一對象有一個內(nèi)部鎖梯捕,并且該鎖有一個內(nèi)部條件厢呵。由鎖來管理那些試圖進(jìn)入synchronized方法的線程,由條件來管理那些調(diào)用wait的線程傀顾。</br>

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

    public synchronized void transfer(int from, int to, double amount) {

        try {
            while (accounts[from] < amount) {
                //將線程添加到一個線程等待集中襟铭,該方法只能在一個同步方法中調(diào)用方法中調(diào)用
                wait();
            }
            System.out.println(Thread.currentThread());
            accounts[from] -= amount;
            System.out.printf(" 10.2f from %d to %d", amount, from, to);
            accounts[to] += amount;
            System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
            //notifyAll/notify方法解除等待線程的阻塞狀態(tài),該方法只能在同步方法或者同步塊中調(diào)用
            notifyAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public synchronized double getTotalBalance() {
        double sum = 0;
        for (double a : accounts) {
            sum += a;
        }
        return sum;
    }

    public int size() {
        return accounts.length;
    }
}

將靜態(tài)方法聲明為synchronized也是合法的短曾,如果調(diào)用這種方法寒砖,該方法獲得相關(guān)的類對象的內(nèi)部鎖。如果Bank類有一個靜態(tài)同步的方法嫉拐,那么當(dāng)該方法被調(diào)用時哩都,Bank.class對象的鎖被鎖住。因此婉徘,沒有其他線程可以調(diào)用同一個類的這個或者任何其他的同步靜態(tài)方法漠嵌。

pulic class Bank
{
    private double[] accounts;
    private Object lock = new Object();
}

6 鎖和條件對象的局限

*. 不能中斷一個正在試圖獲得鎖的線程</br>
*. 試圖獲得鎖時不能設(shè)定超時
*. 每個鎖僅有單一的條件,可能是不夠的</br>
*. 最好既不是用Locl/Condition也不使用synchronized關(guān)鍵字
*. 如果synchronized關(guān)鍵字適合程序盖呼,那么請盡量使用它儒鹿,這樣可以減少編寫的代碼數(shù)量,減少出錯的幾率几晤。</br>
*. 如果特別需要Lock/Condition結(jié)構(gòu)提供的獨有特性時约炎,才使用Lock/Condition</br>

7 Volatile域

有時,僅僅為了讀寫一個或兩個實例域就使用同步蟹瘾,開銷過大圾浅。可以采用volatile關(guān)鍵字聲明域憾朴,該修飾詞告訴編譯器和虛擬機(jī)該域是可能被另一個線程并發(fā)更新的狸捕。它為實例域的同步訪問提供了一個種免鎖機(jī)制。

8 final變量

將域聲明為final伊脓,可以安全的訪問一個共享域府寒。

final Map<String, Double> accounts = new HashMap<>();

其他線程會在構(gòu)造函數(shù)完成構(gòu)造之后才看到這個accounts變量。如果不適用final报腔,就不能保證其他線程看到的是account更新后的值株搔,他們可能都只是看到null,而不是新構(gòu)造的HashMap纯蛾。當(dāng)然纤房,對這個映射表的操作不是線程安全的,如果多個線程在讀寫這個映射表翻诉,仍然需要進(jìn)行的炮姨。

學(xué)習(xí)資料:《Java核心技術(shù)卷一》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末捌刮,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子舒岸,更是在濱河造成了極大的恐慌绅作,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,884評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蛾派,死亡現(xiàn)場離奇詭異俄认,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)洪乍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,755評論 3 385
  • 文/潘曉璐 我一進(jìn)店門眯杏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人壳澳,你說我怎么就攤上這事岂贩。” “怎么了巷波?”我有些...
    開封第一講書人閱讀 158,369評論 0 348
  • 文/不壞的土叔 我叫張陵萎津,是天一觀的道長。 經(jīng)常有香客問我褥紫,道長姜性,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,799評論 1 285
  • 正文 為了忘掉前任髓考,我火速辦了婚禮部念,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘氨菇。我一直安慰自己儡炼,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,910評論 6 386
  • 文/花漫 我一把揭開白布查蓉。 她就那樣靜靜地躺著乌询,像睡著了一般。 火紅的嫁衣襯著肌膚如雪豌研。 梳的紋絲不亂的頭發(fā)上妹田,一...
    開封第一講書人閱讀 50,096評論 1 291
  • 那天,我揣著相機(jī)與錄音鹃共,去河邊找鬼鬼佣。 笑死,一個胖子當(dāng)著我的面吹牛霜浴,可吹牛的內(nèi)容都是我干的晶衷。 我是一名探鬼主播,決...
    沈念sama閱讀 39,159評論 3 411
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼晌纫!你這毒婦竟也來了税迷?” 一聲冷哼從身側(cè)響起滨攻,我...
    開封第一講書人閱讀 37,917評論 0 268
  • 序言:老撾萬榮一對情侶失蹤十嘿,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后钥飞,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體凌蔬,經(jīng)...
    沈念sama閱讀 44,360評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡露懒,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,673評論 2 327
  • 正文 我和宋清朗相戀三年闯冷,在試婚紗的時候發(fā)現(xiàn)自己被綠了砂心。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,814評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡蛇耀,死狀恐怖辩诞,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情纺涤,我是刑警寧澤译暂,帶...
    沈念sama閱讀 34,509評論 4 334
  • 正文 年R本政府宣布,位于F島的核電站撩炊,受9級特大地震影響外永,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜拧咳,卻給世界環(huán)境...
    茶點故事閱讀 40,156評論 3 317
  • 文/蒙蒙 一伯顶、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧骆膝,春花似錦祭衩、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至政钟,卻和暖如春路克,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背养交。 一陣腳步聲響...
    開封第一講書人閱讀 32,123評論 1 267
  • 我被黑心中介騙來泰國打工精算, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人层坠。 一個月前我還...
    沈念sama閱讀 46,641評論 2 362
  • 正文 我出身青樓殖妇,卻偏偏與公主長得像,于是被迫代替她去往敵國和親破花。 傳聞我的和親對象是個殘疾皇子谦趣,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,728評論 2 351

推薦閱讀更多精彩內(nèi)容