前言
在Java併發程式設計中執行緒的使用尤為重要。瞭解執行緒的由來,使用場景及注意事項是作為一個合格的Java程式設計師必備的技能。本文章中會對執行緒的由來、程式與執行緒的區別、及執行緒的使用場景進行簡單介紹。希望通過該文章,小夥伴們能對執行緒有一個更深的瞭解。
從作業系統發展瞭解執行緒
執行緒的出現,離不開程式。而程式的出現又離不開作業系統。作業系統的發展促進了執行緒與程式的技術崛起。所以瞭解作業系統的發展,對我們理解執行緒尤為重要。整個作業系統大致分為如下幾個階段:
- 手工操作
- 單道批處理系統
- 多道批處理系統
- 分時作業系統
- 等等等
在前三個階段中,對程式與執行緒的理解尤為重要,故文章會著重介紹前三個階段。
手工操作
最早的計算機並位出現真正意義上的作業系統,這時的計算機智慧解決簡單的數學問題,比如正鉉,餘鉉等。其執行方式也特別簡單,程式設計師將對應於程式和資料的已穿孔的紙帶(或卡片)裝入輸入機,然後啟動輸入機把程式和資料輸入計算機記憶體,接著通過控制檯開關啟動程式針對資料運。計算完畢後,輸出機輸出計算結果;使用者取走結果並卸下紙帶(或卡片)後,才讓下一個使用者上機。舉個簡單的列子,假設我們需要向計算機傳送吃飯、洗澡、睡覺這三個指令,那麼在傳統計算機中,我們可以得到下圖:
從上圖中我們可以明顯看出,手工操作方式嚴重損害了系統的利用率。在等待使用者輸入指令時,計算機一直處於閒置狀態。
單道批處理系統
為了擺脫手動操作帶來的耗時性,實現作業(程式、資料、指令)的自動過渡。接著又出現了單道批處理系統。單道批處理系統在原來手動操作主要的區別是在輸入機與主機之間增加了一個儲存裝置磁帶(盤)
(下圖,紅色虛線部分),並在主機系統上配上監督程式,其具體執行方式通常是把一批作業以輸入到磁帶上,然後由監督程式將磁帶上的第一個作業裝入記憶體,並把執行控制權交給該作業。當該作業處理完成時,又把控制權交還給監督程式,再由監督程式把磁帶(盤)上的第二個作業調入記憶體。計算機系統就這樣自動地一個作業一個作業地進行處理,直至磁帶上的所有作業全部完成。還是以上文吃飯、洗澡、睡覺這三個指令為例子,我們可以得到下圖:
需要注意的是,雖然單道批處理作業系統能夠解決手動操作時需要人工切換作業導致的系統利用率低的問題,但是又因為單道批處理系統是將作業一個一個加入記憶體的,那麼某一個作業因為等待磁帶(盤)或者其他I/O操作而暫停時,那計算機就只能一直阻塞,直到該I/O完成。對於CPU操作密集型的程式,I/O操作相對較少,因此浪費的時間也很少。但是對於I/O操作較多的場景來說,CPU的資源是屬於嚴重浪費的。
多道批處理系統
為了解決單道批處理系統因為輸入/輸出(I/O)請求後,導致計算機等待I/O完成而造成的計算機的資源的浪費。接下來又出現了多道批處理系統。多道批處理系統與單道批處理系統的主要區別是在記憶體中允許一個或多個作業
,當一個作業在等待I/O處理時,多批處理系統會通過相應排程演算法排程另外一個作業讓計算機執行。從而使CPU的利用率得到更大的提高。如下圖所示:
程式的由來
在多道批處理系統中引申出了一個非常重要的模式,即允許多個作業進入記憶體並執行
。由於在記憶體中儲存了多個作業,那麼多個作業如何進行區分?當某個作業因為等待I/O暫停時,怎麼恢復到之前的執行狀態呢?
所以這個時候,聰明的人們就發明了程式
這一概念,用程式來儲存每個作業的資料與執行狀態,同時對每個程式劃分對應的記憶體地址空間(程式碼、資料、程式空間、開啟的檔案),並且要求程式只能使用它自己的記憶體空間。那麼就可以達到作業的區分及恢復。
執行緒的由來
多道批處理系統所引入的程式的概念,確實提高了計算機的執行效率,但是單單使用程式來處理程式的併發
的弊端也越來與突出,因為一個程式在一個時間段內只能做一件事情。如果某個程式有多個任務,只能逐個執行這些任務。
併發:巨集觀上看起來同一個時間段有多個任務在計算機中執行。
還是以上文提到的吃飯為例子。假設在吃飯程式中包含兩個任務,一個看電視任務,一個吃飯任務。現在我們希望計算機一邊執行吃飯任務,一邊執行看電視任務。那麼根據多道批處理系統的執行規則,計算機實際排程是以程式為排程單位的,那麼我們想一邊吃飯,一邊看電視的行為,只能拆分為兩個程式。但是我們知道程式中儲存了大量資訊(資料,程式執行狀態資訊等)。那麼當計算機進行程式切換的時候,必然存在著很大的時間與空間消耗(因為每個程式對應不同記憶體地址空間,程式的切換,實際是處理器處理不同的地址空間)。如果不拆分為兩個程式,吃飯這兩個任務在同一個程式中,那麼這兩個任務必然是依次執行的,這又違背了我們一邊看電視一邊吃飯的初衷。
那麼問題來了,我們是否可以實現程式中任務的切換,又可以避免程式切換記憶體地址空間呢?聰明的人們又進一步的發明了執行緒
這一概念。用執行緒表示程式中的不同任務,同時又將計算機實際排程的單元轉到執行緒。這樣就避免了程式的記憶體地址空間的切換,也達到了多工的併發執行。具體如下圖所示:
程式與執行緒的區別
前面講解了程式與執行緒的由來,有可能大家現在還有一絲疑惑,下面就讓我們一起來總結一下程式與執行緒的關係。
- 程式是計算機分配資源的單元,而執行緒是計算機排程的基本單元。
- 一個程式由一個或多個執行緒組成。執行緒代表著程式中不同的任務。
- 程式之間相互獨立,同一程式下的各個執行緒之間共享程式的記憶體空間(包括程式碼段、資料集、堆等)及一些程式級的資源(如開啟檔案和訊號)
- 程式的切換由時空開銷。
程式與執行緒的關係如下圖所示:
執行緒的使用場景
執行緒的出現確實提高了程式執行的效能,那麼執行緒的使用場景有哪些呢?
- 執行後臺任務,在很多場景中,可能會有一些定時的批量任務,比如定時傳送簡訊、定時生成批量檔案。在這些場景中可以通過多執行緒的來執行。
- 非同步處理,比如在使用者註冊成功以後給使用者傳送優惠券或者簡訊,可以通過非同步的方式來執行,一方面提升主程式的執行效能;另一方面可以解耦核心功能,防止非核心功能對核心功能造成影響。
- 分散式處理,比如fork/join,將一個任務拆分成多個子任務分別執行。
- BIO模型中的執行緒任務分發,也是一種比較常見的使用場景,一個請求對應一個執行緒。
執行緒使用注意事項
雖然在程式中我們可以建立多個執行緒來提高程式的執行效率與吞吐率,但是也會出現以下問題:
- 由於多個執行緒共同佔有所屬程式的資源和地址空間,那麼多個執行緒要同時訪問某個資源,這個時候該怎麼處理?
- 執行緒既然能提高執行效率,那麼是否在程式中建立越多執行緒越好?
這些問題其實是關於執行緒同步與合理的建立執行緒數的問題。這裡就不做過多的介紹,如果大家對這部分感興趣,可以關注後續文章或查閱相關資料。
最後
站在巨人的肩膀上,才能看的更遠~