Java多執行緒傻瓜入門介紹

banq發表於2019-03-12

現代計算機能夠同時執行多個操作。在硬體改進和更智慧的作業系統的支援下,多個操作的功能使您的程式在執行速度和響應速度方面執行得更快。
編寫利用這種功能的軟體既迷人又棘手:它要求您瞭解計算機引擎蓋下發生的情況。

程式和執行緒:以正確的方式命名
現代作業系統可以同時執行多個程式。這就是為什麼您可以在瀏覽器(程式)中閱讀本文,同時在您的媒體播放器(另一個程式)上聽音樂。每個程式都被稱為正在執行的程式。作業系統知道許多軟體技巧,以使程式與其他程式一起執行,並利用底層硬體。無論哪種方式,最終結果是您感覺所有程式同時執行。

在作業系統中執行程式不是同時執行多個操作的唯一方法。每個程式都能夠在其自身內部同時執行子任務,稱為執行緒。您可以將執行緒視為程式本身的一部分。每個程式在啟動時至少觸發一個執行緒,稱為主執行緒。然後,根據程式/程式設計師的需要,可以啟動或終止其他執行緒。多執行緒是關於使用單個程式執行多個執行緒。

例如,您的媒體播放器可能會執行多個執行緒:一個用於呈現介面 - 這通常是主執行緒,另一個用於播放音樂,等等。

您可以將作業系統視為包含多個程式的容器,其中每個程式都是一個容納多個執行緒的容器。在本文中,我將僅關注執行緒,但整個主題非常吸引人,並且值得在未來進行更深入的分析。

程式和執行緒之間的差異
每個程式都有自己的作業系統分配的記憶體塊。預設情況下,記憶體無法與其他程式共享:您的瀏覽器無法訪問分配給您的媒體播放器的記憶體,反之亦然。如果您執行同一程式的兩個例項,即兩次啟動瀏覽器,則會發生同樣的情況。作業系統將每個例項視為一個新程式,並分配了自己獨立的記憶體部分。因此,預設情況下,兩個或多個程式無法共享資料,除非它們執行高階技巧 - 即所謂的程式間通訊(IPC)

與程式不同,執行緒共享由作業系統分配給其父程式的相同記憶體塊:媒體播放器主介面中的資料可以由音訊引擎輕鬆訪問,反之亦然。因此,兩個執行緒更容易相互通訊。最重要的是,執行緒通常比程式更輕:它們佔用的資源更少,建立速度更快,這就是為什麼它們也被稱為輕量級程式。

執行緒是使程式同時執行多個操作的便捷方式。如果沒有執行緒,則必須為每個任務編寫一個程式,將它們作為程式執行並透過作業系統進行同步。這將更加困難(IPC很棘手)而且速度較慢(程式比執行緒更重)。

綠色執行緒fiber
到目前為止提到的執行緒是作業系統的事情:想要觸發新執行緒的程式必須與作業系統通訊。但並非每個平臺本身都支援執行緒。綠色執行緒(也稱為光纖fiber)是一種模擬,它使多執行緒程式在不提供該功能的環境中工作。例如,如果底層作業系統沒有本機執行緒支援,則虛擬機器可能會實現綠色執行緒。

綠色執行緒的建立和管理速度更快,因為它們完全繞過作業系統,但也有缺點。我會在下一集中寫下這個話題。

“綠色執行緒”這個名稱是指Sun Microsystem的Green Team,它在90年代設計了原始的Java執行緒庫。今天Java不再使用綠色執行緒:它們在2000年轉向本地執行緒。其他一些程式語言 - Go,Haskell或Rust等等 - 實現等效的綠色執行緒而不是本機執行緒。

執行緒用處
為什麼程式應該使用多個執行緒?正如我之前提到的,並行處理可以大大加快速度。假設您要在電影編輯器中渲染電影。編輯器可以足夠聰明,可以跨多個執行緒傳播渲染操作,每個執行緒處理最終影片的一大塊。因此,如果使用一個執行緒,任務將花費一個小時,兩個執行緒需要30分鐘; 用四個執行緒15分鐘,依此類推。

它真的那麼簡單嗎?有三個要點需要考慮:

  1. 並非每個程式都需要多執行緒。如果您的應用程式執行順序操作或經常等待使用者執行某些操作,多執行緒可能不是那麼有用;
  2. 你只是不向應用程式丟擲更多執行緒以使其執行更快:每個子任務都必須仔細考慮和設計以執行並行操作;
  3. 並非100%保證執行緒將真正並行執行其操作,同時:它實際上取決於底層硬體。

最後一個是至關重要的:如果您的計算機不同時支援多個操作,作業系統必須偽造它們。我們將在一分鐘內看到。現在讓我們將併發視為同時執行任務的感知,而將真正的並行視為同時執行的任務。

並行性是併發的一個子集。

什麼使併發和並行成為可能
在中央處理單元(CPU)在您的電腦上執行的程式的辛勤工作。它由幾個部分組成,主要部分是所謂的核心:即實際執行計算的地方。CPU核一次只能執行一個操作。

這當然是一個主要缺點。出於這個原因,作業系統開發了先進的技術,使使用者能夠同時執行多個程式(或執行緒),尤其是在圖形環境中,甚至在單個核心機器上。最重要的一種叫做搶佔式多工處理,搶佔是指中斷任務,切換到另一個任務然後在以後恢復第一個任務的能力。

因此,如果您的CPU只有一個核,那麼作業系統的一部分工作就是將該單核心計算能力分散到多個程式或執行緒中,這些程式或執行緒在一個迴圈中一個接一個地執行。這個操作讓你覺得有多個程式並行執行,或者一個程式同時執行多個程式(如果是多執行緒的)。併發性得到滿足,但真正的並行性 - 同時執行程式的能力- 仍然缺失。

如今,現代CPU在引擎蓋下有多個核,每個核一次執行獨立操作。這意味著使用兩個或更多核心可以實現真正的並行性。例如,我的英特爾酷睿i7有四個核心:它可以同時執行四個不同的程式或執行緒。

作業系統能夠檢測CPU核的數量,併為每個核分配程式或執行緒。執行緒可以分配給作業系統喜歡的任何核數,並且這種排程對於正在執行的程式是完全透明的。此外,如果所有核心都忙,可以啟動搶佔式多工處理。這使您能夠執行比計算機中可用的實際數量或核心數更多的程式和執行緒。

單核上的多執行緒應用程式:它有意義嗎?
單核機器上的真正並行性是不可能實現的。然而,如果您的應用程式可以從中受益,那麼編寫多執行緒程式仍然是有意義的。當程式使用多個執行緒時,即使其中一個執行緒執行緩慢或阻塞任務,搶佔式多工也可以使應用程式保持執行。

比如說你正在開發一個從非常慢的磁碟讀取一些資料的桌面應用程式。如果只用一個執行緒編寫程式,整個應用程式將凍結,直到磁碟操作完成:分配給唯一執行緒的CPU功率在等待磁碟喚醒時被浪費。當然,作業系統除此之外還執行許多其他程式,但您的特定應用程式將不會取得任何進展。

讓我們以多執行緒的方式重新思考您的應用。執行緒A負責磁碟訪問,而執行緒B負責主介面。如果執行緒A由於裝置執行緩慢而等待,則執行緒B仍然可以執行主介面,從而使程式保持響應。這是可能的,因為有兩個執行緒,作業系統可以在它們之間切換CPU資源而不會卡在較慢的執行緒上。

更多執行緒,更多問題
眾所周知,執行緒共享其父程式的相同記憶體塊。這使得它們中的兩個或更多個在同一應用程式內交換資料非常容易。例如:電影編輯器可能包含大部分包含影片時間軸的共享記憶體。這些共享記憶體正被指定用於將電影渲染到檔案的幾個工作執行緒讀取。它們都只需要一個指向該儲存區的控制程式碼(例如指標),以便從中讀取並將渲染幀輸出到磁碟。

只要兩個或多個執行緒從同一個記憶體位置讀取,事情就會順利進行。當至少其中一個人寫入共享記憶體時,其他人正在從中讀取問題。此時可能會出現兩個問題:

  • 資料爭用 - 當編寫器執行緒修改記憶體時,讀者執行緒可能正在讀取它。如果寫者尚未完成其工作,讀者將獲得損壞的資料;
  • 競爭條件 - 讀者執行緒只有在寫者寫完後才能讀取。如果相反的情況怎麼辦?比資料競爭更微妙,競爭條件是關於兩個或更多執行緒以不可預測的順序執行其工作,而實際上操作應該以正確的順序執行以正確完成。您的程式即使受到資料競爭保護也可以觸發競爭條件。


執行緒安全的概念
如果一段程式碼正常工作,即沒有資料競爭或競爭條件,即使許多執行緒同時執行它,也會說它是執行緒安全的。您可能已經注意到某些程式設計庫宣告自己是執行緒安全的:如果您正在編寫多執行緒程式,則需要確保可以跨不同執行緒使用任何其他第三方函式,而不會觸發併發問題。

資料競爭的根本原因
我們知道CPU核心一次只能執行一條機器指令。這樣的指令被認為是原子的,因為它是不可分割的:它不能分解成更小的操作。希臘語“atom”(ἄτομος; atomos)意味著不可切割。

不可分割的屬性使原子操作本質上是執行緒安全的。當執行緒對共享資料執行原子寫入時,沒有其他執行緒可以讀取修改半完成。相反,當執行緒對共享資料執行原子讀取時,它會讀取單個時刻出現的整個值。執行緒無法透過原子操作,因此不會發生資料爭用。

壞訊息是絕大多數的操作都是非原子的。即使像x = 1某些硬體上那樣的微不足道的任務也可能由多個原子機器指令組成,這使得賦值本身就是非原子的。因此,如果執行緒讀取x而另一個執行緒執行分配,則會觸發資料爭用。

導致競爭的根本原因
搶佔式多工處理使作業系統可以完全控制執行緒管理:它可以根據高階排程演算法啟動,停止和暫停執行緒。您作為程式設計師無法控制執行的時間或順序。實際上,無法保證像這樣的簡單程式碼:

writer_thread.start()
reader_thread.start()


按特定順序啟動兩個執行緒。多次執行此程式,您將注意到每次執行時它的行為方式如何:有時候寫者執行緒首先啟動,有時候讀者卻會首先啟動。如果您的程式需要寫者始終在讀者之前執行,您肯定會遇到競爭狀態。

此行為稱為非確定性:結果每次都會更改,您無法預測。受競爭條件影響的除錯程式非常煩人,因為您無法始終以受控方式重現問題。

教導執行緒相處:併發控制
資料競賽和競爭條件都是現實世界的問題:有些人甚至因為他們而死亡。容納兩個或多個併發執行緒的技術稱為併發控制:作業系統和程式語言提供了幾種解決方案來處理它。最重要的是:

  • 同步 - 一種確保資源一次只能由一個執行緒使用的方法。同步是將程式碼的特定部分標記為“受保護”,以便兩個或多個併發執行緒不會同時執行它,從而搞砸了共享資料;
  • 原子操作 - 由於作業系統提供的特殊指令,一堆非原子操作(如之前提到的賦值)可以轉換為原子操作。這樣,無論其他執行緒如何訪問共享資料,共享資料始終保持有效狀態;
  • 不可變資料 - 共享資料被標記為不可變,沒有任何東西可以改變它:只允許執行緒從中讀取,消除了根本原因。我們知道執行緒可以安全地從相同的記憶體位置讀取,只要它們不修改它。這是函數語言程式設計背後的主要哲學。


 

相關文章