1. 程式人生 > >Java執行緒(二)

Java執行緒(二)

執行緒有它自己的生命週期,也就是五種狀態:被建立,執行(start()),臨時狀態、阻塞(具備執行資格,但沒有執行權),凍結(放棄了執行資格),消亡(stop()、run()方法執行完成)。這裡主要就是了解和掌握幾個函式的應用和區別,例如我之前的一篇部落格http://blog.csdn.net/lxjstudyit/article/details/52443872,sleep()和yield()方法區別。本篇主要探討執行緒同步問題。
其實執行緒安全問題,怎麼引發的,當多條執行緒併發修改共享資源就容易引發執行緒安全問題。
一個比較經典的執行緒安全問題就是–銀行取錢問題。
步驟:
1.使用者輸入賬戶、密碼,系統判斷使用者的賬戶、密碼是否匹配
2.使用者判斷取款金額
3.系統判斷賬戶餘額是否大於取款金額
4.如果餘額大於取款金額,則取款成功;如果餘額小於取款金額,則取款·失敗


按上面的流程編寫取款程式,並使用兩個執行緒來模擬取錢操作。模擬兩個人使用同一個賬戶並發現取錢問題。寫三個java檔案,下面先定義一個賬戶類,該賬戶封裝了賬戶編號和餘額兩個例項變數

public class Account {

    private String accountNo;//賬戶編號
    private double balance;//賬戶餘額
    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; } public Account(){} public Account(String accountNo,double balance) { this.accountNo = accountNo; this.balance = balance; } //下面兩個方法根據accountNo來重寫hashCode()和equals()方法 public int hashCode() { return
accountNo.hashCode(); } @Override public boolean equals(Object obj) { if(this == obj) return true; if(obj!=null && obj.getClass() == Account.class) { Account target = (Account) obj; } return false; } }

接下來提供一個取錢的執行緒類,該執行緒類根據執行賬戶、取錢數量進行取錢操作,取錢的邏輯是當其餘額不足時無法提取現金,當餘額足夠時由系統吐出鈔票,餘額減少。

public class DrawThread extends Thread{

    private Account account;//使用者賬戶
    private double drawAmount;//當前取錢執行緒所需要的錢數
    public DrawThread(String name,Account account,double drawAmount)
    {
        super(name);
        this.account = account;
        this.drawAmount = drawAmount;
    }

    public void run()
    {
        //賬戶餘額大於取錢數目
        if(account.getBalance() >= drawAmount)
        {
            System.out.println(getName()+"取錢成功!吐出鈔票:"+drawAmount);
            //修改餘額
            account.setBalance(account.getBalance() - drawAmount);
            System.out.println("\t餘額為:"+account.getBalance());
        }
        else{
            System.out.println(getName()+"取錢失敗!餘額不足!");
        }
    }
}

主程式僅僅是建立一個賬戶,並啟動兩個執行緒從該賬戶中提取。

public class DrawTest {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        //建立一個賬戶
        Account acct = new Account("123",1000);
        //模擬兩個賬戶對同一個賬戶收錢
        new DrawThread("甲",acct,800).start();
        new DrawThread("乙",acct,800).start();

    }
}

多次執行上面程式,很有可能看到如圖所示的結果
這裡寫圖片描述
這正式多執行緒程式設計突然出現的偶然錯誤–因為執行緒排程的不確定性。
Java的多執行緒支援引入了同步監視器來解決這個問題,使用同步監視器的通用方法就是同步程式碼塊。同步程式碼塊的語法格式如下;

synchronized(obj)
{
    ...
    //此處的程式碼就是同步程式碼塊
}

synchronized後括號裡的obj就是同步監視器,上面程式碼的含義是:執行緒開始執行同步程式碼塊之前,必須先獲得對同步監視器的鎖定。物件如同鎖,持有鎖的執行緒可以在同步中執行;沒有持有鎖的執行緒即使獲取cpu的執行權,也進不去,因為沒有獲取鎖。
選擇監視器的目的:阻止兩條執行緒對同一個共享資源進行併發訪問。因此通常推薦使用可能被併發訪問的共享資源充當同步監視器。對於上面的取錢模擬程式,我們應該考慮使用賬戶(account)作為同步監視器。

public class DrawThread extends Thread{

    private Account account;//使用者賬戶
    private double drawAmount;//當前取錢執行緒所需要的錢數
    public DrawThread(String name,Account account,double drawAmount)
    {
        super(name);
        this.account = account;
        this.drawAmount = drawAmount;
    }

    public void run()
    {
        // 使用account作為同步監視器,任何執行緒進入下面同步程式碼塊之前,
        // 必須先獲得對account賬戶的鎖定——其他執行緒無法獲得鎖,也就無法修改它
        // 這種做法符合:“加鎖 → 修改 → 釋放鎖”的邏輯
        synchronized(account)
        {
            //賬戶餘額大於取錢數目
            if(account.getBalance() >= drawAmount)
            {
                System.out.println(getName()+"取錢成功!吐出鈔票:"+drawAmount);
                //修改餘額
                account.setBalance(account.getBalance() - drawAmount);
                System.out.println("\t餘額為:"+account.getBalance());
            }
            else{
                System.out.println(getName()+"取錢失敗!餘額不足!");
            }
        }
    }   
}

通過這種方式就可以保證併發執行緒在任一時刻只有一個執行緒可以進入修改共享資源的程式碼區(也稱為臨界區),所以同一時刻最多一個執行緒處於臨界區,從而保證了執行緒的安全區。修改後,多次執行程式,總可以看到如下圖所示結果
這裡寫圖片描述
Java的多執行緒安全支援還提供了同步方法,同步方法就是使用synchronized關鍵字來修飾某個方法。對於synchronized修飾的例項方法(非static方法)而言,無須顯式指定同步監視器是this,也就是呼叫方法的物件。上面的Account類就是一個可變類,它的accountNo和balance兩個成員變數都可以被改變,當兩個執行緒同時修改Account物件的balance成員變數的值時,程式就出現了異常。下面將Account類對balance類的訪問設定成安全的,那麼只要把修改balance的方法變成同步方法即可。對Account.java檔案進行修改。

public class Account {

    private String accountNo;//賬戶編號
    private double balance;//賬戶餘額
    public String getAccountNo() {
        return accountNo;
    }
    public void setAccountNo(String accountNo) {
        this.accountNo = accountNo;
    }
    public double getBalance() {
        return balance;
    }

    public Account(){}
    public Account(String accountNo,double balance)
    {
        this.accountNo = accountNo;
        this.balance = balance;
    }
    //提供一個執行緒安全的draw()方法完成取錢操作
    public synchronized void draw(double drawAmount)
    {
        //賬戶餘額大於取錢數目
        if(balance >= drawAmount)
        {
            System.out.println(Thread.currentThread()+"取錢成功!吐出鈔票:"+drawAmount);
            //修改餘額
            balance -= drawAmount;
            System.out.println("\t餘額為:"+balance);
        }
        else{
            System.out.println(Thread.currentThread()+"取錢失敗!餘額不足!");
        }
    }
    //省略了hashCode()和equals(方法)

上面程式增加了一個代表取錢的draw方法,並使用了synchronized關鍵字修飾該方法,把該方法變成同步方法。該同步方法的同步監視器是this,因此對於同一個賬戶而言,任意時刻只能有一個執行緒獲得對Account物件的鎖定,然後進入draw()方法執行取錢操作–這樣可以保證多個執行緒併發取錢的執行緒安全。
因為Account類中已經提供了draw()方法,而且取消了setbalance()方法,DrawThread執行緒類需要改寫,該執行緒類的run()方法只要呼叫Account物件的draw()方法即可執行取錢操作。run()方法程式碼片段如下:

public void run()
    {
        // 直接呼叫account物件的draw方法來執行取錢
        // 同步方法的同步監視器是this,this代表呼叫draw()方法的物件。
        // 也就是說:執行緒進入draw()方法之前,必須先對account物件的加鎖。
        account.draw(drawAmount);

    }

上面的drawThread類無須實現取錢操作,而是直接呼叫account的draw()方法來執行取錢操作。由於已經使用synchronized關鍵字修飾了draw()方法,同步方法的監視器是this,而this總代表呼叫該方法的物件–在上面例項中,呼叫draw()方法的物件是account,因此多個執行緒併發修改同一份account之前,必須先對account物件加鎖,這也符合了”加鎖”-“修改”-“釋放鎖”的邏輯。


最後做個小總結:
同步的前提:
1. 必須要有兩個或者兩個以上的執行緒
2. 必須是多個執行緒使用同一個鎖
必須保證同步中只能有一個執行緒在執行。
好處:解決多執行緒的安全問題
弊端:多個執行緒都需要判斷鎖,較為消耗資源。

如何找程式是否有安全問題:
1. 明確哪些程式碼是多執行緒執行程式碼
2. 明確共享資料
3. 明確 多執行緒執行程式碼中哪些語句是操作共享資料的。

同步函式用的是哪個鎖呢?
函式需要被物件呼叫,那麼函式都有一個所屬物件引用,就是this,所以同步函式用的鎖是this。
拓展:
如果同步函式被static修飾後,使用的鎖是什麼?
用過驗證,發現不再是this,因為靜態方法中不可以定義this,靜態進記憶體時,記憶體中沒有本類 物件,但是一定有該類對應的位元組碼檔案物件,該物件的型別是class,靜態的同步方法,使用的鎖是該方法所在類的位元組碼檔案物件,類名.class