380 likes | 469 Views
1-1 行程 Process 與執行緒 Thread 觀念. 一個行程可以有多個執行緒。執行緒也稱為小的行程,執行緒可以節省更多的記憶體,也比行程更有效能。一個行程就是一個程式正在執行。當我們在終端機下達指令時,作業系統就會建立一個行程,而當我們的程式執行完時,這個行程就被終止了。在一個分時系統的作業系統,允許多個使用者使用電腦系統,而許多行程也同步的被執行。像我們的個人電腦,一般只有一顆 CPU 中央處理器,但卻同時的處理多個行程,而這就分是分時系統。
E N D
1-1行程Process與執行緒Thread觀念 一個行程可以有多個執行緒。執行緒也稱為小的行程,執行緒可以節省更多的記憶體,也比行程更有效能。一個行程就是一個程式正在執行。當我們在終端機下達指令時,作業系統就會建立一個行程,而當我們的程式執行完時,這個行程就被終止了。在一個分時系統的作業系統,允許多個使用者使用電腦系統,而許多行程也同步的被執行。像我們的個人電腦,一般只有一顆CPU中央處理器,但卻同時的處理多個行程,而這就分是分時系統。 當我們執行程式時,電腦作業系統就會使用new幫我們產生新的行程,而當我們的行程就緒(ready)時,而核心又排班分配CPU中央處理器給他時,他就會開始執行running,直到執行結束terminated(或Zombie)。當然在過程中也有可能發生像系統呼叫的System call而發生中斷,或者改成其它行程執行(被preempt),這時我們的行程就會回到就緒ready的狀態。當然在過程中也可能發生像輸入/輸出I/O或事件等待(sleep),此時,CPU是在閒置waiting的狀態,而當我們的輸入/輸出或事件完成時(被wake up),才會到就緒ready,等待下一次排班分配CPU中央處理器,直到結束(zombie或terminated)。
在分時系統中一個行程在它整個生命期,將在各種不同的排班佇列中,這當然要看我們的需要設計,以及作業系統的設計。因此作業系統需按排班方式從佇列中選出行程,讓所選出的行程讓中央處理器CPU來執行。而我們所執行的程式也可能只執行一下子,然後就等待輸入/輸出I/O的要求,但I/O的時間,比CPU所需執行的時間大上很多。在整批作業系統中,大部份的行程都會以spooling儲存池的方式存到硬碟或大型儲存裝置,等待一個一個的執行。在分時系統中一個行程在它整個生命期,將在各種不同的排班佇列中,這當然要看我們的需要設計,以及作業系統的設計。因此作業系統需按排班方式從佇列中選出行程,讓所選出的行程讓中央處理器CPU來執行。而我們所執行的程式也可能只執行一下子,然後就等待輸入/輸出I/O的要求,但I/O的時間,比CPU所需執行的時間大上很多。在整批作業系統中,大部份的行程都會以spooling儲存池的方式存到硬碟或大型儲存裝置,等待一個一個的執行。 • 分時系統的中程排班程式是將行程從記憶體中移開,因此而降低多元程式規化的程度,然後再將行程放回記憶體中,並且放在它移開前的位置繼續執行,這種方法稱為swapping,如下圖所示。
1-1-1執行緒狀態 • 執行緒為小的行程,因此行程有的狀態,執行緒都有。這是在Java中執行緒的狀態。 • 當我們使用new來新增執行緒new Thread(r),這個執行是還沒有執行,因為它在new新增的狀態。當一個執行緒在new的狀態,程式尚未執行程式碼。 • 當我們呼叫start方法,這個程式就是可以準備執行runnable。一個runnable可執行的執行緒,它等待作業系統分配CPU的時間給這個執行緒來run跑。 • 當一個執行緒在可執行時runnable,它可能有三個狀態,分別是等待、timed waiting休息(或稱為sleep)和終止terminated。等待和休息teimed waiting都是屬於封鎖block的情況。當執行緒sleep休息時,它就會到teimed waiting休息的狀態。當執行緒操作I/O存取資料時,這時就會在wait等待,直到I/O的操作完成後,執行緒才會回到runnable可執行的狀態。 • 有幾個狀態會讓執行緒由封鎖block的狀態轉移到runnable可執行的狀態。當執行緒在休息tiemed waiting(sleep)時,當休息時間滿了,就會回到runnable的可執行狀態。當一個執行緒等待wait一個條件,而其它的執行緒通知signal給這個執行緒條件已改變,這時這個執行緒就會回到runnable可執行的狀態。 • 有幾個狀態會讓執行緒由runnable可執行的狀態轉移到terminated終止的狀態。當執行緒正常完成工作,它就會到終止terminated的狀態,然後結束。當例外發生終止run()函數時,執行緒會突然的終止。
1-2建立執行緒 • 這是Thread類別的函數。我們無法知道執行緒是可執行runnable還是被封鎖blocked。我們也無法得知執行緒是runnable正在執行或已經終止了。
執行緒類別也有屬性。在Java程式設計的語言中,每一個執行緒都有優先權priority。我們可以設定執行緒的優先權,使用setPriority()函數。我們可以設定執行緒的優先權在MIM_PRIORITY(1)到MAX_PRIORITY(10)之間,NORM_PRIORITY預設是5。當執行緒排程要選取執行緒時,它會先選取高優先權的執行緒來執行。但是執行緒的屬性是由作業系統和硬體所支援。例如Windows XP有七個優先權的層度。一些Java的優先權將對應到這作業系統。
範例:ThreadTest.java • 我們自訂執行緒PChar類別和PNum類別和是繼承java.lang.Thread類別,因此我們的PChar類別和PNum類別是自訂的執行緒類別。
第二行為我們的main()函數,程式由此開始執行。第二行為我們的main()函數,程式由此開始執行。 • 第三行新增自訂執行緒類別PChar的物件t,它會列印字元t,50次。 • 第四行新增自訂執行緒類別PChar的物件r,它會列印字元r,50次。 • 第五行新增自訂執行緒類別PNum的物件print50,它會列印從1到50的數字。 • 第六行的start()函數被呼叫來啟動執行緒t的run()函數執行running。 • 第七行的start()函數被呼叫來啟動執行緒r的run()函數執行running。 • 第八行的start()函數被呼叫來啟動執行緒print50的run()函數執行running。 • 第十一行到第二十三行為我們自訂的PChar類別,它繼承了Thread類別。 • 第十五行到第十八行為PChar()的建構子。 • 第十九行到第二十三行為run()函數,它會執行列印字元t。 • 第二十四行到第三十四行為自訂的PNum類別,它繼承了Thread類別。 • 第二十六行到第二十八行為PNum()的建構子。 • 第三十行到第三十三行為run()函數,它會執行列印1到n的數字。
這個程式有三個獨立的執行緒。因為我們使用執行緒來執行,執行緒它有優先順序。當某個執行緒它執行一段時間後,它的優先權會變低,因此排程就會將CPU的時間分配給較高優先權的執行緒。當執行緒等待一些時間後,它的優先權會變高,高過正在執行runnable的執行緒時,它就會把CPU搶過來preempt,讓高優先權的轉移到可執行runnable狀態。因此這三個執行緒搶來搶去的執行。這個程式有三個獨立的執行緒。因為我們使用執行緒來執行,執行緒它有優先順序。當某個執行緒它執行一段時間後,它的優先權會變低,因此排程就會將CPU的時間分配給較高優先權的執行緒。當執行緒等待一些時間後,它的優先權會變高,高過正在執行runnable的執行緒時,它就會把CPU搶過來preempt,讓高優先權的轉移到可執行runnable狀態。因此這三個執行緒搶來搶去的執行。
1-3Thread群組 • Thread群組就是一群執行緒。有一些程式包含相似功能的少數執行緒,我們可以將它們群組起來。例如我們可以在同一時間暫停或恢復一群Thread。 • 我們使用ThreadGroup建構子來建構Thread群組。 • ThreadGroup tg=new ThreadGroup() • 我們使用Thread建構子,在ThreadGroup群組放置thread。我們建立一個執行緒,並且將它放置到thread群組中。 • Thread t=new Thread(tg,new ThreadClass(),執行緒的標籤); • 我們使用activeCount()函數來查詢有多少在群組中的執行緒正在執行。這顯示了在tg群組中正在執行的執行緒數量。 • System.out.println(tg.activeCount()); • 每一個執行緒都屬於一個執行緒群組。預設,一個新建立的執行緒屬於生產它的執行緒群組。我們使用getThreadGroup()方法來找尋執行緒群組名稱。 • 我們可以使用在執行緒群組的start()函數來各別的啟動每一個執行緒。
1-4執行緒的同步和合作 • 假如一個可被分享的資源同時的被多個執行緒存取,這個資源可能會發生錯誤。在作業系統中,因為資源有限,但同時卻有多個行程或應用程式來存取它,這時就會發生搶資源的情況發生,而造成資料結果不一致的情況發生,因此如何讓行程或應用程式有秩序的存取有限資源,這和同步和合作有關。
1-4-1沒有同步存取資源的範例 • 這是一個有限資源,卻由多個執行緒同時存取它,造成資料不一致的情況發生。 • 範例:WithoutSync.java • 假設我們建立和執行50個執行緒,每一個執行緒都會將一元存入銀行帳號。我們假設一開始存款balance為0元,然後再將1元存入。
第二行我們建立Account帳號類別物件account。, • 第三行我們建立執行緒陣列,並放入50個執行緒。 • 第四行到第九行為main()函數,它會執行兩遍存款50次執行緒的動作。 • 第五行新增WithoutSync類別的物件test。 • 第六行使用test.account.getBalance()來得到銀行存款帳戶的金額。 • 第七行再新增WithoutSync類別的物件test1。 • 第六行再使用test1.account.getBalance()來得到銀行存款帳戶的金額。 • 第十行到第二十行為WithoutSync類別的建構子。 • 第十一行新增ThreadGroup執行緒群組類別物件g。 • 第十四行新增執行緒,並將它放到thread[i]陣列中。 • 第十五行使用thread[i].start()來啟動第i個執行緒。 • 第二十一行到第二十五行為AddAPennyThread執行緒類別,它繼承了Thread執行緒類別。 • 第二十二行到第二十四行為AddApennyThread執行緒類別的run()函數,它會存1元到銀行帳戶中。 • 第二十六行到第四十三行是建立帳號Account類別。 • 第二十九行到第三十一行定義getBalance()函數,他們會回傳balance帳戶的存款金額。 • 第三十三行到第四十二行的deposit()存款函數,帶入整數amount的存款。 • 第三十六行的Thread.sleep(1)會讓執行緒休息百萬分之一秒,這時期它的執行緒就有可能執行,而產生資料不一致的情況,因此將第三十五行到第三十九行去掉,則不一致的情況就不會發生了。因為執行緒互相在搶存入balance的資源。 • 第三十四行將銀行的帳戶金額balance加上存款amount。 • 第三十四行到第四十一行稱為critical region關鍵區域。 • 第十八行的g.activeCount()是會得到目前執行緒群組還存活的數量,當所有執行緒都執行完,它就會回傳true,就會跳出while迴圈。
兩次銀行帳戶的存款一開始都為0,執行完50個執行緒後,應該有50元,但是兩次結果都不是50元,因此這個結果是不可預定的。當所有執行緒同時存取deposite()函數時,這個資料就會發生錯誤不一致。兩次銀行帳戶的存款一開始都為0,執行完50個執行緒後,應該有50元,但是兩次結果都不是50元,因此這個結果是不可預定的。當所有執行緒同時存取deposite()函數時,這個資料就會發生錯誤不一致。 • 將第三十五行到第三十九行拿掉,因為這是會讓程式錯誤發生的情況。
因為執行程式沒有一次執行完,中間又休息,這時又有另外一個程式在執行存取相同的資料,這時就會發生資源存取不一致的情況發生。Thread[i]執行緒i在執行讓newBalacne的值為1。在時間2,執行緒[i]先休息sleep,這時後還沒將1的資料存入到balance中。Thread[j]執行緒j這時後在時間2的地方開始執行,這時取到balance的資料為0,因此0+1為1,newBalance的資料為1,然後再休息1的時間。在時間3,Thread[i]將newBalance的資料1給balance。在時間3Thread[j]為休息。在時間4,Thread[j]將newBalance的資料1給balance,這時balance銀行帳戶存款的結果為1。因為執行程式沒有一次執行完,中間又休息,這時又有另外一個程式在執行存取相同的資料,這時就會發生資源存取不一致的情況發生。Thread[i]執行緒i在執行讓newBalacne的值為1。在時間2,執行緒[i]先休息sleep,這時後還沒將1的資料存入到balance中。Thread[j]執行緒j這時後在時間2的地方開始執行,這時取到balance的資料為0,因此0+1為1,newBalance的資料為1,然後再休息1的時間。在時間3,Thread[i]將newBalance的資料1給balance。在時間3Thread[j]為休息。在時間4,Thread[j]將newBalance的資料1給balance,這時balance銀行帳戶存款的結果為1。 • 兩個執行緒搶資源執行的結果,造成資料存取不一致的現象而發生錯誤。
1-4-2同步的範例 • 我們在這裏為了讓程式能夠一次由一個執行緒執行完,讓資料能夠一致性的存取,我們使用lock鎖的觀念。Lock鎖就是將關鍵區域critical region鎖住的程式區塊,一次只允許一個執行緒來存取,直到unlock解開鎖為止。這樣可以保證資料存取一致的情況。 • JDK5.0使用ReentrantLock類別,它為lock鎖的類別。它是在java.util.concurrent.locks.ReentrantLock套件類別。ReentrantLock()為它的建構子,它可以用來保護關鍵區域critical region。ReentrantLock類別中的lock()方法是取得鎖,unlock()方法是釋放鎖。 • 範例:Sync.java • 第一行我們輸入java.util.concurrent.*。 • 第二行我們輸入鎖的套件java.util.concurrent.locks.*。 • 第三十行我們新增一個ReentrantLock()靜態物件的鎖lock。 • 第三十六行到第四十五行我們使用鎖lock來鎖住關鍵區域critical region。 • 第三十六行使用lock.lock()來鎖住。 • 第四十四行使用lock.unlock()來解開鎖。
1-4-3Syncronized同步化關鍵字 • 我們也可以使用syncronized關鍵字來保護資料,它可以保證資料被同步存取。 • public synchronized void deposit(int amount)。一個synchronized同步化的函數在執行前會需要一個鎖。在這實體函數的範例,當函數被呼叫時,一個鎖lock就會被啟動。在這靜態函數的範例中,鎖是在類別上。假如執行緒呼叫一個在物件上synchronized同步化的實體函數,這物件的鎖就會被獲得,當函數執行完後,這物件的鎖就會被釋放。其它執行緒在這段期間呼叫這個物件synchronized同步化的方法,將被檔住,直到這個函數執行完,鎖被釋放unlocked為止。
我們在deposit()函數前加上synchronized關鍵字。 • 範例:WithoutSync3.java • 假如我們無法修改deposit()函數,我們可以新增一個synchronized同步的方法來呼叫deposit()函數。 • 語法: • synchronized(expr){ • statement; • } • 這個expr運算式一定要是物件的參考。假如這個物件已經被其它執行緒鎖住locked,其它執行緒在鎖解開前是不可以存取這個物件的。synchronized同步化敘述讓我們獲得鎖用在物件上,所以我們可以同步存取物件,而不是只有在方法上。
範例:WithoutSync5.java • 第二十五行到第二十七行使用synchronized()函數來同步化物件account,我們將鎖加入到該物件,直到第二十六行的敘述執行完才解開鎖。
1-4-4執行緒的合作 • 執行緒同步化是未了避免資源競爭的情況發生,我們可以讓關鍵區域critical region的多個執行緒能夠互斥mutual來達成,但有時後執行緒間還是要合作。wait()、notify()和notifyAll()函數可以用來幫助執行緒間作溝通。wait()函數會讓執行緒等待wait,直到某個條件發生。當它發生時,我們可以使用notify()函數或者是notifyAll()函數來通知這waiting等待的執行緒來恢復一般的執行。notifyAll()函數叫醒所有waiting等待的執行緒,而notify()則從等待的佇列叫醒一個執行緒。 • wait()、notify()和notifyAll()函數必需被在一個同步synchronized的函數或同步synchronized的區塊內呼叫,並且在這些函數的接收物件上,否則IllegalMonitorStateException例外將會發生。 • 在執行緒1時,當condition條件為假時,它會執行while迴圈,執行anObject.wait()函數,等待人來解救它,它會一直等待wait,直到執行緒2執行synchronized()同步函數,並且在裏面使用anObject物件的notify()函數來叫醒它。這時後執行緒1又再會繼續執行while迴圈。
一個同步的鎖lock必需被在wait等待或notify通知的物件上獲得。當wait()函數被呼叫時,它暫停這個執行緒,而且同步的釋放在物件上的鎖lock。當這執行緒被通知notify而啟動時,這鎖將自動的被獲得。一個同步的鎖lock必需被在wait等待或notify通知的物件上獲得。當wait()函數被呼叫時,它暫停這個執行緒,而且同步的釋放在物件上的鎖lock。當這執行緒被通知notify而啟動時,這鎖將自動的被獲得。 • 範例:Cooperation.java • 這是銀行存款執行緒1和銀行提款執行緒2,但是銀行的帳戶永遠的金額都大於0。這是典型銀行存款的執行緒合作。 • 第二行我們新增Account類別的物件account。 • 第三行新增存款執行緒DepositThread類別的物件t1。 • 第四行新增提款執行緒WithdrawThread類別的物件t2。 • 第五行到第八行是main()函數,程式執行的地方。 • 第六行會新增Cooperation類別的物件test,這時後Cooperation開始執行。 • 第九行到第十二行是Cooperation的建構子。它讓執行緒1和執行緒2開始執行。 • 第十三行到第二十五行是定義存款執行緒。 • 第十四行到第二十四行是run()函數,當執行緒1.start()被呼叫時它就會被執行。 • 第十六行是呼叫account.deposit()來作存款的動作。 • 第十八行Thread.sleep(100)是讓存款執行緒休息百萬分之100秒,這時就換提款執行緒WithdrawThread會執行。 • 第二十六行到第三十二行是定義WithdrawThread提款執行緒。 • 第二十九行account.withdraw()會作提款的動作,也就是將銀行的帳戶金額作減少。
第三十三行到第五十四行是定義Account銀行帳戶類別。第三十三行到第五十四行是定義Account銀行帳戶類別。 • 第三十四行定義balance為銀行帳戶存款金額。 • 第三十五行到第三十七行會回傳銀行帳戶存款金額。 • 第三十八行到第四十二行定義同步化synchronized的存款deposit()函數。 • 第三十九行balance+=amount是存款amount金額到balance帳戶。 • 第四十一行使用this.notify()來告知這個物件,讓在wait()的物件醒來。 • 第四十三行到第五十三行定義同步化synchronized的提款withdraw()函數。 • 第四十五行到第四十六行為while迴圈,當存款金額小於0時,就會執行這個迴圈,然後wait。當存款後(執行存款執行緒DepositThread),它會執行第十六行的存款,然後執行第四十一行的notify()函數來告知account物件的wait()函數醒來,再執行第四十五行while迴圈。 • 第五十一行balance=balance-amount是將銀行帳戶的balance減去amount金額,也就是從帳戶提款amount。 • 第四十一行使用this.notify()來告知這個物件,讓在wait()的物件醒來。
當銀行帳戶的金額小於提款時,這時後就會到wait等待的狀態,直到執行緒1,將存款存入notify(),而銀行帳戶的存款不小於0時才會顯示銀行帳戶的金額,因此看到的銀行帳戶金額都不小於0。當銀行帳戶的金額小於提款時,這時後就會到wait等待的狀態,直到執行緒1,將存款存入notify(),而銀行帳戶的存款不小於0時才會顯示銀行帳戶的金額,因此看到的銀行帳戶金額都不小於0。 • 死結deadlock的發生,當一個正在waiting的執行緒不能再動時,因為執行緒1在等待執行緒2來處理,但是執行緒2也同時不能再動,因為執行緒2等待執行緒1來處理。兩個執行緒互相等待對方,所以死結就發生了,兩個都不會動了。
1-5執行緒範例實作 • 在SynchTest類別中,我們將銀行Bank類別帳戶的錢作移動TransferRun,當某個帳戶的金額小於要移動的amount金額,這時它就會等待,等到其它帳戶將金錢移動到它帳戶時,而且這個帳戶的金額大於移動的金額amount時,這個帳戶的執行緒就會開始執行移動的動作。這是UML的圖。SynchTest為我們的主程式類別。 • TransferRun移動金錢類別實作Runnable介面。Thread執行緒是由TransferRun物件所組成,Thread類別會啟動TransferRun實體的run()函數。SynchTest類別則由Thread和Bank實體所組成。
範例:SynchTest.java • 第一行輸入java.util.concurrent.locks.* 套件的所有類別。 • 第十行到第十五行為for迴圈,它會執行十次迴圈。 • 第十二行新增TransferRun類別物件r。 • 第十三行新增執行緒類別物件r,並且將r物件(runnable)帶入建構子中。 • 第十四行使用t.start()來啟動執行緒。 • 第七十二行到第九十九行為TransferRun類別,它會實作runnable介面,它會完成run()函數。它是將銀行帳戶的錢從fromAccount移動到toAccount帳戶,使用move()函數。 • 第十八行到第七十行為Bank類別。 • 第二十三行到第三十行為Bank類別的建構子。 • 第二十八行新增ReentrantLock鎖類別物件為bankLock。 • 第二十九行使用ReenttrantLock鎖類別的方法newCondition()來新增Condition類別物件sufficientFunds。Condition條件類別的物件sufficientFunds可以和Lock實體一起用。 • 第三十一行到第四十九定義move()函數,它會丟出InterruptedException中斷例外,它是將from帳號的amount金額移動到to帳號去。 • 第三十三行使用bankLock.lock()的鎖。
第三十六行到第三十七行為while迴圈,當from帳號的金額小於amount移動的金額時,它就會等待,直到其它執行緒執行move()將錢移動到這個帳戶,然後在signalAll()通知它sufficientFunds,這時後才會再執行第三十六行的while迴圈來判別是否from的金額大於移動amount的金額,如果是則到第三十八行。第三十六行到第三十七行為while迴圈,當from帳號的金額小於amount移動的金額時,它就會等待,直到其它執行緒執行move()將錢移動到這個帳戶,然後在signalAll()通知它sufficientFunds,這時後才會再執行第三十六行的while迴圈來判別是否from的金額大於移動amount的金額,如果是則到第三十八行。 • 第三十八行會顯示目前執行緒的名稱Tread.currentThread()。 • 第三十八行到第四十二行為關鍵區域critical region,它會將from帳號的錢amount移動到to帳號去,也就是作一加一減的動作。 • 第四十三行是通知signal所有sufficientFunds條件鎖物件。 • 第四十七行最後解開鎖。 • 第五十行到第六十六行為getTotalBalance,它會將陣列accounts的所有元素值,也就是將所有帳號的金額加起來,這就是帳號總金額,然後回傳。 • 第五十二行到第六十四行為關鍵區域,所以使用鎖bankLock來將它鎖住。 • 第七十二行為TransferRun類別,它實作了Runnable介面。 • 第七十八行我們定義DELAY為0.1秒。在Java中百萬分之一秒為單位。 • 第七十九行到第八十四行為定義TransferRun建構子。 • 第八十五行到第九十八行為實作Runnable介面的run()函數。 • 每一個執行緒物件都有TransferRun類別的物件,它會執行run()函數。 • 第九十三行為移動帳戶間的移動金錢move()。 • 第九十四行為執行緒休息sleep()。