1. 程式人生 > >讀書筆記:java多執行緒之執行緒同步

讀書筆記:java多執行緒之執行緒同步

閱讀的書籍:《java瘋狂講義》

關鍵詞:執行緒安全問題,同步程式碼塊,同步方法,釋放同步監視器的鎖定,同步鎖,死鎖

執行緒安全問題:當使用多個執行緒來訪問同一個資料時,會導致一些錯誤情況的發生

到底什麼是執行緒安全問題呢,先看一個經典的案例:銀行取錢的問題

模擬步驟:

1.匹配使用者賬戶的正確性(這裡就簡化了)

2.使用者輸入取款金額

3.系統判斷賬戶餘額是否大於取款金額

4.返回取款成功或者失敗

案例中一共就三個類,首先是Account:

/**
 * 銀行賬戶
 */
public class Account {

    private String accountNo;//賬戶名
    private double balance;//賬戶餘額

    public Account(String accountNo,double balance){
        this.accountNo = accountNo;
        this.balance = balance;
    }

    @Override
    public int hashCode() {
        return accountNo.hashCode();
    }

    @Override
    public boolean equals(Object obj) {
        if(obj == this){
            return true;
        }

        if (obj != null && obj.getClass() == Account.class){
            Account target = (Account) obj;
            return target.getAccountNo().equals(accountNo);
        }

        return false;

    }

    public String getAccountNo() {
        return accountNo;
    }

    public void setAccountNo(String accountNo) {
        this.accountNo = accountNo;
    }

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }
}

 然後是DrawThread:

/**
 * 模擬取錢過程的執行緒
 */
public class DrawThread extends Thread {

    private Account mAccount;//賬戶資訊
    private double mDrawAmount;//要取的金額

    public DrawThread(String name, Account account, double drawAmount) {
        super(name);
        mAccount = account;
        mDrawAmount = drawAmount;
    }

    @Override
    public void run() {

        if (mAccount.getBalance() >= mDrawAmount) {//如果 餘額 >= 要取的數額,則進入取錢流程
            System.out.println(getName() + "取錢成功,取出金額:" + mDrawAmount);
            mAccount.setBalance(mAccount.getBalance() - mDrawAmount);//更新賬戶餘額
            System.out.println("賬戶餘額:" + mAccount.getBalance());
        } else {
            System.out.println(getName() + "取錢失敗,餘額不足");
        }
    }
}

最後用DrawThread進行測試:

public class DrawTest {

    public static void main(String[] args){
        Account account = new Account("王尼瑪",1000);
        new DrawThread("小偷甲",account,800).start();
        new DrawThread("小偷乙",account,800).start();
    }

}

如果只是按上面的程式碼來執行的話,執行結果是這樣的:

可以看到,這裡就存在著一個問題:一開始賬戶中有1000塊錢,小偷甲取走800後剩下200,若小偷乙繼續取的話餘額就不夠了,可是這裡卻沒有返回 “取錢失敗,餘額不足”,而是輸出了一個負數:-600。兩個併發執行緒同時在修改Account物件,出現了問題。

這就是執行緒安全問題的一個體現。那麼如何解決呢?——使用同步監視器

同步程式碼塊:使用同步監視器的通用方法就是同步程式碼塊

語法格式:

synchronized(obj){

         //需要同步的程式碼塊

}

執行緒在開始執行同步程式碼塊之前,必須先獲得對同步監視器的鎖定。

任何時刻只能由一個執行緒可以獲得對同步監視器的鎖定,當同步程式碼塊執行完成之後,該執行緒會釋放對該同步監視器的鎖定

明白了這個原理之後,對DrawThread中的run()方法小改一下:

@Override
    public void run() {

        synchronized (mAccount) {//看這裡
            if (mAccount.getBalance() >= mDrawAmount) {//如果 餘額 >= 要取的數額,則進入取錢流程
                System.out.println(getName() + "取錢成功,取出金額:" + mDrawAmount);
                mAccount.setBalance(mAccount.getBalance() - mDrawAmount);//更新賬戶餘額
                System.out.println("賬戶餘額:" + mAccount.getBalance());
            } else {
                System.out.println(getName() + "取錢失敗,餘額不足");
            }
        }
    }

可以看到,此時取錢的這塊邏輯就是同步程式碼塊,mAccount就是同步監視器了,取錢就是對mAccount進行操作,而經過這麼一包裹之後,對於同一個賬戶來說,同一時間就只能有一個人對它進行取錢的操作了。再看看此時輸出的結果:

嗯,小偷乙這會兒就取錢失敗了,問題終於得到了解決。

同步方法:使用synchronized修飾某個方法來達到多執行緒安全

同步方法的同步監視器是呼叫該方法的物件,通過使用同步方法可以很方便地實現執行緒安全的類

執行緒安全的類的特點:

  • 該類的物件可以被多個執行緒安全地訪問
  • 每個執行緒呼叫該物件的任意方法之後都將得到正確的結果,並且該物件狀態依然保持合理狀態

用同步方法對案例程式碼也進行小改:

    //在Account類中加入這個方法
    public synchronized void draw(double drawAmount){

        if (getBalance() >= drawAmount) {//如果 餘額 >= 要取的數額,則進入取錢流程
            System.out.println(Thread.currentThread().getName() + "取錢成功,取出金額:" + drawAmount);
            setBalance(getBalance() - drawAmount);//更新賬戶餘額
            System.out.println("賬戶餘額:" + getBalance());
        } else {
            System.out.println(Thread.currentThread().getName() + "取錢失敗,餘額不足");
        }

    }

然後回到DrawThread中呼叫:

 @Override
    public void run() {
        mAccount.draw(mDrawAmount);
    }

此時輸出結果也是正確的,就不再重複貼圖了

釋放同步監視器的鎖定:執行緒會在如下幾種情況釋放對同步監視器的鎖定

  • 當前執行緒的同步程式碼塊,同步方法正常執行結束
  • 遇到break,return終止了該同步程式碼塊,同步方法的執行
  • 出現未處理的Error或Exception,該方法異常結束
  • 程式執行了同步監視器物件的wait()方法

與釋放對應的是,當執行緒執行同步程式碼塊,同步方法時,如下幾種情況執行緒是不會釋放同步監視器的:

  • 程式呼叫Thread.sleep(),Thread.yield()
  • 其他程式呼叫了該執行緒的suspend()方法

同步鎖:通過顯示定義同步鎖物件(Lock)來實現同步

Lock是控制多個執行緒對共享資源進行訪問的工具,通常每次只能由一個執行緒對Lock物件加鎖,執行緒開始訪問共享資源之前先要獲得Lock物件

之所以說通常,是因為某些鎖可能允許對共享資源併發訪問,如ReadWriteLock

在實現執行緒安全的控制中,比較常用的是ReentrantLock(可重入鎖)

死鎖:當兩個執行緒互相等待對方釋放同步監視器時就會發生死鎖,導致所有執行緒處於阻塞狀態,無法繼續

 由於篇幅有限,關於死鎖就不展開了,後面會專門寫一篇關於死鎖的筆記或文章

注意事項:

  1. synchronized關鍵字可以修飾方法和程式碼塊,但不能修飾構造器和成員變數等
  2. Thread類的suspend()方法很容易導致死鎖,故Java不推薦使用這個方法