(六)、Java 多線程——線程安全問題

1、線程安全問題的出現

在大多數的多線程應用程序中,兩個或者兩個以上的線程需要共享對同一數據的存取。這時可能發生多線程同時修改共享變量的情況,以在銀行取錢來說,可以分爲一下幾個步驟:
1. 輸入卡號和密碼,系統判斷是否匹配並有效
2. 用戶輸入支取金額
3. 系統判斷賬戶可用餘額是否足夠支取
4. 如果滿足支取條件則取款並更新餘額,否則取款失敗
我們使用兩個線程來同時模擬取款操作:

public class Account {

        private String acctNo;
        private double balance;

        //getter/setter
        //有參構造方法
}
public class GetMoney extends Thread {

    private Account account;
    private double tranAmt; //支取金額

    public GetMoney(String name, Account account, double tranAmt) {
        super(name);
        this.account = account;
        this.tranAmt = tranAmt;
    }

    @Override
    public void run() {
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if (account.getBalance() >= tranAmt) {
            //更新餘額
            account.setBalance(account.getBalance() - tranAmt);
            System.out.println(getName() + " 餘額爲 : " + account.getBalance());
        } else {
            System.out.println(getName() + "賬戶餘額不足,支取失敗!");
        }

    }

    public static void main(String[] args) {
        Account account = new Account("3303214000000007654", 1000);
        new GetMoney("A", account, 400).start();
        new GetMoney("B", account, 400).start();
    }

}

較大概率出現如下輸出結果:

B 餘額爲 : 200.0
A 餘額爲 : 200.0

運行結果並不是我們希望的:

A 餘額爲 : 600.0
b 餘額爲 : 200.0

在多線程的環境下,如果一個共享資源(取款操作中的賬戶餘額balance)被多個線程同時訪問,可能會出現意向不到的情況。特定場景的分析見我的另一篇文章線程安全問題

出現這類問題的原因大多數是因爲單個操作的顆粒度較小,例如取款中:①、獲取賬戶餘額;②、判斷餘額是否充足;③、更新餘額。這明顯是三個獨立的操作。可以使用同步機制將顆粒度較小的原子操作包裹成顆粒度較大的操作。

2、同步代碼塊

爲了解決線程安全問題,Java引入同步代碼:

synchronized(obj){
    //需要同步的操作
}

任何時刻只能有一個線程能夠獲得obj資源並進入同步代碼塊進行操作,當同步代碼塊執行完畢後,該線程會釋放obj資源。通常使用多線程共享的資源充當同步代碼塊中的“鎖對象”。 例如取錢的例子中應該使用賬戶account充當“鎖對象”。我們將上例修改爲:

@Override
    public void run() {
        synchronized (account) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (account.getBalance() >= tranAmt) {
                //更新餘額
                account.setBalance(account.getBalance() - tranAmt);
                System.out.println(getName() + " 餘額爲 : " + account.getBalance());
            } else {
                System.out.println(getName() + "賬戶餘額不足,支取失敗!");
            }
        }

    }

使用synchronized將取款的邏輯包裹起來,任何線程進入run方法時都會試圖獲取account“鎖對象”,如果某個線程獲取到了“鎖對象”,它就可以執行取款操作,其餘線程由於不能獲取“鎖對象”,只能等待那個線程執行完同步代碼塊中的代碼後釋放鎖。
添加同步代碼塊後程序總會輸出:

A 餘額爲 : 600.0
B 餘額爲 : 200.0

3、同步方法

同步方法就是使用synchronized關鍵字來修飾某個方法,對於同步方法而言,無需顯示地聲明“鎖對象”,它的“鎖對象”就是對象本身(this)。

package com.xiaopeng.multthread;

public class AccountSyn {

    private String acctNo;
    private double balance;

    public AccountSyn(String acctNo, double balance) {
        this.acctNo = acctNo;
        this.balance = balance;
    }

    public String getAcctNo() {
        return acctNo;
    }

    public double getBalance() {
        return balance;
    }

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

    //同步方法
    public synchronized void draw(double tranAmt) {
        if (balance >= tranAmt) {
            System.out.println(Thread.currentThread().getName() + " 取款 :" + tranAmt + "元");
            balance -= tranAmt;
            System.out.println(Thread.currentThread().getName() + " 餘額爲 :" + balance + "元");
        } else {
            System.out.println("賬戶餘額不足");
        }
    }

}

使用同步方法可以實現線程安全的類,它們具有以下特徵:

  1. 該類的每個對象都可以被多線程訪問
  2. 任意線程調用該對象的任意方法都可以得到正確的輸出
  3. 線程調用之後,該對象依舊保存正常狀態

增加同步的注意點:

  1. 代碼同步後,同一時間點只能有一個線程對其中的任務進行訪問,這會明顯降低程序的運行效率,所以應該只對必要的邏輯進行同步操作。
  2. 如果某段代碼會運行在單線程和多線程的環境中,那麼應該提供兩種版本同時保證單線程中的效率以及多線程中的安全。例如 StringBuffer 保證了多線程中的安全性, StringBuilder 保證了單線程中的高效率。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章