Java併發編程(二)同步

1. 鎖對象

Synchronized

synchronized 關鍵字,代表這個方法加鎖,相當於不管哪一個線程(例如線程A),運行到這個方法時,都要檢查有沒有其它線程B(或者C、 D等)正在用這個方法(或者該類的其他同步方法),有的話要等正在使用synchronized方法的線程B(或者C 、D)運行完這個方法後再運行此線程A,沒有的話,鎖定調用者,然後直接運行。它包括兩種用法:synchronized 方法和 synchronized 塊。

Java語言的關鍵字,可用來給對象和方法或者代碼塊加鎖,當它鎖定一個方法或者一個代碼塊的時候,同一時刻最多隻有一個線程執行這段代碼。當兩個併發線程訪問同一個對象object中的這個加鎖同步代碼塊時,一個時間內只能有一個線程得到執行。另一個線程必須等待當前線程執行完這個代碼塊以後才能執行該代碼塊。然而,當一個線程訪問object的一個加鎖代碼塊時,另一個線程仍然可以訪問該object中的非加鎖代碼塊。

synchronized關鍵字自動提供了鎖以及相關的條件,大多數需要顯式鎖的情況使用synchronized非常的方便,但是等我們瞭解ReentrantLock類和條件對象時,我們能更好的理解synchronized關鍵字。ReentrantLock是JAVA SE 5.0引入的, 用ReentrantLock保護代碼塊的結構如下:

mLock.lock();
try{
...
}
finally{
mLock.unlock();
}

這一結構確保任何時刻只有一個線程進入臨界區,一旦一個線程封鎖了鎖對象,其他任何線程都無法通過lock語句。當其他線程調用lock時,它們則被阻塞直到第一個線程釋放鎖對象。把解鎖的操作放在finally中是十分必要的,如果在臨界區發生了異常,鎖是必須要釋放的,否則其他線程將會永遠阻塞。

2. 條件對象

進入臨界區時,卻發現在某一個條件滿足之後,它才能執行。要使用一個條件對象來管理那些已經獲得了一個鎖但是卻不能做有用工作的線程,條件對象又稱作條件變量。
我們來看看下面的例子來看看爲何需要條件對象

假設一個場景我們需要用銀行轉賬,我們首先寫了銀行的類,它的構造函數需要傳入賬戶數量和賬戶金額

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

接下來我們要提款,寫一個提款的方法,from是轉賬方,to是接收方,amount轉賬金額,結果我們發現轉賬方餘額不足,如果有其他線程給這個轉賬方再存足夠的錢就可以轉賬成功了,但是這個線程已經獲取了鎖,它具有排他性,別的線程也無法獲取鎖來進行存款操作,這就是我們需要引入條件對象的原因。

public void transfer(int from,int to,int amount){
     bankLock.lock();
     try{
         while (accounts[from]<amount){
             //wait
         }
     }finally {
         bankLock.unlock();
     }
 }

一個鎖對象擁有多個相關的條件對象,可以用newCondition方法獲得一個條件對象,我們得到條件對象後調用await方法,當前線程就被阻塞了並放棄了鎖

public class Bank {
private double[] accounts;
    private Lock bankLock;
    private Condition condition;
    public Bank(int n,double initialBalance){
        accounts=new double[n];
        bankLock=new ReentrantLock();
        //得到條件對象
        condition=bankLock.newCondition();
        for (int i=0;i<accounts.length;i++){
            accounts[i]=initialBalance;
        }
    }
    public void transfer(int from,int to,int amount) throws InterruptedException {
        bankLock.lock();
        try{
            while (accounts[from]<amount){
                //阻塞當前線程,並放棄鎖
                condition.await();
            }
        }finally {
            bankLock.unlock();
        }
    }
}

等待獲得鎖的線程和調用await方法的線程本質上是不同的,一旦一個線程調用的await方法,他就會進入該條件的等待集。當鎖可用時,該線程不能馬上解鎖,相反他處於阻塞狀態,直到另一個線程調用了同一個條件上的signalAll方法時爲止。當另一個線程準備轉賬給我們此前的轉賬方時,只要調用condition.signalAll();該調用會重新激活因爲這一條件而等待的所有線程。

當一個線程調用了await方法他沒法重新激活自身,並寄希望於其他線程來調用signalAll方法來激活自身,如果沒有其他線程來激活等待的線程,那麼就會產生死鎖現象,如果所有的其他線程都被阻塞,最後一個活動線程在解除其他線程阻塞狀態前調用await,那麼它也被阻塞,就沒有任何線程可以解除其他線程的阻塞,程序就被掛起了。
那何時調用signalAll呢?正常來說應該是有利於等待線程的方向改變時來調用signalAll。在這個例子裏就是,當一個賬戶餘額發生變化時,等待的線程應該有機會檢查餘額。

public void transfer(int from,int to,int amount) throws InterruptedException {
       bankLock.lock();
       try{
           while (accounts[from]<amount){
               //阻塞當前線程,並放棄鎖
               condition.await();
           }
           //轉賬的操作
           ...
           condition.signalAll();
       }finally {
           bankLock.unlock();
       }
   }

當調用signalAll方法時並不是立即激活一個等待線程,它僅僅解除了等待線程的阻塞,以便這些線程能夠在當前線程退出同步方法後,通過競爭實現對對象的訪問。還有一個方法是signal,它則是隨機解除某個線程的阻塞,如果該線程仍然不能運行,那麼則再次被阻塞,如果沒有其他線程再次調用signal,那麼系統就死鎖了。

3. Synchronized關鍵字

Lock和Condition接口爲程序設計人員提供了高度的鎖定控制,然而大多數情況下,並不需要那樣的控制,並且可以使用一種嵌入到java語言內部的機制。從Java1.0版開始,Java中的每一個對象都有一個內部鎖。如果一個方法用synchronized關鍵字聲明,那麼對象的鎖將保護整個方法。也就是說,要調用該方法,線程必須獲得內部的對象鎖。
換句話說,

public synchronized void method(){
}
等價於

public void method(){
this.lock.lock();
try{
}finally{
this.lock.unlock();
}

上面銀行的例子,我們可以將Bank類的transfer方法聲明爲synchronized,而不是使用一個顯示的鎖。
內部對象鎖只有一個相關條件,wait方法添加到一個線程到等待集中,notifyAll或者notify方法解除等待線程的阻塞狀態。也就是說wait相當於調用condition.await(),notifyAll等價於condition.signalAll();

我們上面的例子transfer方法也可以這樣寫:

public synchronized void transfer(int from,int to,int amount)throws InterruptedException{
    while (accounts[from]<amount) {
        wait();
    }
    //轉賬的操作
    ...
    notifyAll();   
    }

可以看到使用synchronized關鍵字來編寫代碼要簡潔很多,當然要理解這一代碼,你必須要了解每一個對象有一個內部鎖,並且該鎖有一個內部條件。由鎖來管理那些試圖進入synchronized方法的線程,由條件來管理那些調用wait的線程。

4. 同步阻塞

上面我們說過,每一個Java對象都有一個鎖,線程可以調用同步方法來獲得鎖,還有另一種機制可以獲得鎖,通過進入一個同步阻塞,當線程進入如下形式的阻塞:

synchronized(obj){
}

於是他獲得了obj的鎖。再來看看Bank類

public class Bank {
private double[] accounts;
private Object lock=new Object();
   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,int amount){
        synchronized(lock){
          //轉賬的操作
            ...
        }
    }
}

在此,lock對象創建僅僅是用來使用每個Java對象持有的鎖。有時開發人員使用一個對象的鎖來實現額外的原子操作,稱爲客戶端鎖定。例如Vector類,它的方法是同步的。現在假設在Vector中存儲銀行餘額

  public void transfer(Vector<Double>accounts,int from,int to,int amount){
  accounts.set(from,accounts.get(from)-amount);
  accounts.set(to,accounts.get(to)+amount;
}

Vecror類的get和set方法是同步的,但是這並未對我們有所幫助。在第一次對get調用完成以後,一個線程完全可能在transfer方法中被被剝奪運行權,於是另一個線程可能在相同的存儲位置存入了不同的值,但是,我們可以截獲這個鎖

  public void transfer(Vector<Double>accounts,int from,int to,int amount){
  synchronized(accounts){
  accounts.set(from,accounts.get(from)-amount);
  accounts.set(to,accounts.get(to)+amount;
  }
}

客戶端鎖定(同步代碼塊)是非常脆弱的,通常不推薦使用,一般實現同步最好用java.util.concurrent包下提供的類,比如阻塞隊列。如果同步方法適合你的程序,那麼請儘量的使用同步方法,他可以減少編寫代碼的數量,減少出錯的機率,如果特別需要使用Lock/Condition結構提供的獨有特性時,才使用Lock/Condition。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章