關於現代程序的編寫一般都是基於多處理或者多內核處理器來進行的,所以多線程並發處理設計成為提高應用程式運行效率的首選,我在前面的幾篇文章中都詳細介紹了Java語言對於多線程並發編程設計的一些主要內容和思想,本文將從一個整體的角度來串聯一下,有關程序,進程,多任務設計以及並發編程等概念,對Java多線程編程來個總結性說明。
程序(program),進程(process),多任務(multitasking),順序編程(Sequential Programming),並發編程(concrrent programming)。
我們知道程序就是使用某種特定的程式語言將一個算法表示出來。
那麼進程呢,就是這段程序在作業系統上會被讀取並分配了其所定義的所有系統資源而開始運行的一個實例。
通常一個進程在作業系統上運行必須有一個唯一標識符(PID),一個程序計數器(PC),包括可執行的代碼片段,還有可供使用的內存地址空間,以及能夠調用作業系統資源的句柄,當然這些之外還需要一個安全不受干擾的上下文環境等等。
程序計數器又被稱為指令指針,它是在CPU的寄存器里維護的一個值,用來追蹤CPU執行的指令地址。CPU執行完一條指令它的值會自動增加。
我們還可以將進程理解為作業系統中一個活動單元,或者一個工作單元,一個執行單元,或者一個執行路徑。
進程概念的定義能夠讓一個計算機系統支持多個執行單元運行。
多任務:是指作業系統能夠一次執行多個任務(即多個進程)的能力。
由於CPU每次只能執行一條指令,所以在單個CPU機器上真正意義上的多任務是不能實現的。
在這種情況下,作業系統通過將單個CPU的時間分配給所有正在運行的進程,並在進程之間進行足夠快的切換來實現多任務處理,從而給人一種所有進程都在同時運行的印象。
進程之間的CPU切換稱為上下文切換。
在上下文切換中,正在運行的進程被停止,它的狀態被保存,將要獲得CPU的進程的狀態被恢復,新的進程被運行。
有必要在將CPU分配給另一個進程之前保存正在運行的進程的狀態,這樣當這個進程再次獲得CPU時,它就可以從離開的地方開始執行。
通常,進程的狀態由程序計數器、進程使用的寄存器值和其他以後恢復進程所需的任何信息組成。
作業系統存儲進程狀態的數據結構,被稱為進程控制塊(PCB)。
但要注意,線程的上下文切換是相當昂貴的操作,儘量減少強制這類切換。
多任務處理有兩種類型:協同式和搶占式。
在協同多任務處理中,正在運行的進程決定何時釋放CPU,以便其他進程可以使用CPU。
在搶占式多任務處理中,作業系統為每個進程分配一個時間片。一旦一個進程用完它的時間片,它就會被搶占,作業系統就會把CPU分配給另一個進程。
在協同多任務處理中,一個進程可能長時間獨占CPU,其他進程可能沒有機會運行。
在搶占式多任務處理中,作業系統確保所有進程都能獲取CPU時間。
UNIX,OS/2和Windows使用搶占式的多任務處理,其中Windows 3.x使用協同式多任務處理。
多任務處理是指計算機同時使用多個處理器的能力。
並行處理是系統在多個處理器上同時執行同一任務的能力。
對於並行處理,必須將任務分解為多個子任務,以便可以在多個處理器上同時執行這些子任務。
假設我們設計一個包含六條指令的程序:
Instruction-1
Instruction-2
Instruction-3
Instruction-4
Instruction-5
Instruction-6
為了完整地執行這個程序,CPU必須執行所有六條指令,假設前三條指令相互依賴,即假設Instruction-2使用Instruction-1的結果,Instruction-3使用Instruction-2的結果。
假設後三條指令也像前三條指令一樣相互依賴。而前三條指令和後三條指令作為兩個組,彼此不依賴。
如果我們希望執行這六個指令以獲得最佳的結果該如何執行呢?
當然,最直接能想到的其中一種方法是按順序執行它們,遵照它們出現在程序中的順序被執行。
這樣執行的話,會為我們的程序提供一個執行序列,可以看成一個進程。
當然還可以有另一種執行方法,就是按兩個序列來執行。
比如執行一個序列Instruction-1, Instruction-2和 Instruction-3,同時,另一個執行序列會執行Instruction-4,Instruction-5, Instruction-6指令組。
其實我們可以看到,這兩組執行序列的「執行單位」和「執行順序」的是相同的,完全可以互換進行。
這裡要注意了,由於進程也是一個執行單元。所以,這兩組指令可以作為兩個進程來運行,如此我們就可以以在執行過程中實現並發性。
到目前為止,請注意我們假設這兩組指令是相互獨立的,執行過程中是相互無干擾的。
這時我們要問了,如果這兩組指令訪問一個共享內存會怎樣?
或者,當這兩組指令完成運行時,我們需要合併這兩組指令的結果來計算最終結果?
首先要注意,由於進程在運行時通常不允許訪問另一個進程的地址空間。
進程間的通信必須必須使用諸如socket、管道等進程間通信設施進行交互。
當多個進程需要通信或共享資源時,進程的本質是一個獨立於其他進程運行的代碼片段,所以可能會造成問題。
所有現代作業系統都允許我們在一個進程中創建多個執行單元來解決進程間無法通信這個問題,其中所有執行單元都可以共享分配給該進程的地址空間和資源。
進程中的每個執行單元稱為線程,即CPU每次可執行處理的單位。
我們知道,我們編寫一段代碼來解決一個問題,其實就是設計一段程序,讓其在作業系統上運行為一個進程,該進程至少有一個線程,這就是我們的主線程。Java編程里的Main函數啟用該進程。當然如果需要通過複雜步驟來解決問題,在可以為該進程創建多個線程。
通常一個進程可以創建的最大線程數由作業系統及其可用的資源數決定。前面我在多篇文章中專門對這個問題進行了討論,這裡就不再贅述了。
一個進程中的所有線程共享該線程管理的所有資源,包括分配給該進程的內存地址空間,同一進程中的所有線程可以很容易地相互通信,因為它們在相同的進程中執行操作,並且共享相同的內存。
由於一個進程中的每個線程獨立於同一進程中的其他線程運行,所以每個線程會獨自維護兩樣東西:
程序計數器和堆棧。
程序計數器讓線程跟蹤它當前正在執行的指令。
因為進程中的每個線程可能同時執行不同的指令,所以必須為每個線程維護單獨的程序計數器。
每個線程都維護自己的堆棧來存儲本地變量的值。
一個線程還可以維護它的私有內存,即使這些內存在同一個進程中它們也不能與其他線程共享私有內存。
線程維護的私有內存稱為線程本地存儲(TLS)。
其實現在所有的作業系統中,線程都是CPU調度執行的單位,而不是進程。也就是說CPU調度的不是進程,而是以線程為操作執行單位的。
所以,我們可以講CPU的上下文切換都是發生在線程之間的。
當然,進程也存在著在CPU上的運行切換,比如我們給某個程序以焦點,或者激活它。
與進程之間的上下文切換相比,線程之間的上下文切換成本更低。
由於易於在進程內的線程之間進行通信、共享資源以及更便宜的上下文切換,所以最好將程序拆分為多個線程,而不是多個進程。
有時線程也被稱為輕量級進程。如前所述,帶有六條指令的程序也可以在一個進程中分成兩個線程。
在多處理器機器上,一個進程的多個線程可能被調度在不同的處理器上,從而提供了一個程序的真正的並發執行。
所以我們可以將進程和線程之間的關係視為:
進程 = 地址空間 + 計算資源 + 線程
線程是進程內的執行單元,它們維護自己獨特的程序計數器和堆棧或者叫私有內存空間,這些線程共享進程的地址空間和所擁有管理的資源。
每個線程都可以被單獨安排在一個可用的CPU上執行。
由於現在Java的高並發應用程式編寫,需要對應用程式,進程,線程,並發和並行應用程式等概念有一個準確的理解,才能在程序設計實現過程中,準確的把握線程的應用,以及線程之間通信方式的選擇,才能更好的理解JDK提供的並發線程池的設計理念和用途。