菜鳥初學Java的備忘錄(七) (轉)

worldblog發表於2007-12-15
菜鳥初學Java的備忘錄(七) (轉)[@more@]

年1月22日 星期三 晴

我突然發現還有很多東西需要我弄明白,比如synchronized這個關鍵字的用法.因為在我昨天進行建立連線池套接字的研究的時候,發現假如我不弄清楚這個概念,根本就無法進行下去,所以我決定將自己對Socket的興趣先冷卻一下,而回過頭來看synchronized.


看了一上午的Think in ,覺得還是卓有成效的,應該立即寫下來加深印象.我感覺自己的大腦可重用性極低,總是需要生成新的記憶,從而耗費許多重複勞動.所以象記錄,分析,總結這樣類似的工作應該多多益善.


要弄清synchronized的用法,首先要知道它是用來解決什麼問題的.既然synchronized是同步的意思,那麼它當然就是來解決不同步的問題的.下面就舉一個不同步的例子來演示可能出現的問題.

在這個例子當中,我們會建立兩個執行緒類.一個叫TwoCounter,其工作是對兩個計數器變數同時進行累加,從1開始,你馬上會想道,我們是要用它來實現一個同步.另一個物件叫Watcher,顧名思義,是用來做監視工作的,它負責檢查TwoCounter執行緒中的兩個計數器的值是否相等,看起來這似乎是毫無意義的工作,因為既然是同步累加的,那麼兩個計數器的值怎麼可能不相等呢??

但,事實情況不是這樣的.我們先來看.在看這個程式之前,最好先翻翻Think in Java的14.2.1,我的程式實際上是根據該節中給出的例子簡化的,其中的主類改作了Sharing2

class TwoCounter extends Thread {
  private int count1 = 0, count2 = 0;
  private boolean started=false;
  public void start(){
  if (!started) 止多次對一個執行緒Start方法
  {
  started=true;
  super.start();
  }
  }
  public void run() {
  while (true) {
  count1++;
果TwoCounter執行到這個時候,時間片被分配給了Watcher,那麼這個時候Watcher讀出來的兩個計數器的值當然會不一樣了,這個可能性是存在的。“這是由執行緒的本質造成的——它們可在任何時候掛起(暫停)。所以在上述兩行的時刻之間,有時會出現執行暫停現象。同時,Watcher執行緒也正好跟隨著進來,並正好在這個時候進行比較,造成計數器出現不相等的情況.”(Think in Java)
  count2++;
  System.out.println("Count1="+count1+",Count2="+count2);
  try {
  sleep(500);
  } catch (InterruptedException e){}
  }
  }

  public void synchTest() {
  Sharing2.incrementAccess();
  if(count1 != count2)
  System.out.println("Unsynched");//一旦發現不同步,立即顯示
  }
}

class Watcher extends Thread {
  private Sharing2 p;
  public Watcher(Sharing2 p) {
  this.p = p;
  start();
  }
  public void run() {
  while(true) {
  p.s.synchTest();
  try {
  sleep(500);
  } catch (InterruptedException e){}
  }
  }
}

public class Sharing2 {
  TwoCounter s;
  private static int accesunt = 0;
  public static void incrementAccess() {
  accessCount++;
  System.out.println("accessCount="+accessCount);
  }
  public static void main(String[] args) {
  Sharing2 aaa = new Sharing2();
  aaa.s=new TwoCounter();
  aaa.s.start();//開啟TwoCounter執行緒
  new Watcher(aaa);//開啟Watcher執行緒
  }
}

上面的註釋講得很清楚了,有可能出現不同步的情況.但奇怪的是,我在執行的時候,卻始終沒有遇到不同步的情況,那麼只有一種情況,就是程式中count1++和count2++幾乎是同時進行的,watcher執行緒插不進來,但是為什麼Think in Java上面的程式執行之後就肯定有不同步的情況呢?兩個程式的原理是完全一樣的,唯一不同的是我的程式較為簡單,並且在命令列下執行,未使用GUI.難道是因為使用Applet方式執行或者以主視窗的方式執行開銷更大,使得watcher有機可趁嗎?於是我試著在count1++和count2++之間加了一條迴圈語句,人為的增大空隙,目的是為了讓watcher好插進來,造成監測出來的count1不等於count2的情況,實現不同步.修改後的程式是這樣的
  ......
  count1++;
  for(int i=0;i<5000;i++);
  count2++;
  ......

OK!再執行程式,很快就有不同步現象產生了,這似乎證明我剛才的分析是正確的.但奇怪的是,輸出了一次Unsynchrized之後,以後就再也沒有出現了,也就是說,watcher執行緒只有一次檢測到了兩個計數器count不同.這讓我覺得有點鬱悶,是巧合還是必然呢?也許是時間太短了,等下去肯定還會有Unsynchrized輸出的.

算了,這個問題先放下來,我們繼續.
既然出現了不同步的問題,那很顯然,解決的方法就是synchronized:將TwoCounter的run方法和SynchTest方法都變成同步方法.這樣做代表什麼意思呢? 有什麼好處呢?請參考Think in Java的14.2.2節,裡面有非常詳盡透徹的闡述.特別是對監視器,也就是我們通常所說的物件鎖的概念,書中講的很清楚.

總之,需要修改的程式碼如下:
class TwoCounter extends Thread {
  public synchronized void run() {
  while (true) {
  count1++;
  count2++;
  System.out.println("Count1="+count1+",Count2="+count2);
  try {
  sleep(500);
  } catch (InterruptedException e){}
  }
  }

  public synchronized void synchTest() {
  Sharing2.incrementAccess();
  if(count1 != count2)
  System.out.println("Unsynched");//一旦發現不同步,立即顯示
  }
}

略去其它不寫,表示從問題到解決其實很簡單,呵呵.
我們注意到無論run()還是synchTest()都是“同步的”。如果只同步其中的一個方法,那麼另一個就可以自由忽視物件的鎖定,並可無礙地呼叫。所以必須記住一個重要的規則:對於訪問某個關鍵共享資源的所有方法,都必須把它們設為synchronized,否則就不能正常地工作。

現在又遇到了一個新問題。Watcher2永遠都不能看到正在進行的事情,因為整個run()方法已設為“同步”。而且由於肯定要為每個物件執行run(),所以鎖永遠不能開啟,而synchTest()永遠不會得到呼叫。之所以能看到這一結果,是因為accessCount根本沒有變化。


為解決這個問題,我們能採取的一個辦法是隻將run()中的一部分程式碼隔離出來。想用這個辦法隔離出來的那部分程式碼叫作“關鍵區域”,而且要用不同的方式來使用synchronized關鍵字,以設定一個關鍵區域。Java透過“同步塊”提供對關鍵區域的支援;這一次,我們用synchronized關鍵字指出物件的鎖用於對其中封閉的程式碼進行同步。如下所示:

synchronized(sync) {
  // This code can be accessed by only
  // one thread at a time, assuming all
  // threads respect syncObject's lock
}

在能進入同步塊之前,必須在synchObject上取得鎖。如果已有其他執行緒取得了這把鎖,塊便不能進入,必須等候那把鎖被釋放。
可從整個run()中刪除synchronized關鍵字,換成用一個同步塊包圍兩個關鍵行,從而完成對Sharing2例子的修改。但什麼物件應作為鎖來使用呢?那個物件已由synchTest()標記出來了——也就是當前物件(this)!所以修改過的run()方法象下面這個樣子:

意沒有synchronized關鍵字了
  public void run() {
  while (true) {
  synchronized(this){
  count1++;
  count2++;
  }
  System.out.println("Count1="+count1+",Count2="+count2);
  try {
  sleep(500);
  } catch (InterruptedException e){}
  }
  }

意,synchTest()還是要有synchronized關鍵字的,考慮一下為什麼

這樣的話,synchTest方法就可以得到呼叫了,我們也可以看到accessCount的變化了.


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752043/viewspace-993920/,如需轉載,請註明出處,否則將追究法律責任。

相關文章