Java是第一個在語言級提供了對多線程程式設計支援的語言。語言級支援是啥意思呢,就是拋開作業系統,只使用Java內建的語句就能實現多線程。一般來講,多線程會依賴作業系統,我們要高用與系統相關的API才能建立多個線程,如在Windows中的CreateThread()函數。但JAVA就不用了。JAVA運行時系統實現了一個用於調用線程執行的線程調度器,用來決定在某一時刻哪個線程在CPU上運行。
在java技術中,線程通常是搶佔式的而不需要時間片分配進程(分配給每個線程相等的CPU時間的進程)。搶佔式調度模型就是許多線程處於可以運行狀態(等待狀態),但實際上只有一個線程在運行。該線程一直運行到它終止進入可運行狀態(等待狀態),或者另一個具有更高優先順序的線程變成可運行狀態。在後一種情況下,低優先順序的線程被高優先順序的線程搶佔,高優先順序的線程獲得啟動並執行機會。線程調度器支援不同優先順序線程的搶先方式,但其本身不支援相同優先順序線程的時間片輪換。若要想讓其支援,則必須將運行時系統運行在支援時間片輪換作業系統上。
1、Thread類和Runnable介面
在Java中實現多線程可以有2種方式,一個是讓類繼承Thread類,另一個是實現Runnable介面。由於Java不允許多繼承,所以當我們讓自訂的類繼承了Thread類後就不能再繼承其它類了,這樣會降低日後程式的可擴充性。所以一般我們都是採用實現Runnable介面的方法讓一個類具有可執行檔特性。
Runnable介面只有一個方法run(),該線程啟動時,JVM就會調用run()方法以執行該線程。線程類建立後,必須調用start()方法才能啟動線程。如:
package cls;public class ThreadDemo{ public static void main(String[] args) { //建立線程類對象並實現 Runnable介面 Thread th1 = new Thread(new Runnable() { // 實現Runnable中的run()方法 public void run() { while(true) System.out.println("Thread1 is running !"); } }); Thread th2 = new Thread(new Runnable() { public void run() { while(true) System.out.println("Thread2 is running !"); } }); // 調用start()方法啟動線程 th1.start(); th2.start(); }}
2、Daemon線程
一個Daemon線程指的是一個在後台執行服務的線程。在執行上例代碼時,我們發現,當主線程(main函數)執行完th2.start()這一行的時候,主線程的代碼就已經全部執行完,所以主線程退出了。但我們看到自己建立的th1和th2兩個線程仍然在不停地執行,並沒有退出。如果我們想讓建立的線程在主函數退出後一同退出,那麼就應當將我們建立的線程設定為daemon.Daemon線程當在所有的非Daemon線程都結束後自動結束。
讓我們修改一下上例:
package cls;public class ThreadDemo{ public static void main(String[] args) { //建立線程類對象並實現 Runnable介面 Thread th1 = new Thread(new Runnable() { // 實現Runnable中的run()方法 public void run() { while(true) System.out.println("Thread1 is running !"); } }); Thread th2 = new Thread(new Runnable() { public void run() { while(true) System.out.println("Thread2 is running !"); } }); // 設定成daemon線程 th1.setDaemon(true); th2.setDaemon(true); // 調用start()方法啟動線程 th1.start(); th2.start(); // 主函數睡眠一小會 try { Thread.currentThread().sleep(200); } catch(Exception e) { System.out.println(e.toString()); } }}
運行後可以看到,200ms後主線程退出,相應的th1和th2也退出了。
3、線程同步
假設對象A有兩個屬性a和b,另有一個對象B有屬性a和b,並且我們要用對象B來更新對象A。當這種更新操作完成後(對象A的a,b屬性值變成了對象B的a,b屬性值),則稱A與B同步。
如果對象B在更新對象A時(還沒更新完),又來了個對象C,對象C搶在B之前更新了A,則稱A與B不同步。
討論區對話就不得不考慮線程同步的問題。當多個線程共用同一個資料時,容易造成資料的不同步。而這種不同步引發的錯誤往往難以修正,常常是程式在執行了幾千次、幾萬次甚至是幾年才會出現。因此線程同步就顯得尤為重要。
下面寫一個類比火車站賣票的例子,故意使得線程不同步,看看會發生什麼問題:
package cls;public class ThreadSynchronized{ public static void main(String[] args) { Ticket tk = new Ticket(); Thread th1 = new Thread(new Sold(tk)); Thread th2 = new Thread(new Sold(tk)); th1.start(); th2.start(); }}class Ticket{ int amount = 100; // 有100張票 // 賣出一張票,總票數減1,返回被賣出的票號 public int sold() { return amount--; } // 返回當前剩餘票數 public int getAmount() { return amount; }}// 售票線程class Sold implements Runnable{ private Ticket tk; public Sold1(Ticket tk) { this.tk = tk; } public void run() { while(tk.getAmount() > 0) // 還有餘票 { try { Thread.currentThread().sleep(10); // 睡眠一小會 } catch(Exception e) { System.out.println(e.toString()); } System.out.println("Thread1 sold " + tk.sold()); } }}
執行:
我們發現,Thread1這個線程居然賣出了0這張不存在的票。售票線程在賣票前已經做了
tk.getAmount() > 0
的判斷了,為什麼還會賣出0這張票呢?
我們來分析下程式執行的過程:
當還剩下1這張票時,Thread1用getAmout()判斷出票數大於0,則進入while迴圈進行售票。在還沒有賣之前遇到了sleep(),於是Thread1會放棄執行機會,進入阻塞狀態;這時輪到Thread2執行了,由於Thead1還沒有把1賣出,因此Thread2用getAmout()的結果仍然大於0,所以就把1這張票賣出去了。這時候Thread1睡醒了,則繼續從sleep()這行開始執行,而這時候票數已經變成了0,那麼Thead1自然就會賣出0這張票了。
為了避免此類情況的發生,我們可以使用synchronized關鍵字來進行線程的同步。
synchronized關鍵字可以用在方法上,也可以用在語句塊中。我們可以形象地把synchronized想象成一把鎖。當程式執行到synchronized語句塊(以後稱為同步塊)時,會將這個同步塊的代碼“上鎖”,這時如果我們調用了sleep()方法或者恰好CPU分配的時間片用光了,另一個線程如果也要執行這段代碼時,由於先前的線程已經對這個同步塊進行了“上鎖”,因此這個線程就不能執行同步塊中的代碼,只能等待,直到它的時間片也用完為止。這時前一個線程又得到了執行的機會,執行完代碼塊,它會將這個代碼塊“解鎖”,這樣別的線程才可以執行這個代碼塊。也就是說,一個同步塊在一個時間段內只能被一個特定的線程執行,在這個線程沒有執行完代碼塊的語句之前,其它線程是沒有辦法再執行這個塊中的代碼的。Java是通過請求一個對象的監視器實現這種功能的。一個對象只有一個監視器,當一個線程請求到後,另一個線程在這個線程歸還(執行完同步塊)監視器之前是無法請求到該對象的監視器的。
現在我們來改寫上面的例子:
package cls;class ThreadSynchronized { public static void main(String[] args) { Ticket tk = new Ticket(); Sold sd = new Sold(tk); Thread th1 = new Thread(sd); Thread th2 = new Thread(sd); th1.start(); th2.start(); } } class Ticket { int amount = 100; // 有100張票 // 賣出一張票,總票數減1,返回被賣出的票號 public int sold() { return amount--; } // 返回當前剩餘票數 public int getAmount() { return amount; } } // 售票線程 class Sold implements Runnable { private Ticket tk; public Sold(Ticket tk) { this.tk = tk; } public void run() { while(true) { // 同步語句塊 synchronized(this)//請求 this對象的監視器 { if(tk.getAmount() > 0) { try { Thread.currentThread().sleep(1); // 睡眠一小會 } catch(Exception e) { System.out.println(e.toString()); } System.out.println(Thread.currentThread().getName() + " sold " + tk.sold()); } } } } }
可以看到,問題就解決了。