2013-04-24

執行緒與 synchronized 同步函式的應用

程式功能 : 

模擬一個 CandyBox (糖果盒),裡面放了 100 顆糖果,然後由 13 個 kid (小孩) 隨意抓取,每次抓取的糖果數量為 5 ~ 10 顆不等 (以亂數決定),每個小孩抓取的次數不限,直到糖果盒裡面的糖果被取完為止,最後再以文字的方式將各個數據結果呈現在螢幕上。

◎此範例主要是模擬多個執行緒共同取用一個固定值的程式架構示範,並沒有穿插精彩的圖片動畫作為輔助說明。



程式說明 :

程式主要分為四個部份 :
1. MainActivity  : 主要的 Activity (Activity 類別) 
2. CandyBox    : 糖果盒類別 (自訂類別)
3. Kid                : 小孩類別 (自訂類別並實作 Runnable 介面) 
4. ExecResult  : 負責處理整個事件運作的核心類別 (View類別)


在 MainActivity 中僅簡單地設定了全螢幕顯示模式,而負責呈現畫面內容的 setContentView() 函式中則將 ExecResult 類別物件導入。其他部份則維持原始設定。

程式碼如下 :

MainActivity.java
package a.b.c.testsync;

import android.os.Bundle;
import android.app.Activity;
import android.view.Menu;
import android.view.Window;
import android.view.WindowManager;

public class MainActivity extends Activity {

 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  
  //設定全螢幕
  requestWindowFeature(Window.FEATURE_NO_TITLE);
        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
          WindowManager.LayoutParams.FLAG_FULLSCREEN);
  
        setContentView(new ExecResult(this));
 }
 
 @Override
 public boolean onCreateOptionsMenu(Menu menu) {
  // Inflate the menu; this adds items to the action bar if it is present.
  getMenuInflater().inflate(R.menu.main, menu);
  return true;
 }
} 



CandyBox 類別基本上算是一個靜態類別,此類別並不會被拿來生成物件 (object),所以不會在其他程序中看到類似 CandyBox candyBox = new CandyBox(); 的敘述發生。

此類別的構造非常簡單,兩個 static 資料欄位 (candy 與 catchCount) 和一個 static 函式 (函式名稱 : takeout),candy 與 catchCount 分別記錄糖果剩餘數量與抓取次數的總合。

注意到 static 函式 takeout 使用了 synchronized (同步) 修飾詞,當函式宣告為 synchronized 後,於程式執行期間,若有一執行緒呼叫此函式,在該函式未完成此執行緒交付的工作之前,其他執行緒無法再進入執行此函式。

您可以想成這個糖果盒設定了一道規則,就是一次只能允許一個小孩來抓取裡面的糖果;而這個 takeout 函式就是糖果盒( CandyBox 類別 )所設定的規則,同一時間只能允許一個 kid 執行緒來抓取糖果。

在現實的世界中即使所有的小孩在同一時間中搶成一團,不管各自搶到多少糖果,在糖果還未被吃掉之前,糖果的總合並不會有所改變。但反觀在多執行緒程式運作的世界裡,若沒有針對數值資料作保護限制,最後呈現的結果將會出乎意料、錯誤百出。

◎synchronized 同步敘述還有其他用法,這裡僅示範以函式為施行對象。

程式碼如下 :

CandyBox.java
package a.b.c.testsync;

public class CandyBox {
 
 static int candy;
 static int catchCount; 
 
 //靜態(類別)初始函式
 static {
  candy = 100;    //預設有 100 顆糖果
  catchCount = 0; //記錄糖果被抓取的次數總合
 }
 
 //糖果抓取事件
 protected synchronized static int takeout(int count) {
  //candy:糖果目前剩餘的數量      count:此次要取出的糖果數量 
  //當糖果的剩餘數量不足以應付要取出的數量時
 
  if (candy <= count) {
   //剩多少就給多少
   count = candy;
   candy = 0;
  } else {
   //扣除被取走的糖果
   candy -= count;
  }
  catchCount++;  //記錄糖果被抓取的次數總合
  return count;  //傳回該次糖果被抓取時所取出的數量
 }
}



Kid 類別的結構也很簡單,由於我們只需要這個 kid (小孩) 去做抓取糖果的動作,沒有要他們互相打架,所以不會發武器給他們 XD。

此類別設置了一個口袋 pocket 用來放置糖果,count 是這個小孩一次想抓取的糖果數量,因為此類別必須 "動起來" 所以我實作了 Runnable 介面,只要一生成此類別物件,其執行緒也會跟著啟動。

執行緒的工作內容 : public void run() {  //工作內容 ........  }
小孩 (kid) 會先去檢查糖果盒 (CandyBox) 裡面是否還有糖果,接著以亂數值來模擬每次抓取一把糖果的數量 count,再透過呼叫 CandyBox.takeout(int count) 函式確定實際取得的糖果數量,最後再放入口袋 (pocket) 中。

在糖果還未被搶光以前,每個小孩都可能還有第二次 (甚至第三次) 機會由糖果盒中抓取糖果,程式中雖然設定執行緒每次抓取糖果後會稍停 0.2 秒空檔 -- Thread.sleep(200) 以便讓其它小孩也有空檔可以抓取糖果,但仍無法保證每個小孩都可以搶到糖果。

簡略來說,小孩 (kid) 的工作內容就是去糖果盒 (CandyBox)  抓取 (takeout) 糖果 (candy) 並放入口袋 (pocket),當小孩發現 CandyBox 裡的糖果 (candy <= 0) 都已經沒有的時候,該小孩才會停止動作 (即執行緒停止)。

最後我們可以藉由 checkpocket 函式來檢查這個小孩共拿了多少糖果。

程式碼如下 :

Kid.java
package a.b.c.testsync;

public class Kid implements Runnable {

 private int pocket = 0;   //口袋
 private boolean flag = true;
 private int count = 0;    //抓取一把糖果的數量
 
 //建構函式
 public Kid() {
  new Thread(this).start();
 }
 
 //檢查口袋
 protected int checkpocket() {
  return pocket;  //傳回口袋裡的糖果數量
 }
 
 @Override
 public void run() {
  // TODO Auto-generated method stub
  
  while(flag){
   if (CandyBox.candy <= 0) {
    count = 0;
    flag = false;
   } else {
    count = (int)(Math.random()*6)+5; //count 亂數值介於 5 ~ 10
   }
   int temp = 0;
   temp = CandyBox.takeout(count);  //從CandyBox中 隨機抓取 5 ~ 10 顆糖果
   pocket += temp;  //把抓取到的糖果放入口袋
   
   try {
    Thread.sleep(200);  //延遲 0.2 秒
   } catch (InterruptedException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
   }
  }
 }
}



最後一個類別 ExecResult,它繼承了 View 類別,其主要工作內容如下 :
1. 生成 kid 物件
2. 隨時檢查 CandyBox 裡的 candy 是否已經歸零
3. 負責處理各項數據的畫面佈局
4. 加入螢幕觸碰函式 (這裡的作用是碰觸螢幕後就直接結束程式)

程式中宣告了一個物件陣列 ArrayList,用 ArrayList 來存放所產生的 kid 物件,以便後續的一些數據操作,程式中設定產生 13 個 kid 物件。

當這些 kid 物件被產生之後,每一個 kid 物件隨即啟動自身的執行緒進行指定的工作,kid 的工作細節請回顧上方 Kid 類別的內容。

在生成 13 個 kid 物件並放入 kid 物件陣列後,接著會執行 checkResult() 函式,這個函式裡面宣告了一個 check 執行緒,並在函式內啟動,check 執行緒會不斷地去檢查 CandyBox 裡的 candy 數量,一旦確定 candy 為零時,就會執行 invalidate() 函式,而這個 invalidate() 函式會自動去呼叫 onDraw() 函式,onDraw() 函式會依照我們所指定的內容將數據結果顯示到畫面上。

程式碼如下 :

ExecResult.java
package a.b.c.testsync;

import java.util.ArrayList;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.view.MotionEvent;
import android.view.View;

public class ExecResult extends View {

 ArrayList<Kid> kid;
 Paint paint;
 boolean flag;  //執行緒 check 旗標
   
 //建構函式
 public ExecResult(Context context) {
  super(context);
  // TODO Auto-generated constructor stub
  
    paint = new Paint();        //產生畫筆物件
    kid = new ArrayList<Kid>(); //產生 Kid 物件陣列
    initialSet();               //初始設定
  
  }
  
  //初始設定函式
  private void initialSet() {
  
        CandyBox.candy = 100;    //設定糖果盒有 100 顆糖果
        CandyBox.catchCount = 0; //清除抓取總合
  
        //設定畫筆
        paint.setColor(Color.WHITE);
        paint.setTextSize(30);
 paint.setStrokeWidth(2);
 paint.setAntiAlias(true);
  
 //checkResult 函式裡的 check 執行緒旗標
 flag = true;
  
 kid.clear();  //先清除 kid 物件陣列
 //產生 13 個 Kid 執行緒
 for (int i=0; i<13; i++) {
  kid.add(new Kid());
 }
 //檢查結果函式(呼叫後將同時啟動其內部的 check 執行緒)
 checkResult();
 }
 
 protected void checkResult() {
  //宣告在函式內部的執行緒及工作內容
  Runnable check = new Runnable() {
   
   //執行緒 check 的工作內容
   @Override
   public void run() {
    // TODO Auto-generated method stub
    while(flag) {
     //檢查 CandyBox 內的 candy 數量是否歸零
     if(CandyBox.candy <= 0) {
        flag = false;  //設定 flag 為 false 準備結束執行緒
        invalidate();  //invalidate()會執行 onDraw()函式,繪出結果
     }
    }
   }
  };
  
  //上方寫好了執行緒的工作內容之後
  //接著把 check 套入 Thread 類別並將它啟動
  Thread p;
  p = new Thread(check);
  p.start();   //啟動 check 執行緒
  
  //如果於執行期間造成緒程方面的錯誤而導致程式被迫中斷 
  //請使用 join(),等待此 check 執行緒結束,即可解決此情形
  try {
   p.join();
  } catch (InterruptedException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  }
 }
  
 @Override
 protected void onDraw(Canvas canvas) {
  // TODO Auto-generated method stub
  super.onDraw(canvas);
  canvas.drawColor(Color.BLACK);  //清除畫面
  canvas.drawText("糖果盒裡面共有 100 顆糖果", 10, 30, paint);
  
  //當所有的糖果都被取光時  印出結果
  if (done){
   int num = 0;
   int sum = 0;
   for (Kid k : kid) {
    num++;
    sum += k.checkpocket();
       canvas.drawText("第 " + num + " 號 kid 拿了 " + k.checkpocket() + 
      " 個糖果", 10, 60 + 30 * num, paint);
   }
   canvas.drawText("糖果盒剩 : " + CandyBox.candy + " 顆 ", 10, 30 + 30 * (num + 3, paint);
   canvas.drawText("共發生 " + CandyBox.catchCount + " 次抓取事件", 10, 30 + 30 * (num + 4), paint);
   canvas.drawText("所有 Kid 的糖果總合 : " + sum , 10, 30 + 30 * (num + 5), paint);
  }
 }
}

執行結果 :



3 則留言:

  1. 好複雜的程式,看了眼花。這跟FB的糖果遊戲是一樣的嗎?我玩這個遊戲很遜,應該是所有遊戲是都一樣
    希維亞

    回覆刪除
    回覆
    1. ㄟ嘿 ~
      妳怎跑到這裡留言了 ~.~
      這只是在描述某個語法的實際運用 .... 與 FB 的 Candy Crush 完全無關 XD
      最近 Candy Crush 很熱門 ! 不曉得妳到第幾關了 ? XDDD

      刪除
  2. 達仔, 您好。難得看到台灣有人這麼熱心寫這麼詳細的文章介紹Android開發。感謝!
    Android應該會是最普及的平台。加油!

    回覆刪除

搜尋此網誌