Java 線程同步 鎖 條件變量

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

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

  1. 互斥條件。即某個(gè)資源在一段時(shí)間內(nèi)只能由一個(gè)進(jìn)程占有,不能同時(shí)被兩個(gè)或兩個(gè)以上的進(jìn)程占有。這種獨(dú)占資源如CD-ROM驅(qū)動(dòng)器,打印機(jī)等等,必須在占有該資源的進(jìn)程主動(dòng)釋放它之后,其它進(jìn)程才能占有該資源。這是由資源本身的屬性所決定的。</br>
  2. 不可搶占條件。進(jìn)程所獲得的資源在未使用完畢之前,資源申請(qǐng)者不能強(qiáng)行地從資源占有者手中奪取資源,而只能由該資源的占有者進(jìn)程自行釋放。</br>
  3. 占有且申請(qǐng)條件。進(jìn)程至少已經(jīng)占有一個(gè)資源,但又申請(qǐng)新的資源;由于該資源已被另外進(jìn)程占有,此時(shí)該進(jìn)程阻塞;但是,它在等待新資源之時(shí),仍繼續(xù)占用已占有的資源。</br>
  4. 循環(huán)等待條件。存在一個(gè)進(jìn)程等待序列{P1,P2,...,Pn},其中P1等待P2所占有的某一資源,P2等待P3所占有的某一源,......,而Pn等待P1所占有的的某一資源,形成一個(gè)進(jìn)程循環(huán)等待環(huán)。</br>

當(dāng)程序存在競(jìng)爭(zhēng)條件時(shí),需要同步,避免出現(xiàn)不合預(yù)期的運(yùn)行結(jié)果。同步實(shí)現(xiàn)的兩個(gè)工具:鎖和條件狀態(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;
    }
}

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

accounts[to]  += amount;

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

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

2. ReentrantLock可重入鎖

可重入鎖:是一種特殊的互斥鎖,可以被同一個(gè)線程多次獲取,而不會(huì)產(chǎn)生死鎖。具有兩個(gè)特點(diǎn):</br>
1.是互斥的,任意時(shí)刻,只有一個(gè)線程鎖,假設(shè)A線程已經(jīng)獲取了鎖,在A線程釋放這個(gè)鎖之前,B線程無(wú)法獲取到。</br>
2.它可以被同一線程多次持有,即假設(shè)A線程已經(jīng)獲取了這個(gè)鎖,如果A線程在釋放這個(gè)鎖前又一次請(qǐng)求獲取這個(gè)鎖,能夠獲取成功</br>
鎖持有一個(gè)計(jì)數(shù)器,來(lái)跟蹤lock方法的嵌套調(diào)用。如下代碼,transfer調(diào)用getTotalBalance方法,也會(huì)封鎖bankLock對(duì)象,此時(shí)bankLock對(duì)象的持有計(jì)數(shù)為2。當(dāng)getTotalBalance方法退出時(shí),持有計(jì)數(shù)變回1。當(dāng)transfer退出時(shí),持有計(jì)數(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
//獲取這個(gè)鎖,如果鎖同時(shí)被另一個(gè)線程擁有則發(fā)生阻塞
void lock():
//釋放這個(gè)鎖
void unlock();
package java.util.concurrent.locks.ReentrantLock
//構(gòu)建一個(gè)可以被用來(lái)保護(hù)臨界區(qū)的可重入鎖
ReentrantLock();
//構(gòu)建一個(gè)帶有公平策略的鎖。一個(gè)公平鎖偏愛等待時(shí)間最長(zhǎng)的線程。但是,這一公平的保證將大大降低性能,所以默認(rèn)情況下,鎖沒有被強(qiáng)制為公平的。
ReentrantLock(boolean fair);

3 條件對(duì)象(條件變量)

使用場(chǎng)景:線程進(jìn)入臨界區(qū),卻發(fā)現(xiàn)在某一條件滿足之后它才能執(zhí)行。要使用一個(gè)條件對(duì)象對(duì)象來(lái)管理那些已經(jīng)獲得了一個(gè)鎖卻不能做有用工作的線程。</br>
銀行賬戶需要轉(zhuǎn)賬,賬戶內(nèi)只有500元卻需要轉(zhuǎn)600元,即賬戶中沒有足夠的余額,應(yīng)該怎么辦呢?現(xiàn)實(shí)情況下,銀行柜員會(huì)告訴你賬戶余額不足,無(wú)法辦理,直接退出?;蛘撸覀兛梢缘却硪粋€(gè)線程賬戶注入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();
    }
}

可以看出這個(gè)線程剛剛獲得了對(duì)banklock的排他性訪問,因此別的線程沒有進(jìn)行存取操作的機(jī)會(huì)。所以這是需要條件對(duì)象的原因。</br>
一個(gè)鎖對(duì)象可以有一個(gè)或多個(gè)相關(guān)的條件對(duì)象,可以用newCondition方法獲得一個(gè)條件對(duì)象。習(xí)慣上給每一個(gè)條件對(duì)象命名為可以反映它所表達(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;
        }
        //一個(gè)bank對(duì)象擁有一個(gè)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());
            //重新激活因?yàn)檫@一條件而等待的所有線程。這些線程中等待集中移出時(shí),它們?cè)俅纬蔀榭蛇\(yùn)行的,調(diào)度器將再次激活它們
            //同時(shí),它們將試圖重新進(jìn)入該對(duì)象
            //一旦鎖成為可用的,它們中的某一個(gè)將從await調(diào)用返回,獲得該鎖并且從被阻塞的地方繼續(xù)執(zhí)行
            //采用循環(huán)while表明此時(shí)線程應(yīng)該再次檢測(cè)該條件。由于無(wú)法確保該條件被滿足,signalAll方法僅僅是通知正在等待的線程
            //siganlAll語(yǔ)義可以理解為:此時(shí)有可能已經(jīng)滿足條件,值得再次去檢測(cè)條件
            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ī)解除等待集中某個(gè)線程的阻塞狀態(tài)。這比解除所有線程的等待狀態(tài)效率要高,但是存在危險(xiǎn)。如果隨機(jī)選擇的線程發(fā)現(xiàn)自己仍然不能運(yùn)行,那么它再次被阻塞。如果沒有其他線程再次調(diào)用signal,那么系統(tǒng)就死鎖了。

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

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

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

5 synchronized 關(guān)鍵字

每一對(duì)象有一個(gè)內(nèi)部鎖,并且該鎖有一個(gè)內(nèi)部條件。由鎖來(lái)管理那些試圖進(jìn)入synchronized方法的線程,由條件來(lái)管理那些調(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) {
                //將線程添加到一個(gè)線程等待集中,該方法只能在一個(gè)同步方法中調(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)的類對(duì)象的內(nèi)部鎖。如果Bank類有一個(gè)靜態(tài)同步的方法,那么當(dāng)該方法被調(diào)用時(shí),Bank.class對(duì)象的鎖被鎖住。因此,沒有其他線程可以調(diào)用同一個(gè)類的這個(gè)或者任何其他的同步靜態(tài)方法。

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

6 鎖和條件對(duì)象的局限

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

7 Volatile域

有時(shí),僅僅為了讀寫一個(gè)或兩個(gè)實(shí)例域就使用同步,開銷過(guò)大??梢圆捎胿olatile關(guān)鍵字聲明域,該修飾詞告訴編譯器和虛擬機(jī)該域是可能被另一個(gè)線程并發(fā)更新的。它為實(shí)例域的同步訪問提供了一個(gè)種免鎖機(jī)制。

8 final變量

將域聲明為final,可以安全的訪問一個(gè)共享域。

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

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容