程式和執行緒
談到多執行緒,就得先講程式和執行緒的概念。
程式
程式可以理解為受作業系統管理的基本執行單元。360瀏覽器是一個程式、WPS也是一個程式,正在作業系統中執行的".exe"都可以理解為一個程式
執行緒
程式中獨立執行的子任務就是一個執行緒。像QQ.exe執行的時候就有很多子任務在執行,比如聊天執行緒、好友視訊執行緒、下載檔案執行緒等等。
為什麼要使用多執行緒
如果使用得當,執行緒可以有效地降低程式的開發和維護等成本,同時提升複雜應用程式的效能。具體說,執行緒的優勢有:
1、發揮多處理器的強大能力
現在,多處理器系統正日益盛行,並且價格不斷降低,即時在低端伺服器和中斷桌面系統中,通常也會採用多個處理器,這種趨勢還在進一步加快,因為通過提高時脈頻率來提升效能已變得越來越困難,處理器生產廠商都開始轉而在單個晶片上放置多個處理器核。試想,如果只有單個執行緒,雙核處理器系統上程式只能使用一半的CPU資源,擁有100個處理器的系統上將有99%的資源無法使用。多執行緒程式則可以同時在多個處理器上執行,如果設計正確,多執行緒程式可以通過提高處理器資源的利用率來提升系統吞吐率。
2、在單處理器系統上獲得更高的吞吐率
如果程式是單執行緒的,那麼當程式等待某個同步I/O操作完成時,處理器將處於空閒狀態。而在多執行緒程式中,如果一個執行緒在等待I/O操作完成,另一個執行緒可以繼續執行,使得程式能在I/O阻塞期間繼續執行。
3、建模的簡單性
通過使用執行緒,可以將複雜並且非同步的工作流進一步分解為一組簡單並且同步的工作流,每個工作流在一個單獨的執行緒中執行,並在特定的同步位置進行互動。我們可以通過一些現有框架來實現上述目標,例如Servlet和RMI,框架負責解決一些細節問題,例如請求管理、執行緒建立、負載平衡,並在正確的時候將請求分發給正確的應用程式元件。編寫Servlet的開發人員不需要了解多少請求在同一時刻要被處理,也不需要了解套接字的輸入流或輸出流是否被阻塞,當呼叫Servlet的service方法來響應Web請求時,可以以同步的方式來處理這個請求,就好像它是一個單執行緒程式。
4、非同步事件的簡化處理
伺服器應用程式在接受多個來自遠端客戶端的套接字連線請求時,如果為每個連線都分配其各自的執行緒並且使用同步I/O,那麼就會降低這類程式的開發難度。如果某個應用程式對套接字執行讀操作而此時還沒有資料到來,那麼這個讀操作將一直阻塞,直到有資料到達。在單執行緒應用程式中,這不僅意味著在處理請求的過程中將停頓,而且還意味著在這個執行緒被阻塞期間,對所有請求的處理都將停頓。為了避免這個問題,單執行緒伺服器應用程式必須使用非阻塞I/O,但是這種I/O的複雜性要遠遠高於同步I/O,並且很容易出錯。然而,如果每個請求都擁有自己的處理執行緒,那麼在處理某個請求時發生的阻塞將不會影響其他請求的處理。
建立執行緒的方式
建立執行緒有兩種方式:
1、繼承Thread,重寫父類的run()方法。
public class MyThread00 extends Thread { public void run() { for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + "在執行!"); } } }
public static void main(String[] args) { MyThread00 mt0 = new MyThread00(); mt0.start(); for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + "在執行!"); } }
看一下執行結果:
main在執行! Thread-0在執行! main在執行! Thread-0在執行! main在執行! Thread-0在執行! main在執行! Thread-0在執行! Thread-0在執行! main在執行!
看到main執行緒和Thread-0執行緒交替執行,效果十分明顯。
有可能有些人看不到這麼明顯的效果,這也很正常。所謂的多執行緒,指的是兩個執行緒的程式碼可以同時執行,而不必一個執行緒需要等待另一個執行緒內的程式碼執行完才可以執行。對於單核CPU來說,是無法做到真正的多執行緒的,每個時間點上,CPU都會執行特定的程式碼,由於CPU執行程式碼時間很快,所以兩個執行緒的程式碼交替執行看起來像是同時執行的一樣。那具體執行某段程式碼多少時間,就和分時機制系統有關了。分時系統把CPU時間劃分為多個時間片,作業系統以時間片為單位片為單位各個執行緒的程式碼,越好的CPU分出的時間片越小。所以看不到明顯效果也很正常,一個執行緒列印5句話本來就很快,可能在分出的時間片內就執行完成了。所以,最簡單的解決辦法就是把for迴圈的值調大一點就可以了(也可以在for迴圈里加Thread.sleep方法,這個之後再說)。
2、實現Runnable介面。和繼承自Thread類差不多,不過實現Runnable後,還是要通過一個Thread來啟動:
public class MyThread01 implements Runnable { public void run() { for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + "在執行!"); } } }
public static void main(String[] args) { MyThread01 mt0 = new MyThread01(); Thread t = new Thread(mt0); t.start(); for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + "在執行!"); } }
效果也十分明顯:
main在執行! Thread-0在執行! main在執行! Thread-0在執行! main在執行! Thread-0在執行! main在執行! Thread-0在執行! main在執行! Thread-0在執行!
兩種多執行緒實現方式的對比
看一下Thread類的API:
其實Thread類也是實現的Runnable介面。兩種實現方式對比的關鍵就在於extends和implements的對比,當然是後者好。因為第一,繼承只能單繼承,實現可以多實現;第二,實現的方式對比繼承的方式,也有利於減小程式之間的耦合。
因此,多執行緒的實現幾乎都是使用的Runnable介面的方式。不過,後面的文章,為了簡單,就用繼承Thread類的方式了。
執行緒狀態
虛擬機器中的執行緒狀態有六種,定義在Thread.State中:
1、新建狀態NEW
new了但是沒有啟動的執行緒的狀態。比如"Thread t = new Thread()",t就是一個處於NEW狀態的執行緒
2、可執行狀態RUNNABLE
new出來執行緒,呼叫start()方法即處於RUNNABLE狀態了。處於RUNNABLE狀態的執行緒可能正在Java虛擬機器中執行,也可能正在等待處理器的資源,因為一個執行緒必須獲得CPU的資源後,才可以執行其run()方法中的內容,否則排隊等待
3、阻塞BLOCKED
如果某一執行緒正在等待監視器鎖,以便進入一個同步的塊/方法,那麼這個執行緒的狀態就是阻塞BLOCKED
4、等待WAITING
某一執行緒因為呼叫不帶超時的Object的wait()方法、不帶超時的Thread的join()方法、LockSupport的park()方法,就會處於等待WAITING狀態
5、超時等待TIMED_WAITING
某一執行緒因為呼叫帶有指定正等待時間的Object的wait()方法、Thread的join()方法、Thread的sleep()方法、LockSupport的parkNanos()方法、LockSupport的parkUntil()方法,就會處於超時等待TIMED_WAITING狀態
6、終止狀態TERMINATED
執行緒呼叫終止或者run()方法執行結束後,執行緒即處於終止狀態。處於終止狀態的執行緒不具備繼續執行的能力