不積跬步,無以致千里;不積小流,無以成江海。在學習Java多線程相關的知識前,咱們首先須要去了解一點操做系統的進程、線程以及相關的基礎概念。數據結構
一般,咱們把一個程序的執行稱爲一個進程。反過來說,進程用於描述程序的執行過程。所以,程序和進程是一對概念,它們分別描述了一個程序的靜態和動態特徵:除此以外,進程還操做系統進行資源分配的一個基本單位。多線程
進程使用fork
系統調用來建立。父進程調用fork
建立子進程。每一個子進程都是源自它的父進程的一個副本,它會得到父進程的數據段、堆和棧的副本,並與父進程共享代碼段。每一份副本都是獨立的,子進程對屬於它的副本的修改對其父進程和兄弟進程(同父進程)都是不可見的,反之亦然。全盤複製父進程的數據是一種至關低效的作法。 Linux操做系統內核使用寫時複製(Copy on Write,常簡稱爲COW)等技術來提升進程建立的效率。固然,剛建立的子進程也能夠經過系統調用exec把一個新的程序加載到己的內存中,而原先在內存中的數據段、堆、棧以及代碼段就會被替換掉,在這以後,子進程執行的就會是那個剛剛加載進來的新程序。併發
父進程被若是優先於子進程結束,那麼子進程就會被原來父進程的父進程「收養」。異步
爲了管理進程,內核必須對每一個進程的數據和行爲進行詳細的記錄,包括進程的優先級、狀態、虛擬地址範圍以及各類訪問權限等等。更具體地說,這些信息都會被記在每一個進程的進程描述符中。進程描述符並非一個簡單的符號,而是一個很是複雜的數據結構。保存在進程描述符中的進程ID (常稱爲PID )是進程在操做系統中的惟一標識,其中進程ID爲1的進程就是以前提到的內核啓動進程。進程id是一個非負整數且老是順序的編號,新建立的進程ID老是前一個進程ID遞增的結果。此外,進程ID也能夠重複使用。當進程ID達到其最大限值時,內核會從頭開始查找閒置的進程ID並使用M先找到的那一個做爲新進程的ID。另外,進程描述符中還會包含當前進程的父進程的ID (常稱爲PPID )。函數
若是多個進程之間須要協做完成任務,那麼進程間通訊的方式就是須要重點考慮的事項之一。這種通訊叫作IPC(Inter-Process Communication)。那麼在Linux中,從處理機制的角度看,能夠分爲三大類方法:工具
以數據爲傳送手段的IPC學習
以共享內存爲手段的IPCatom
kill -l
查看。在Linux中,每一個進程在每一個時刻只會有一種狀態,分別有如下六種spa
該進程馬上或正在CPU上運行。可是運行的時期是不肯定的,由進程調度來決定。操作系統
若是一個進程正在等待某個事件到來時,會進入此狀態。這樣的進程會被放入對應的等待隊列中。當事件發生時,對應的等待隊列中的一個或多個進程就會被喚醒。
此種狀態可與中斷的睡眠狀態的惟一區別是它不可被打斷。這意味着此種狀態的進程不會對任何信號做出響應。更確切地講,發送給此狀態的進程的信號直到它狀態轉出纔會被傳遞過去。處於此狀態的進程一般是在等待一個特殊的時間,好比等待同步的IO操做完成。
向進程發送SIGSTOP信號
,就會使該進程轉入暫停狀態,除非該進程正處於不可中斷的睡眠狀態。
向正處於暫停的進程發送SIGCONT
信號,會使用該進程轉向可運行狀態。處於該狀態的進程會暫停,並等待另外一個進程(跟蹤它的那個進程)對它進行操做。例如,咱們使用調試工具GDB在某個程序中設置一個斷點,然後對應的進程運行到該斷點處就會停下來。這時,該進程就處於跟蹤狀態。跟蹤狀態與暫停狀態很是相似。可是,向處於跟蹤狀態的進程發送SIGCONT信號並不能使它回覆。只有當調試進程進行了相應的系統調用或退出後,它纔可以恢復。
處於此狀態的進程即將結束運行,該進程佔用的絕大多數資源也都已經被回收,不過還有一些信息未仍是拿出,好比退出碼以及一些統計信息。之因此保留這些信息,主要是考慮到該進程的父進程可能須要它們。因爲此時的進程主體已經被刪除而只留下一個空殼,故此狀態才被稱爲殭屍狀態。
在進程退出的過程當中,有可能連退出碼和統計信息都不須要保留。形成這種狀況的緣由多是顯示地讓該進程的父進程忽略掉SIGCHLD信號(當一個進程消亡的時候,內核會給其父進程發送SIGCHLD信號以告知此狀況),也多是該進程已經被分離(分離即讓子進程和父進程分別獨立運行)。分離後的子程序將不會再使用和執行與父進程共享代碼段中的指令,而是加載並運行一個全新的程序。在這些狀況下,該進程處於退出的時候就不會轉入殭屍狀態,而會直接轉入退出狀態。處於退出狀態的進程會當即被幹淨利落地結束掉,它佔用的系統資源也會被操做系統自動回收。
內核爲每一個用戶進程分配的是虛擬內存而不是物理內存。同時,內核會把進程的虛擬內存劃分爲若干頁(page),而物理內存單元的劃分由CPU負責。一個物理內存單元被稱爲一個頁框(page freame)。不一樣進程的大多數頁都會與不一樣的頁框相對應。對應的時候那就是共享內存了。
線程能夠視爲進程中的控制流。一個進程至少包含一個線程,由於其餘至少會有一個控制流持續運行。於是,一個進程的第一個線程會隨着這個進程的啓動而建立,這個線程被稱爲該進程的主線程。固然,一個進程能夠包含多個線程。這些線程都是由當前線程中已經存在的線程建立出來的,建立的方法就是調用系統調用(pthread_create
)。擁有多個線程的進程能夠併發執行多個任務,而且即時某個或某些任務被阻塞,也不會影響其餘任務執行,這能夠大大改善程序的響應時間和吞吐量。另外一方面,線程不可能獨立於進程存在。它的生命週期不可能逾越所屬進程的生命週期。
一個進程中的全部線程都擁有本身線程棧,並以此存儲本身的私有數據。這些線程的線程棧都包含在其所屬進程的虛擬內存地址中。不過要注意,一個進程中的不少資源都會被其中的全部線程共享,這些被線程共享的資源包含當前進程所持有文件描述符,等等。正由於如此,同一個進程的多個線程運行的必定是同一個程序,只不過具體的控制流程的執行函數可能有所不一樣。在同一個進程的多個線程之間共享數據也是一件很是輕鬆和天然的事情。另外,建立一個新線程,也不會像建立一個新進程那樣耗時費力,由於在其所屬進程的虛擬內存地址中存儲的代碼、數據和資源都不須要被複制。
另外,操做系統和提供了必定的系統調用用於管理當前進程中的線程。
和進程同樣,每一個線程都有本身的ID(由內核分配),叫作線程ID或者TID。可是在操做系統範圍內不惟一,在所屬進程的範圍內惟一。
任何一個線程均可以同一線程中的其餘線程進行有限管理,以下:
主線程在其所屬進程啓動時建立。其餘線程能夠經過別的線程用pthread_create來建立——要傳入新線程將要執行的函數以及傳入該函數的參數值。在建立成功的時候,該函數會返回線程的TID。
線程能夠經過多種方式來終結同一個進程中的其餘線程。其餘一種方式就是調用系統調用pthread_cancel
,其做用是取消掉給定線程ID表明的那個線程。更確切地講,它會向目標線程發送一個請求,要求它馬上終止執行。可是該函數只是發送請求並便可返回。可是,該函數只是發送請求並馬上返回,而不會等待目標線程對該請求作出響應。至於目標線程何時對此作出線程、怎麼樣的響應,則取決與另外的因素(好比線程目標的取消狀態及類型)。在默認狀況下,目標線程老是會接受線程取消請求,不過等到時機成熟(執行到某個取消點)的時候,目標線程纔會響應線程的取消請求。
此操做由系統調用pthread_join
來執行,該函數會一直等待與給定的線程ID對應的那個線程終止,並把線程執行的pthread_create
函數的返回值告知調用線程。若是目標線程已經處於終止狀態,那麼該函數會當即返回。這就像是把調用線程放置在了目標線程的後面,當目標線程把線程控制權交出時,調用線程會接過流程控制權並繼續執行pthread_join函數調用以後的代碼。這也把這一操做稱爲鏈接
的原因之一。實際上,若是一個線程可被鏈接,那麼在它終止以前就必須鏈接,不然就會變成一個殭屍線程。殭屍線程不但會致使系統資源浪費,還會無心義減小其進程的可建立線程數量。
將一個線程分離後那麼它將變得不可鏈接。而在默認狀況下,一個線程老是能夠被鏈接的。分離操做的另外一個做用是讓操做系統內核在目標線程終止時自行進行清理和銷燬工做。注意,分離操做是不可逆的。也就是說,咱們沒法使一個不可鏈接的線程變回可鏈接的狀態。不過,對於一個已處於分離狀態的線程,執行終止操做仍然會起做用。分離操做由系統調用pthread_detach來執行,它接受一個表明了線程ID的參數值。
一個線程對自身也能夠進行兩種控制:終止和分離。線程終止自身的方式有不少種。在線程執行的start函數中執行return語句,會使該線程隨着start函數的結束而終止。須要注意的是,若是在主線程中執行了return語句,那麼當前進程中的全部線程都會終止。另外,在任意線程中調用系統調用exit也會達到這種效果。還有一種終止自身的方式就是顯示調用pthread_exit。
而分離pthread_detach函數則是傳入本身的TID。
在多個線程之間交換線程是很是簡單和天然的事,而在多個進程之間只能經過一些額外的手段(好比管道、消息隊列、信號量和共享內存區)傳遞數據。顯然,使用這些額外手段會增長開發成本。不過,線程間交換數據雖然簡單但卻因爲可能發生競態條件而不得不使用一些同步工具(好比互斥量和條件變量)加以保護。這些與業務邏輯無關的代碼會增長程序的複雜度,尤爲在使用不當的狀況下還會引發災難。
互斥量能夠理解爲咱們常見的鎖。而條件變量所作的就是保證線程間共享的數據狀態改變時通知到其餘所以而被阻塞的線程。條件變量老是與互斥量組合使用。當線程成功鎖定互斥量並訪問到共享數據時,共享數據的狀態並不必定知足它的要求。下面就經過一個示例來描述條件變量的使用場景。
執行過程不能中斷的操做稱爲原子操做(atomic operation)。必須一個單一的彙編指令表示,並且須要獲得芯片級別的支持。
臨界區(critical section)用來表示一種公共資源或者共享數據,能夠被多個線程使用。可是每一次,只有一個線程可使它,一旦臨界區資源被佔用,其餘線程要想使用資源,就必須等待,即串行化訪問或執行。
保證只有一個進程或線程在臨界區內的作法只有一個——互斥(mutual exclusion。簡稱 mutex)。
描述的是用戶線程與內核的交互方式:
描述的是用戶線程調用內核 I/O 操做的方式:
一個 I/O 操做其實分紅了兩個步驟:
阻塞 I/O 和非阻塞 I/O 的區別在於第一步,發起 I/O 請求是否會被阻塞。若是阻塞直到完成那麼就是傳統的阻塞 I/O ,若是不阻塞,那麼就是非阻塞 I/O 。 同步 I/O 和異步 I/O 的區別就在於第二個步驟是否阻塞,若是實際的 I/O 讀寫阻塞請求進程,那麼就是同步 I/O 。
併發和並行每每被人所混淆。它們均可以表示兩個或多個任務一塊兒執行,可是偏重點有些不一樣。併發偏重於多個任務交替執行,而多個任務有可能仍是串行。而並行則是真正意義上的「同時執行」。
嚴格來講,並行的多個任務是真實的同時執行,而對併發來講,這個過程這是交替的,一下子運行任務A一下子執行任務B,系統會不停地在二者間切換。但對於外部觀察者來講,即便多個任務之間是串行併發的,也會形成多任務間是並行執行的錯覺。
死鎖、飢餓和活鎖都屬於多線程的活躍性問題,若是發生上述狀況,那麼相關線程可能就再也不活躍,也就是說它可能很難繼續往下執行了。
死鎖應該是最糟糕的一種狀況了,雖然別的狀況也沒有好到哪兒去。