一、并發(fā)簡史
早期的計算機中不包含操作系統,它們從頭到尾只執(zhí)行一個程序,并且這個程序能訪問計算機中所有的資源。在這種裸機環(huán)境中,不僅很難編寫和運行程序,而且每次只能運行一個程序,這對于昂貴并且稀有的計算機資源來說也是一種浪費。為此,現代計算機中加入了操作系統來支持多個程序同時執(zhí)行。這主要基于以下考慮:
資源利用率:在某些情況下,程序必須等待某個外部操作完成,例如輸入操作或輸出操作等,而在等待時程序無法執(zhí)行其他任何工作。因此,如果在等待的同時可以運行另一個程序,那么無疑將提高資源的利用率。
公平性:不同的用戶和程序對于計算機上的資源有著相同的使用權。一種高效的運行方式是通過粗粒度的時間分片使這些用戶和程序能共享計算機資源,而不是由一個程序從頭運行到尾,然后再啟動下一個程序。
便利性:通常來說,在計算多個任務時,應該編寫多個程序,每個程序執(zhí)行一個任務并在必要時相互通信,這比只編寫一個程序來計算所有任務更容易實現。
二、進程和線程的概念
進程
進程,是計算機中的程序關于某數據集合上的一次運行活動,是系統進行資源分配和調度的基本單位,是操作系統結構的基礎。我們可以理解為一個應用就是一個進程,比如說QQ。
線程
是操作系統能夠進行運算調度的最小單位。它被包含在進程之中,是進程中的實際運作單位。一條線程指的是進程中一個單一順序的控制流,一個進程中可以并發(fā)多個線程,每條線程并行執(zhí)行不同的任務。像QQ.exe運行的時候就有很多子任務在運行,比如聊天線程、好友視頻線程、下載文件線程等等。
三、為什么要使用多線程
如果使用得當,線程可以有效地降低程序的開發(fā)和維護等成本,同時提升復雜應用程序的性能。具體說,線程的優(yōu)勢有:
1、發(fā)揮多處理器的強大能力
現在,多處理器系統正日益盛行,并且價格不斷降低,即時在低端服務器和中斷桌面系統中,通常也會采用多個處理器,這種趨勢還在進一步加快,因為通過提高時鐘頻率來提升性能已變得越來越困難,處理器生產廠商都開始轉而在單個芯片上放置多個處理器核。試想,如果只有單個線程,雙核處理器系統上程序只能使用一半的CPU資源,擁有100個處理器的系統上將有99%的資源無法使用。多線程程序則可以同時在多個處理器上執(zhí)行,如果設計正確,多線程程序可以通過提高處理器資源的利用率來提升系統吞吐率。
2、在單處理器系統上獲得更高的吞吐率
如果程序是單線程的,那么當程序等待某個同步I/O操作完成時,處理器將處于空閑狀態(tài)。而在多線程程序中,如果一個線程在等待I/O操作完成,另一個線程可以繼續(xù)運行,使得程序能在I/O阻塞期間繼續(xù)運行。
3、建模的簡單性
通過使用線程,可以將復雜并且異步的工作流進一步分解為一組簡單并且同步的工作流,每個工作流在一個單獨的線程中運行,并在特定的同步位置進行交互。我們可以通過一些現有框架來實現上述目標,例如Servlet和RMI,框架負責解決一些細節(jié)問題,例如請求管理、線程創(chuàng)建、負載平衡,并在正確的時候將請求分發(fā)給正確的應用程序組件。編寫Servlet的開發(fā)人員不需要了解多少請求在同一時刻要被處理,也不需要了解套接字的輸入流或輸出流是否被阻塞,當調用Servlet的service方法來響應Web請求時,可以以同步的方式來處理這個請求,就好像它是一個單線程程序。
4、異步事件的簡化處理
服務器應用程序在接受多個來自遠程客戶端的套接字連接請求時,如果為每個連接都分配其各自的線程并且使用同步I/O,那么就會降低這類程序的開發(fā)難度。如果某個應用程序對套接字執(zhí)行讀操作而此時還沒有數據到來,那么這個讀操作將一直阻塞,直到有數據到達。在單線程應用程序中,這不僅意味著在處理請求的過程中將停頓,而且還意味著在這個線程被阻塞期間,對所有請求的處理都將停頓。為了避免這個問題,單線程服務器應用程序必須使用非阻塞I/O,但是這種I/O的復雜性要遠遠高于同步I/O,并且很容易出錯。然而,如果每個請求都擁有自己的處理線程,那么在處理某個請求時發(fā)生的阻塞將不會影響其他請求的處理。
四、線程帶來的風險
任何事物都具有兩面性,多線程也不例外,多線程用好了非常爽,用不好讓就痛苦非常。下面就來了解一下多線程存在的一些風險:
1、安全問題
線程安全問題是非常復雜的,在沒有充足的同步情況下,多個線程的操作執(zhí)行順序是不可預測的,這會產生非常奇怪的結果。如下代碼,在單線程環(huán)境沒有問題,但是在多線程環(huán)境,就會出現不可預料的結果
public class UnsafeSequence {
private int value;
/** 返回一個唯一的數值**/
public int getValue() {
return value++;// 該操作包含三個子操作:從內存中讀取value到線程的工作內存、在線程的的工作內存將value+1、并將結果寫回主內存
}
}
由于,value++涉及到三個子操作,而CPU在為每個線程分配時間片去執(zhí)行時,如果不能在該時間片內一起完成這三個子操作,那么就可能被其他線程捷足先登,修改了原來的的value值,而等CPU再分配給這個線程的時間片后,該線程去執(zhí)行剩下的操作,就可能覆蓋已經修改的value值,導致數據不一致。
2、性能問題
可能你會疑惑,不是說多線程是提升服務性能的么?怎么又會有性能問題?這是因為在多線程程序中,當線程調度器臨時掛起活躍線程并轉而運行另外一個線程,就會頻繁的出現上下文切換(Context Switch)操作,這種操作將帶來極大的開銷:保存和恢復執(zhí)行上下文,丟失局部性,并且CPU時間將更多的花在線程調度而不是線程運行上。當線程共享數據時,必須使用同步機制,而這些機制往往會抑制某些編譯器優(yōu)化,是內存緩存區(qū)中的數據無效,以及增加共享內存總線的同步流量。
3、活躍性問題
在開發(fā)并發(fā)代碼時一定要注意,線程安全性是可不被破壞的。安全性不僅對于多線程很重要,對單線程同樣重要。安全性的含義是“永遠不要發(fā)生糟糕的事情”,而活躍性的含義是“某件正確的事情最終會發(fā)生”。當某個操作無法繼續(xù)執(zhí)行下去時,就會發(fā)生活躍性問題,比如線程A等待線程B釋放資源,而線程B永遠不會釋放資源,那么A就會永久等待下去。就是我們常說的死鎖問題。
五、線程狀態(tài)
虛擬機中的線程狀態(tài)有六種,定義在Thread.State中:
1、新建狀態(tài)NEW
new了但是沒有啟動的線程的狀態(tài)。比如"Thread t = new Thread()",t就是一個處于NEW狀態(tài)的線程
2、可運行狀態(tài)RUNNABLE
new出來線程,調用start()方法即處于RUNNABLE狀態(tài)了。處于RUNNABLE狀態(tài)的線程可能正在Java虛擬機中運行,也可能正在等待處理器的資源,因為一個線程必須獲得CPU的資源后,才可以運行其run()方法中的內容,否則排隊等待
3、阻塞BLOCKED
如果某一線程正在等待監(jiān)視器鎖,以便進入一個同步的塊/方法,那么這個線程的狀態(tài)就是阻塞BLOCKED
4、等待WAITING
某一線程因為調用不帶超時的Object的wait()方法、不帶超時的Thread的join()方法、LockSupport的park()方法,就會處于等待WAITING狀態(tài)
5、超時等待TIMED_WAITING
某一線程因為調用帶有指定正等待時間的Object的wait()方法、Thread的join()方法、Thread的sleep()方法、LockSupport的parkNanos()方法、LockSupport的parkUntil()方法,就會處于超時等待TIMED_WAITING狀態(tài)
6、終止狀態(tài)TERMINATED
線程調用終止或者run()方法執(zhí)行結束后,線程即處于終止狀態(tài)。處于終止狀態(tài)的線程不具備繼續(xù)運行的能力