JavaRush /Java Blog /Random-TW /Java 中的多執行緒:本質、優點和常見陷阱

Java 中的多執行緒:本質、優點和常見陷阱

在 Random-TW 群組發布
你好!首先,恭喜您:您已經到達了 Java 多執行緒的主題!這是一項重大成就,還有很長的路要走。但做好準備:這是本課程最困難的主題之一。重點不在於這裡使用了複雜的類別或許多方法:相反,甚至沒有兩打。更重要的是你需要稍微改變一下你的想法。以前,您的程式是按順序執行的。有些程式碼行跟隨其他程式碼行,有些方法跟隨其他程式碼行,整體來說一切都很清楚。首先,計算一些東西,然後將結果顯示在控制台上,然後終止程式。要理解多線程,最好從並發的角度來思考。讓我們從一些非常簡單的事情開始:)Java 中的多執行緒:本質、優點和常見陷阱 - 1想像一下您的家人正在從一所房子搬到另一所房子。搬家的一個重要部分是收拾你的書。你累積了很多書,你需要把它們裝進盒子裡。現在只有你是自由的。媽媽在準備食物,哥哥在收衣服,妹妹去商店了。至少你可以獨自完成任務,而且遲早你甚至會自己完成任務,但這會花費很多時間。然而,20 分鐘後你姐姐就會從商店回來,她無事可做。這樣她就可以加入你了。任務還是一樣:把書放進盒子裡。它的運行速度只是原來的兩倍。為什麼?因為工作是並行完成的。兩個不同的「線程」(你和你的妹妹)同時執行相同的任務,如果沒有任何變化,與你獨自完成所有事情的情況相比,時間差將非常大。如果你的兄弟很快就完成了他的任務,他可以幫助你,事情會進展得更快。

Java中多執行緒解決的問題

本質上,Java 多執行緒的發明是為了解決兩個主要問題:
  1. 同時執行多個操作。

    在上面的範例中,不同的執行緒(即家庭成員)並行執行多個操作:洗碗、去商店、折疊東西。

    可以舉一個更「程式設計師」的例子。想像一下您有一個帶有用戶界面的程式。當您按一下「繼續」按鈕時,程式內應進行一些計算,使用者應看到以下介面畫面。如果按順序執行這些操作,則按一下「繼續」按鈕後,程式將直接凍結。使用者將看到相同的螢幕,並帶有“繼續”按鈕,直到所有內部計算完成並且程式到達開始繪製介面的部分。

    好吧,讓我們等幾分鐘!

    Java 中的多執行緒:本質、優點和常見陷阱 - 3

    我們也可以重新編寫我們的程序,或者正如程式設計師所說,「並行化」。讓必要的計算在一個線程中執行,並在另一個線程中進行介面渲染。大多數計算機都有足夠的資源來執行此操作。這樣的話,程式就不會「傻」了,使用者會從容地在介面螢幕之間移動,不用擔心裡面發生了什麼事。它不會幹擾:)

  2. 加快計算速度。

    這裡一切都簡單得多。如果我們的處理器有多個核心,並且現在大多數處理器都是多核心的,那麼我們的任務清單可以由多個核心並行解決。顯然,如果我們需要解決1000 個問題,並且每個問題都在一秒鐘內解決,則一個核心將在1000 秒內處理該列表,兩個核心將在500 秒內處理,三個核心將在333秒多一點的時間內處理,依此類推。

但是,正如您在講座中已經讀到的那樣,現代系統非常智能,即使在一個計算核心上,當任務交替執行時,它們也能夠實現並行性或偽並行性。讓我們從一般的事情轉向具體的事情,並熟悉與多線程相關的 Java 庫中的主類別 - java.lang.Thread。嚴格來說,Java 中的執行緒是由類別的實例表示的Thread。也就是說,要建立並運行 10 個線程,您將需要 10 個此類的物件。我們來寫一個最簡單的例子:
public class MyFirstThread extends Thread {

   @Override
   public void run() {
       System.out.println("I'm Thread! My name is " + getName());
   }
}
要建立和啟動線程,我們需要建立一個類別並從java.lang. Thread並重寫其中的方法run()。最後一項非常重要。我們在方法中run()規定了線程必須執行的邏輯。現在,如果我們建立一個實例MyFirstThread並運行它,該方法run()將在控制台上列印一行及其名稱:該方法會getName()列印執行緒的「系統」名稱,該名稱是自動分配的。但事實上,為什麼要用「如果」呢?讓我們來創建並測試吧!
public class Main {

   public static void main(String[] args) {

       for (int i = 0; i < 10; i++) {

           MyFirstThread thread = new MyFirstThread();
           thread.start();
       }
   }
}
控制台輸出: 我是線程!我的名字是 Thread-2 我是 Thread!我的名字是 Thread-1 我是 Thread!我的名字是 Thread-0 我是 Thread!我的名字是 Thread-3 我是 Thread!我的名字是 Thread-6 我是 Thread!我的名字是 Thread-7 我是 Thread!我的名字是 Thread-4 我是 Thread!我的名字是 Thread-5 我是 Thread!我的名字是 Thread-9 我是 Thread!我的名字是 Thread-8 我們創建 10 個線程(物件) ,它們MyFirstThread繼承Thread並透過呼叫物件的方法來啟動它們start()。呼叫一個方法後,start()該方法開始工作run(),並執行其中編寫的邏輯。請注意:線程名稱不按順序排列。很奇怪,為什麼不依序執行:Thread-0、、、Thread-1等等Thread-2?這正是標準的「順序」思維不起作用的例子。事實上,在本例中我們只發出指令來建立和啟動 10 個執行緒。它們的啟動順序由執行緒調度程序決定:作業系統內部的一種特殊機制。它到底是如何建構的以及它根據什麼原則做出決策是一個非常複雜的議題,我們現在不會深入探討。主要要記住的是程式設計師無法控制執行緒執行的順序。要認識到情況的嚴重性,請嘗試main()多運行上面範例中的方法幾次。第二個控制台輸出: 我是線程!我的名字是 Thread-0 我是 Thread!我的名字是 Thread-4 我是 Thread!我的名字是 Thread-3 我是 Thread!我的名字是 Thread-2 我是 Thread!我的名字是 Thread-1 我是 Thread!我的名字是 Thread-5 我是 Thread!我的名字是 Thread-6 我是 Thread!我的名字是 Thread-8 我是 Thread!我的名字是 Thread-9 我是 Thread!我的名字是 Thread-7 第三個控制台輸出: 我是 Thread!我的名字是 Thread-0 我是 Thread!我的名字是 Thread-3 我是 Thread!我的名字是 Thread-1 我是 Thread!我的名字是 Thread-2 我是 Thread!我的名字是 Thread-6 我是 Thread!我的名字是 Thread-4 我是 Thread!我的名字是 Thread-9 我是 Thread!我的名字是 Thread-5 我是 Thread!我的名字是 Thread-7 我是 Thread!我的名字是Thread-8

多線程產生的問題

在書籍的範例中,您看到多執行緒解決了非常重要的問題,並且它的使用加快了我們程式的工作速度。在很多情況下——很多次。但多線程被認為是一個複雜的話題並非沒有道理。畢竟,如果使用不當,只會產生問題而不是解決問題。當我說「製造問題」時,我並不是指抽象的東西。多執行緒可能導致兩個特定問題:死鎖和競爭條件。死鎖是多個執行緒正在等待彼此佔用的資源,而沒有一個執行緒可以繼續執行的情況。我們將在以後的講座中詳細討論它,但現在這個範例就足夠了: Java 中的多執行緒:本質、優點和常見陷阱 - 4 想像線程 1 正在處理某個物件 1,而線程 2 正在處理物件 2。程式是這樣寫的:
  1. 一旦執行緒 2 停止使用物件 2 並切換到物件 1,執行緒 1 將停止使用物件 1 並切換到物件 2。
  2. 一旦執行緒 1 停止使用物件 1 並切換到物件 2,執行緒 2 將停止使用物件 2 並切換到物件 1。
即使沒有深入了解多線程,您也可以輕鬆理解它不會產生任何結果。線程永遠不會改變位置並且將永遠等待彼此。這個錯誤看起來很明顯,但實際上並非如此。您可以輕鬆地允許它進入程式。我們將在接下來的講座中查看導致死鎖的程式碼範例。順便說一下,Quora 有一個很好的現實例子來解釋什麼是死鎖。「在印度的一些邦,除非你註冊為農民,否則他們不會向你出售農業用地。但是,如果您沒有農業用地,則不會註冊為農民。” 太好了,我還能說什麼!:) 現在關於競爭條件 - 競爭的狀態。 Java 中的多執行緒:本質、優點和常見陷阱 - 5競爭條件是多執行緒系統或應用程式中的設計缺陷,其中系統或應用程式的操作取決於程式碼部分的執行順序。請記住運行線程的範例:
public class MyFirstThread extends Thread {

   @Override
   public void run() {
       System.out.println("Выполнен поток " + getName());
   }
}

public class Main {

   public static void main(String[] args) {

       for (int i = 0; i < 10; i++) {

           MyFirstThread thread = new MyFirstThread();
           thread.start();
       }
   }
}
現在想像一下該程式負責操作準備食物的機器人! Thread-0 從冰箱取出雞蛋。流 1 打開爐子。Stream-2拿出煎鍋放在爐子上。流 3 點燃爐子上的火。流4將油倒入鍋中。流5將雞蛋打碎,倒入煎鍋。流 6 將貝殼丟進垃圾桶。Stream-7 將完成的炒蛋從火上移開。Potok-8 將炒蛋放在盤子上。Stream-9 洗碗。 看看我們程式的結果: Thread-0執行 Thread-2執行緒執行 Thread-1執行緒執行 Thread-4執行緒執行 Thread-9執行緒執行 Thread-5執行緒執行 Thread-8執行緒執行 Thread-7執行緒執行 Thread-7執行緒執行-3 線程-6 線程執行。 腳本有趣嗎?:) 這都是因為我們程式的操作取決於執行緒的執行順序。只要稍微違反這個順序,我們的廚房就會變成地獄,一個瘋狂的機器人就會摧毀周圍的一切。這也是多執行緒程式設計中的常見問題,您會不只一次聽到這個問題。在講座的最後,我想向您推薦一本關於多線程的書。
Java 中的多執行緒:本質、優點和常見陷阱 - 6
《Java 並發實踐》寫於 2006 年,但並沒有失去其相關性。它涵蓋了 Java 中的多線程編程,從基礎知識開始,到最常見錯誤和反模式的列表結束。如果您決定成為多執行緒程式設計大師,那麼這本書是必讀的。下期講座再見!:)
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION