讓你的程式具有多工(Multitasking) 及多重處理(Multiprocessing)的能力 執行緒 Threads 讓你的程式具有多工(Multitasking) 及多重處理(Multiprocessing)的能力
什麼是執行緒 支援多重處理的執行控制機制,它可以執行程式中任何一組相關且可與程式中其它部分多重並行處理的程式片斷。 播放背景音樂 執行緒a: 數數運算 執行緒c: 動畫顯示 執行緒b: 程式(多執行緒)
執行緒具有以下幾個基本特徵: 不是完整的程式。 隸屬於同一個程式的執行緒必需共享系統分配給所屬主程式的資源(如記憶體空間、CPU時間等)。 每一個執行緒都有自己的執行堆疊及程式計數器 程式可以設定執行緒的執行優先順序及所隸屬的執行緒群組。 每個執行緒於程式執行時的可能狀態有:born、ready、running、blocked、suspend、sleeping、waiting及dead。
如何建立執行緒 在Java程式中,執行緒是以執行緒物件來表示,亦即在程式中 一個執行緒物件就代表了一個可以執行程式片斷的執行緒
建立執行緒物件以便指定程式中部分的程式碼給執行緒執行的方式有二種: 在程式中建立Thread類別的子類別,然後直接在子類別中改寫繼承自Thread類別的run()方法 在程式中定義界面Runnable,然後改寫Runnable的run()方法 Note: run()方法為執行緒的執行進入點(Entry Point)
11-1 Java的多執行緒 11-1.1 何謂執行緒 11-1.2 利用Thread類別建立多執行緒 11-1.1 何謂執行緒 11-1.2 利用Thread類別建立多執行緒 11-1.3 利用Runnable介面建立多執行緒
前言: 執行緒是指單一連續的程式行程,傳統的程式語言大多只有一個執行緒,但是在Java中允許應用程式同時執行兩個或兩個以上的執行緒,稱為多執行緒。
11-1.1 何謂執行緒 在介紹執行緒之前,必須先了解什麼是行程(Process)。 11-1.1 何謂執行緒 在介紹執行緒之前,必須先了解什麼是行程(Process)。 所謂行程是指作業系統執行每個應用程式的流程,作業系統會分配足夠的記憶體空間與CPU時間給每個行程。而因為每個應用程式不一定會佔用CPU的所有時間,為了充分使用CPU時間,避免有CPU閒置的情況,所以衍生出多執行緒的概念。
單一執行緒: 所謂單一執行緒(Thread)是指應用程式在一個行程中只能處理一項工作,也就是說同一個時間點只能執行一道指令,必須等前一項工作執行後,才能依序執行下一個工作,如下圖所示:
多執行緒與多工: 所謂多執行緒是指應用程式在一個行程中可以同時處理多項工作,而每一個行程都是各自獨立的,不會互相干擾。 利用多執行緒可以讓應用程式的行程共享CPU記憶體空間與時間,善加利用CPU的閒置時間,有效率的執行程式,而多執行緒其實就是模擬多工功能而發展出來的概念。
多工與多執行緒相異處: 多工是在Windows作業系統中常見的特性,舉例來說,作業系統可以同時執行瀏覽器、Word、小畫家等多個應用程式。
多執行緒的運作方式如下圖所示:
11-1.2 利用Thread類別建立多執行緒 在Java中必須利用Thread類別的物件來建立執行緒,可以採用下面兩種方式建立多執行緒: 實作Runnable介面:間接產生執行緒。
利用Thread類別建立多執行緒: 若要利用Thread類別建立多執行緒,必須先建立一個繼承Thread的衍生類別,然後在類別中覆載Thread類別的run成員函式,其語法如下:
Thread類別的建構式與成員函式-1: 建立多執行緒類別前,必須先了解Thread類別中有哪些成員可以使用,說明如下:
Thread類別的建構式與成員函式-2:
Thread類別的建構式與成員函式-3:
Thread類別的建構式與成員函式-4:
下面使用基礎類別Object中的函式,說明如下:
例如下面的程式碼利用Thread類別建立多個執行緒:
上述利用Thread類別建立多個執行緒的程式碼分析如下-1: 第26~28行宣告建構式,設定列印的份數與樣式。 第30~41行覆載Thread基礎類別中的run成員函式,第31行顯示目前執行緒的名稱。 第36行利用sleep函式將執行緒暫停2秒(2000毫秒=2秒),必須拋出一個InterruptedException例外,使暫停中的執行緒可以隨時偵測中斷例外狀況。 第4行顯式預設執行緒的名稱為main,表示執行Java程式時,不需要利用start函式,就會自動啟動main執行緒了。
上述利用Thread類別建立多個執行緒的程式碼分析如下-2: 第6行判斷如果預設的執行緒還存活,就執行第7與8行,中斷該執行緒,並且顯示訊息。 在Java中也可以直接建立Thread類別的物件,例如第11行建立Thread物件,並同時指定執行緒的名稱,而因為沒有以start函式啟動此執行緒,所以第12行的isAlive函式值為false,表示執行緒沒有在執行。 第14與15行分別建立兩個PrintThread物件,並在第16與17行以start函式啟動執行緒。 第18行利用activeCount函式傳回目前有幾個執行緒正在執行,傳回值為3,分別為預設的main執行緒、p1與p2執行緒。
完成後,執行程式的畫面顯示如下:
11-1.3 利用Runnable介面建立多執行緒 除了利用Thread類別,也可以利用Runnable介面間接建立多執行緒,其實Java內建的Thread類別本來就繼承Object類別,並且實作Runnable介面,其程式碼如下:
透過實作Runnable介面來建立多執行緒的語法如下: 在Java中可以利用介面達到多重繼承的目的,當要建立多執行緒的類別已經繼承其他類別時,就可以透過實作Runnable介面來建立多執行緒,然後在類別中覆載Thread類別的run成員函式,其語法如下: 多執行緒類別不一定要繼承其他類別,才能以實作Runnable介面來建立多執行緒,所以上述語法可以省略extends類別名稱。實作Runnable介面後,必須再利用Thread類別才能產生執行緒物件,同樣可以使用Thread類別中的建構式與成員函式。
下面修改範例檔ch11_01_02改採用Runnable介面間接建立多個執行緒的方式:
上述建立多個執行緒的程式碼說明如下: 上述程式碼與範例檔ch11_01_02的差別在於粗體字的部份,因為利用Runnable介面必須要間接建立執行緒,所以要在程式碼第15與17行利用Thread類別建立物件。 完成後,執行程式的畫面與範例檔ch11_01_02相同。
11-2 管理執行緒 11-2.1 執行緒的生命週期 11-2.2 在排程中加入執行緒 11-2.3 執行緒的同步化 11-2 管理執行緒 11-2.1 執行緒的生命週期 11-2.2 在排程中加入執行緒 11-2.3 執行緒的同步化 11-2.4 執行緒之間的溝通
前言: 執行緒可能會隨著不同的操作,而處於不同的狀態,當程式中有多個執行緒同時執行時,就必須管理每個執行緒的執行方式,這樣這些執行緒才不會互相干擾。
11-2.1 執行緒的生命週期 其實每個執行緒在新建到終止之間,都會處於生命週期的四種狀態之一,執行緒的生命週期如下圖所示:
有關生命週期的說明如下-1: 新建(new):利用關鍵字new建立新的執行緒物件,系統尚未配置CPU資源,就是處於此狀態。 執行start函式,執行緒會進入可執行(runnable)狀態。 執行stop或run函式結束時,執行緒會進入死亡(dead)狀態。 可執行(runnable):啟動執行緒後,新的執行緒會進入佇列(queue)中等待執行的機會,程式會先執行優先權值較高的執行緒。 執行yield函式,執行緒會將執行權轉讓給其他執行緒。 執行sleep、suspend、wait函式或等待I/O完成時,執行緒會進入不可執行(not runnable)狀態。
有關生命週期的說明如下-2: 不可執行(not runnable):執行緒處於凍結的狀態,等待重新進入佇列的機會。 執行stop或run函式結束時,執行緒會進入死亡(dead)狀態。 當sleep函式的睡眠時間結束,或是執行interrupt函式時,執行緒會回到可執行狀態。 執行resume函式可以解除suspend函式凍結的執行緒,重新回到可執行狀態。 執行notify函式可以解除wait函式凍結的執行緒,重新回到可執行狀態。 當I/O(輸入/輸出動作)完成時,執行緒會重新回到可執行狀態。 死亡(dead):當執行stop函式或是run函式的程式區塊結束時,就會進入死亡狀態。
11-2.2 在排程中加入執行緒 如果程式中存活多個執行緒,而希望程式按照想要的順序,執行執行緒中的敘述時,就可以利用join函式暫停目前正在執行的執行緒,然後執行指定的執行緒內容。
下面先來看看沒有利用join函式執行執行緒物件的程式碼:
完成後,畫面顯示如下: 完成後執行程式,本來希望在最後一行才顯示字串“複習功課完畢...”,但是執行畫面卻顯示在第一行。這是因為雖然第6與7行啟動執行緒math與english,但Java本身會自動執行main執行緒的start函式,所以先顯示第8行的字串後,才會執行執行緒中run函式的程式碼,畫面顯示如下: 範例檔ch11_02_02a可能不是設計者想要的執行結果,這時可以利用join函式暫停目前的main執行緒,然後先執行指定的執行緒,等到指定的執行緒執行完成,再接著執行被暫停的main執行緒。
下面將範例檔ch11_02_02a修改為先執行math與english執行緒,再執行main執行緒:
上述程式碼與範例檔ch11_02_02a的差別在於粗體字的部分,第8與13行利用join函式強制程式先執行math與english執行緒,值得注意的是,使用join函式必須在程式中拋出InterruptedException例外。
完成後,執行程式的畫面顯示如下:
11-2.3 執行緒的同步化 如果程式同時執行同一個類別產生的多個執行緒,因為這些執行緒共用相同的成員變數,所以當進行新增、刪除或修改資料等動作時,可能會存取到錯誤的資料,例如:X物件在修改物件的sum成員變數時,突然有另一個Y物件讀取sum成員變數,則此時X與Y物件不同步,會發生讀取資料錯誤的情形。 這種因為資料不同步所引起的錯誤,通常是不易察覺的,為了避免這種錯誤發生,Java提供了同步(Synchronization)的機制,可以將類別中的函式設定為互斥(Mutua1 Exclusion),鎖定存取成員的動作,則在任何時間點,都只能有一個函式被執行,不讓其他執行緒共用成員。
在函式的前方加上關鍵字synchronized就可以設定執行緒同步化,其語法如下:
下面範例為執行緒尚未同步化,其程式碼如下: 下面的程式碼設定某人的帳戶餘額sum為20000元,然後建立兩個ATM執行緒,分別到兩家銀行的提款機領錢,再顯示帳戶剩下的餘額,在此範例中尚未將執行緒同步化:
上述程式碼分析如下-1: 第12~37行建立一個提款機的執行緒類別ATM,第14行宣告一個靜態成員變數sum,並指定初始值為20000,就是本範例中會被多個執行緒所共用的成員變數。 第18~22行覆載Thread類別的run函式,每當啟動執行緒時,就會執行2次draw_money函式。 第24~36行宣告函式draw_money,其中第25行修改成員變數sum的值,然後在第28~30行利用sleep函式讓執行緒暫停0.5秒,此時其他執行緒就有機會搶到執行權,則會發生“同步”問題。
上述程式碼分析如下-2: 在正常情況下,如果沒有加入第28~30行的程式碼,則程式會先執行完A1執行緒的draw_money函式2次,再執行A2執行緒的draw_money函式2次,其示意圖如下所示:
上述程式碼分析如下-3: 如果執行緒發生“同步”問題,則A1執行緒執行完第1次draw_money函式後,就會休眠0.5秒,此時執行權會被A2執行緒搶去,執行該執行緒的draw_money函式,如此反覆執行,因為沒有共享ATM類別中的成員,所以計算的sum值就可能發生錯誤,其示意圖如下所示:
完成後,畫面顯示如下: 完成後執行程式,雖然總餘額是正確的,但是 第1~3次提款後的餘額都有錯誤,畫面顯示如 下:
下面解決前面範例的錯誤,將程式碼修改為同步執行緒,其程式碼如下: 為了解決範例檔ch11_02_03a的錯誤,只要將 程式碼修改為同步執行緒,在第26行的函式前 方加上關鍵字synchronized即可,程式碼如下:
完成後執行程式,每次提款後餘額都會減少1000元,畫面顯示如下:
11-2.4 執行緒之間的溝通 當多個執行緒共用相同的資料時,除了利用同步化的功能來解決問題,另一個方法是採用基礎類別Object提供的wait與notify函式,建立執行緒之間的溝通機制,以決定執行緒是否可進入runnable狀態,這些函式的說明如下。
建立執行緒之間溝通機制的函式說明如下: wait():讓目前的執行緒等待,直到被中斷或被notify/notifyAll函式喚醒,必須將此函式撰寫在try…catch…區塊中,以捕捉wait函式拋出的InterruptedException例外。 wait(long timeout):與wait函式相同,但可以設定暫停timeout毫秒,直到時間到之後、被中斷或被notify/notifyAll函式喚醒,才繼續執行(若執行緒尚未被中斷或呼叫notify/notifyAll喚醒前,設定時間就已經終止,則執行緒會先被喚醒)。 notify():喚醒正在等待執行的執行緒。 notifyAll():喚醒所有正在等待執行的執行緒。 值得注意的是,上述的函式必須要撰寫在有同步化的函式(具有synchronized的程式區塊)中,才可以正確執行。
下面範例為撰寫一個籃球處理機的類別,其圖示說明如下: 下面的範例撰寫一個籃球處理機的類別,最多只能放置10顆籃球,裝滿之後就不能再裝,必須等待投籃後,才能再輸入籃球;而當籃球處理機中的籃球被取空後,必須等待新輸入的籃球,才能繼續投籃,如下圖所示:
程式碼撰寫如下:
上述程式碼分析如下: 第2行建立一個靜態的籃球處理機物件ball,importball與outportball類別中會應用到此物件。 第15~19行設定籃球代號為隨機1~10的號碼,每輸入一次就等待0.5秒。 第33~37行顯示有哪幾個號碼的籃球,每輸出一次就等待0.5秒。 第50~61行的importball函式表示當索引值index大於或等於9時,就等待1秒,或執行到第60行喚醒執行緒。 第62~74行的outportball函式表示當索引值index小於0時,就等待執行到第72行喚醒執行緒。
完成後,執行程式的畫面顯示如下:
溫故知新-1:
溫故知新-2:
自我突破習題-選擇題:
自我突破習題-實作題: