標籤:代碼塊 his out 動作 修改 代碼 rgs static 延遲
多線程編程為程式開發帶來了很多的方便,但是也帶來了一些問題,這些問題是在程式開發過程中必須進行處理的問題。
這些問題的核心是,如果多個線程同時訪問一個資源,例如變數、檔案等,時如何保證訪問安全的問題。在多線程編程中,這種會被多個線程同時訪問的資源叫做臨界資源。
下面通過一個簡單的樣本,示範多個線程訪問臨界資源時產生的問題。在該樣本中,啟動了兩個線程類DataThread的對象,該線程每隔200毫秒輸出一次變數n的值,並將n的值減少1。變數n的值儲存在類比臨界資源的Data類中,該樣本的核心是兩個線程類都使用同一個Data類的對象,這樣Data類的這個對象就是一個臨界資源了。
範例程式碼如下:
package syn1;/** * 類比臨界資源的類 */public class Data { public int n; public Data(){ n = 60; }}
package syn1;/** * 測試多線程訪問時的問題 */public class TestMulThread1 { public static void main(String[] args) { Data data = new Data(); DataThread d1 = new DataThread(data,"線程1"); DataThread d2 = new DataThread(data,"線程2"); }}
package syn1;/** * 訪問資料的線程 */public class DataThread extends Thread { Data data; String name; public DataThread(Data data,String name){ this.data = data; this.name = name; start(); } public void run(){ try{ for(int i = 0;i < 10;i++){ System.out.println(name + ":" + data.n); data.n--; Thread.sleep(200); } }catch(Exception e){} }}
在運行時,因為不同情況下該程式的運行結果會出現不同,該程式的一種執行結果為:
線程1:60線程2:60線程2:58線程1:58線程2:56線程1:56線程2:54線程1:54線程2:52線程1:52線程2:50線程1:50線程2:48線程1:48線程2:47線程1:46線程2:44線程1:44線程2:42線程1:42
View Code
從執行結果來看,第一次都輸出60是可以理解的,因為線程在執行時首先輸出變數的值,這個時候變數n的值還是初始值60,而後續的輸出就比較麻煩了,在開始的時候兩個變數保持一致的輸出,而不是依次輸出n的每個值的內容,而到將要結束時,線程2輸出47這個中間數值。
出現這種結果的原因很簡單:線程1改變了變數n的值以後,還沒有來得及輸出,這個變數n的值就被線程2給改變了,所以在輸出時看的輸出都是跳躍的,偶爾出現了連續。
出現這個問題也比較容易接受,因為最基本的多線程程式,系統只保證線程同時執行,至於哪個先執行,哪個後執行,或者執行中會出現一個線程執行到一半,就把CPU的執行權交給了另外一個線程,這樣線程的執行順序是隨機的,不受控制的。所以會出現上面的結果。
這種結果在很多實際應用中是不能被接受的,例如銀行的應用,兩個人同時取一個賬戶的存款,一個使用存摺、一個使用卡,這樣訪問賬戶的金額就會出現問題。或者是售票系統中,如果也這樣就出現有人買到相同座位的票,而有些座位的票卻未售出。
在多線程編程中,這個是一個典型的臨界資源問題,解決這個問題最基本,最簡單的思路就是使用同步關鍵字synchronized。
synchronized關鍵字是一個修飾符,可以修飾方法或代碼塊,其的作用就是,對於同一個對象(不是一個類的不同對象), 當多個線程都同時調用該方法或代碼塊時,必須依次執行,也就是說,如果兩個或兩個以上的線程同時執行該段代碼時,如果一個線程已經開始執行該段代碼,則另 外一個線程必須等待這個線程執行完這段代碼才能開始執行。就和在銀行的櫃檯辦理業務一樣,營業員就是這個對象,每個顧客就好比線程,當一個顧客開始辦理 時,其它顧客都必須等待,及時這個正在辦理的顧客在辦理過程中接了一個電話 (類比於這個線程釋放了佔用CPU的時間,而處於阻塞狀態),其它線程也只能等待。
使用synchronized關鍵字修改以後的上面的代碼為:
package syn2;/** * 類比臨界資源的類 */public class Data2 { public int n; public Data2(){ n = 60; } public synchronized void action(String name){ System.out.println(name + ":" + n); n--; }}
package syn2;/** * 測試多線程訪問時的問題 */public class TestMulThread2 { public static void main(String[] args) { Data2 data = new Data2(); Data2Thread d1 = new Data2Thread(data,"線程1"); Data2Thread d2 = new Data2Thread(data,"線程2"); }}
package syn2;/** * 訪問資料的線程 */public class Data2Thread extends Thread { Data2 data; String name; public Data2Thread(Data2 data,String name){ this.data = data; this.name = name; start(); } public void run(){ try{ for(int i = 0;i < 10;i++){ data.action(name); Thread.sleep(200); } }catch(Exception e){} }}
該範例程式碼的執行結果會出現不同,一種執行結果為:
線程1:60線程2:59線程2:58線程1:57線程2:56線程1:55線程2:54線程1:53線程2:52線程1:51線程2:50線程1:49線程1:48線程2:47線程2:46線程1:45線程2:44線程1:43線程2:42線程1:41
View Code
在該樣本中,將列印變數n的代碼和變數n變化的程式碼群組成一個專門的方法action,並且使用修飾符synchronized修改該方法,也就是說對於一個Data2的對象,無論多少個線程同時調用action方法時,只有一個線程完全執行完該方法以後,別的線程才能夠執行該方法。這就相當於一個線程執行到該對象的synchronized方法時,就為這個對象加上了一把鎖,鎖住了這個對象,別的線程在調用該方法時,發現了這把鎖以後就繼續等待下去了。
如果這個例子還不能協助你理解如何解決多線程的問題,那麼下面再來看一個更加實際的例子——衛生間問題。
例 如火車上車廂的衛生間,為了簡單,這裡只類比一個衛生間,這個衛生間會被多個人同時使用,在實際使用時,當一個人進入衛生間時則會把衛生間鎖上,等出來時 開啟門,下一個人進去把門鎖上,如果有一個人在衛生間內部則別人的人發現門是鎖的則只能在外面等待。從編程的角度來看,這裡的每個人都可以看作是一個線程 對象,而這個衛生間對象由於被多個線程訪問,則就是臨界資源,在一個線程實際使用時,使用synchronized關鍵將臨界資源鎖定,當結束時,釋放鎖定。實現的代碼如下:
package syn3;/** * 測試類別 */public class TestHuman { public static void main(String[] args) { Toilet t = new Toilet(); //衛生間對象 Human h1 = new Human("1",t); Human h2 = new Human("2",t); Human h3 = new Human("3",t); }}
package syn3;/** * 人線程類,示範互斥 */public class Human extends Thread { Toilet t; String name; public Human(String name,Toilet t){ this.name = name; this.t = t; start(); //啟動線程 } public void run(){ //進入衛生間 t.enter(name); }}
package syn3;/** * 衛生間,互斥的示範 */public class Toilet { public synchronized void enter(String name){ System.out.println(name + "已進入!"); try{ Thread.sleep(2000); }catch(Exception e){} System.out.println(name + "離開!"); }}
該樣本的執行結果為,不同次數下執行結果會有所不同:
1已進入!1離開!3已進入!3離開!2已進入!2離開!
View Code
在該範例程式碼中,Toilet類表示衛生間類,Human類類比人,是該樣本中的線程類,TestHuman類是測試類別,用於啟動線程。在TestHuman中,首先建立一個Toilet類型的對象t,並將該對象傳遞到後續建立的線程對象中,這樣後續的線程對象就使用同一個Toilet對象,該對象就成為了臨界資源。下面建立了三個Human類型的線程對象,每個線程具有自己的名稱name參數,類比3個線程,在每個線程對象中,只是調用對象t中的enter方法,類比進入衛生間的動作,在enter方法中,在進入時輸出調用該方法的線程進入,然後延遲2秒,輸出該線程離開,然後後續的一個線程進入,直到三個線程都完成enter方法則程式結束。
在該樣本中,同一個Toilet類的對象t的enter方法由於具有synchronized修飾符修飾,則在多個線程同時調用該方法時,如果一個線程進入到enter方法內部,則為對象t上鎖,直到enter方法結束以後釋放對該對象的鎖定,通過這種方式實現無論多少個Human類型的線程,對於同一個對象t,任何時候只能有一個線程執行enter方法,這就是解決多線程問題的第一種思路——互斥的解決原理。
JAVA多線程的問題以及處理(一)【轉】