程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> Java線程機制(四) 同步方法和同步塊

Java線程機制(四) 同步方法和同步塊

編輯:關於JAVA

在之前例子的基礎上,我們增加新的功能:根據正確與不正確的響應來顯示玩家的分數。

public class ScoreLabel extends JLabel implements CharacterListener {
    private volatile int score = 0;
    private int char2type = -1;
    private CharacterSource generator = null, typist = null;
    
    public ScoreLabel(CharacterSource generator, CharacterSource typist) {
        this.generator = generator;
        this.typist = typist;
        if (generator != null) {
            generator.addCharacterListener(this);
        }
        if (typist != null) {
            typist.addCharacterListener(this);
        }
    }
    
    public ScoreLabel() {
        this(null, null);
    }
    
    public synchronized void resetGenerator(CharacterSource newCharactor) {
        if (generator != null) {
            generator.removeCharacterListener(this);
        }
        generator = newCharactor;
        if (generator != null) {
            generator.addCharacterListener(this);
        }
    }
    
    public synchronized void resetTypist(CharacterSource newTypist) {
        if (typist != null) {
            typist.removeCharacterListener(this);
            typist = newTypist;
        }
        if (typist != null) {
            typist.addCharacterListener(this);
        }
    }
    
    public synchronized void resetScore() {
        score = 0;
        char2type = -1;
        setScore();
    }
    
    private synchronized void setScore() {
        SwingUtilities.invokeLater(new Runnable() {
    
            @Override
            public void run() {
                setText(Integer.toString(score));
            }
        });
    }
    
    @Override
    public synchronized void newCharacter(CharacterEvent ce) {
        if (ce.source == generator) {
            if (char2type != -1) {
                score--;
                setScore();
            }
            char2type = ce.character;
        } else {
            if (char2type != ce.character) {
                score--;
            } else {
                score++;
                char2type = -1;
            }
            setScore();
        }
    }
}

這裡我們將newCharacter()方法用synchronized進行同步,是因為這個方法會被多個線程調用, 而我們根本就不知道哪個線程會在什麼時候調用這個方法。這就是race condition。
    變量的volatile無法解決上面的多線程調度問題,因為這裡的問題是方法調度的問題,而且更加可怕的是,需 要共享的變量不少,其中有些變量是作為條件判斷,這就會導致在這些條件變量沒有正確的設置前,有些線程 已經開始啟動了。

這並不是簡單的將這些變量設置為volatile就能解決的問題,因為就算這些變量的 狀態不對,其他線程依然能夠啟動。

這裡有幾個方法的同步是需要引起我們注意的:resetScore (),resetGenerator()和resetTypist()這幾個方法是在重新啟動時才會被調用,似乎我們不需要為此同步它們 :其他線程這時根本就沒有開始啟動!!

但是我們還是需要同步這些方法,這是一種防衛性的設計, 保證整個Class所有相關的方法都是線程安全的。遺憾的是,我們必須這樣考慮,因為多線程編程的最大問題 就是我們永遠也不知道我們的程序會出現什麼問題,所以,任何可能會引起線程不安全的因素我們都要盡量避 免。

這也就引出我們的問題:如何能夠對兩個不同的方法同步化以防止多個線程在調用這些方法的時 候影響對方呢?

對方法做同步化,能夠控制方法執行的順序,因為某個線程上已經運行的方法無法被 其他線程調用。這個機制的實現是由指定對象本身的lock來完成的,因為方法需要訪問的對象的lock被一個線 程占有,但值得注意的是,所謂的對象鎖其實並不是綁定在對象上,而是對象實例上,如果兩個線程擁有對象 的兩個實例,它們都可以同時訪問該對象,

同步的方法如何和沒有同步的方法共同執行呢?

所 有的同步方法都會執行獲取對象鎖的步驟,但是沒有同步的方法,也就是異步方法並不會這樣,所以它們能夠 在任意的時間點被任意的線程執行,而不管到底是否有同步方法在執行。

關於對象鎖的話題自然就會 引出一個疑問:靜態的同步方法呢?靜態的同步方法是無法獲取對象鎖的,因為它沒有this引用,對於它的調 用是不存在對象的。但靜態的同步方法的確是存在的,那麼它又是怎樣運作的呢?

這需要另一個鎖: 類鎖。

我們可以從對象實例上獲得鎖,也能從class(因為class對象的存在)上獲得鎖,即使這東西實 際上是不存在的,因為它無法實現,只是幫助我們理解的概念。值得注意的是,因為一個class只有一個class 對象,所以一個class只有一個線程可以執行同步的靜態方法,而且與對象的鎖毫無相關,類鎖可以再對象鎖 外被獨立的獲得和釋放,一個非靜態的同步方法如果調用同步的靜態方法,那麼它可以同時獲得這兩個鎖。

提供synchronized關鍵字的目的是為了讓對象中的方法能夠循序的進入,大部分數據保護的需求都可 以由這個關鍵字實現,但在更加復雜的同步化情況中還是太簡單了。

在java這個對象王國裡,難道真 的是沒有Lock這個對象的容身之處嗎?答案當然是不可能的,J2SE 5.0開始提供Lock這個接口:

private Lock scoreLock = new ReentrantLock();
    
public void newCharacter(CharacterEvent ce){
    if(ce.source == generator){
         try{
             scoreLock.lock();
             if(char2type != -1){
                  score--;
                  setScore();
             }
             char2type = ce.character;
         }finally{
             scoreLock.unlock();
         }
     }
     else{
         try{
             scoreLock.lock();
             if(char2type != ce.character){
                  score--;
             }
             else{
                 score++;
                 char2type = -1;
             }
             setScore();
         }finally{
              scoreLock.unlock();
         }
     }

Lock這個接口有兩個方法:lock()和unlock(),我們可以在開始的時候調用lock(),然後在 結束的時候調用unlock(),這樣就能有效的同步化這個方法。

我們可以看到,其實使用Lock接口只是為了 讓Lock更加容易被管理:我們可以存儲,傳遞,甚至是拋棄,其余和使用synchronized是一樣的,但更加靈活 :我們可以在有需要的時候才獲取和釋放鎖,因為lock不再依附於任何調用方法的對象,我們甚至可以讓兩個 對象共享同一個lock!也可以讓一個對象占有多個lock!!

使用Lock接口,是一種明確的加鎖機制, 之前我們的加鎖是我們無法掌握的,我們無法知道是哪個線程的哪個方法獲得鎖,但能確保同一時間只有一個 線程的一個方法獲得鎖,現在我們可以明確得的把握這個過程,靈活的設置lock scope,將一些耗時和具有線 程安全性的代碼移出lock scope,這樣我們就可以寫出高效而且線程安全的程序代碼,不用像之前一樣,為了 防止未知錯誤必須對所有相關方法進行同步。

使用lock接口,可以方便的利用它裡面提供的一些便利 的方法,像是tryLock(),它可以嘗試取得鎖,如果無法獲取,我們就可以執行其他操作,而不是浪費時間在 等待鎖的釋放。tryLock()還可以指定等待鎖的時間。

synchronized不僅可以同步方法,它還可以同步 一個程序塊:

public void newCharacter(CharacterEvent ce){
     if(ce.source == generator){
           synchronized(this)[
                  if(char2type != -1){
                        score--;
                        setScore();
                  }
                  char2type = ce.character;
            }
      }
      else{
            synchronized(this){
                  if(char2type != ce.character){
                        score--;
                  }
                  else{
                        score--;
                        char2type = -1;
                   }
                   setScore();
            }
      }
}

如果是為了縮小lock的范圍,我們依然還是可以使用synchronized而不是使用lock接口,而且這 種方式才是更加常見的,因為使用lock接口時我們需要創建新的對象,需要異常管理。我們可以lock住其他對 象,如被共享的數據對象。

選擇synchronized整個方法還是代碼塊,都沒有什麼問題,但lock scope還是 盡可能的越小越好。

考慮到newCharacter()這個方法裡面出現了策略選擇,我們可以對它進行重構:

private synchronized void newGeneratorCharacter(int c){
      if(char2type != -1){
            score--;
            setScore();
      }
      char2type = c;
}
    
private synchronized void newTpistCharacter(int c){
      if(char2type != c){
             score--;
      }
      else{
           score++;
           char2type = -1;
      }
      setScore();
}
    
public synchronized void newCharacter(CharacterEvent ce){
       if(ce.source == generator){
              newGeneratorCharacter(ce.character);
       }
       else{
            newTypistCharacter(ce.character);
       }
}

我們會注意到,兩種策略方法都要用synchronized鎖住,但真的有必要嗎?因為它們是private, 只會在該對象中使用,沒有理由要讓這些方法獲取鎖,因為它們也只會被對象內的synchronized方法調用,而 這時已經獲得鎖了。但是我們還是要這樣做,考慮到以後的開發者可能不知道調用這些方法之前需要獲取鎖的 情況。

由此可見,java的鎖機制遠比我們想象中要聰明:它並不是盲目的在進入 synchronized程序代碼塊時就開始獲取鎖,如果當前的線程已經獲得鎖,根本就沒有必要等到鎖被釋放還是去 獲取,只要讓synchronized程序段運行就可以。如果沒有獲取鎖,也就不會將它釋放掉。這種機制之所以能夠 運行是因為系統會保持追蹤遞歸取得lock的數目,最後會在第一個取得lock的方法或者代碼塊退出的時候釋放 鎖。

這就是所謂的nested lock。

之前我們使用的ReentrantLock同樣支持nested lock:如果 lock的請求是由當前占有lock的線程發出,內部的nested lock就會要求計數遞增,調用unlock()就會遞減, 直到計數為0就會釋放該鎖。但這個是ReentrantLock才具有的特性,其他實現了Lock這個接口的類並不具有。

nested lock是非常重要的,因為它有利於避免死鎖的發生。死鎖的發生遠比我們想象中要更常見,像 是方法間的相互調用,更加常見的情況就是回調,像是Swing編程中依賴事件處理程序與監聽者的窗口系統, 考慮一下監聽者經常變動的情況,同步簡直就是一個惡夢!!

Synchronized無法知道lock被遞歸調用 的次數,但是使用ReentrantLock可以做到這點。我們可以通過getHoldCount()方法來獲得當前線程對lock所 要求的數量,如果數量為0,代表當前線程並未持有鎖,但是還不能知道鎖是自由的,我們必須通過isLocked ()來判斷。我們還可以通過isHeldByCurrentThread()來判斷lock是否由當前的線程所持有,getQueueLength ()可以用來取得有多少個線程在等待取得該鎖,但這個只是預估值。

在多線程編程中經常講到死鎖, 但是即使沒有涉及到同步也有可能會產生死鎖。死鎖之所以是個問題,是因為它會讓程序無法正確的執行,更 加可怕的是,死鎖是很難被檢測的,特別是多線程編程往往都會是一個復雜的程序,它可能永遠也不會被發現 !!

更加悲哀的是,系統無法解決死鎖這種情況!

最後一個問題是關於公平的授予鎖。

我們知道,鎖是要被授予線程的,但是應該按照什麼依據來授予呢?是按照先到先得嗎?還是服務請 求最多?或者是對系統最有利的形式來授予?java的同步行為最接近第三種,因為同步並不是用來對特殊情況 授予鎖,它是通用的,所以沒有理由讓鎖按照到達的順序來授予,應該是由各實現所定義在底層線程系統的行 為所決定,但ReentrantLock提供了一種選項可以按照先進先出的順序獲取鎖:new ReentrantLock(true),這 是為了防止發生鎖饑餓的現象。

我們可以根據自己的具體實現來決定這種公平。

最後,我們來 總結一下:

1.對於同時涉及到靜態和非靜態方法的同步情況,使用lock對象更加容易,因為lock對象 無關於使用它的對象。

2.將整個方法同步化是最簡單的,但是這樣范圍會變大,讓確實沒有必要的程 序段無效率的持有鎖。

3.如果涉及到太多的對象,使用同步塊機制也是有問題的,同步塊無法解決跨 方法的鎖范圍。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved