1. 程式人生 > >多執行緒相關知識

多執行緒相關知識

一、多執行緒的基本概念

1、程序與執行緒的區別和聯絡

  • 程序:程序是一個動態的過程,是一個活動的實體。簡單來說,一個應用程式的執行就可以被看做是一個程序;
  • 執行緒:是執行中的實際的任務執行者。可以說,程序中包含了多個可以同時執行的執行緒。通俗理解:例如你開啟微信就是開啟一個程序,在微信裡面和好友視訊聊天就是開啟了一條執行緒。
  • 兩者之間的關係: 
    一個程序裡面可以有多條執行緒,至少有一條執行緒。 
    一條執行緒一定會在一個程序裡面。

2、建立執行緒的2種方法

方式1:繼承java.lang.Thread類,並覆蓋run()方法。優勢:編寫簡單;劣勢:無法繼承其他父類

方式2:實現java.lang.Runnable介面,並實現run()方法。優勢:可以繼承其他類,多執行緒可以共享同一個Thread物件;劣勢:程式設計方式稍微複雜,如需訪問當前執行緒,需呼叫Thread.currentThread()方法

3、Java建立執行緒後,呼叫start()方法和run()的區別

  兩種方法的區別

  1) start:

       用start方法來啟動執行緒,真正實現了多執行緒執行,這時無需等待run方法體程式碼執行完畢而直接繼續執行下面的程式碼。通過呼叫Thread類的start()方法來啟動一個執行緒,這時此執行緒處於就緒(可執行)狀態,並沒有執行,一旦得到cpu時間片,就開始執行run()方法,這裡方法run()稱為執行緒體,它包含了要執行的這個執行緒的內容,Run方法執行結束,此執行緒隨即終止。

  2) run:

       run()方法只是類的一個普通方法而已,如果直接呼叫run方法,程式中依然只有主執行緒這一個執行緒,其程式執行路徑還是隻有一條,還是要順序執行,還是要等待run方法體執行完畢後才可繼續執行下面的程式碼,這樣就沒有達到寫執行緒的目的。

總結:呼叫start方法方可啟動執行緒,而run方法只是thread的一個普通方法呼叫,還是在主執行緒裡執行。

       這兩個方法應該都比較熟悉,把需要並行處理的程式碼放在run()方法中,start()方法啟動執行緒將自動呼叫 run()方法,這是由jvm的記憶體機制規定的。並且run()方法必須是public訪問許可權,返回值型別為void。

兩種方式的比較 :

       實際中往往採用實現Runable介面,一方面因為java只支援單繼承,繼承了Thread類就無法再繼續繼承其它類,而且Runable介面只有一個run方法;另一方面通過結果可以看出實現Runable接口才是真正的多執行緒。

4、併發,並行的區別

  • 併發:同一時間段內交替執行多個程序(執行緒);
  • 並行:同一時刻執行多個程序(執行緒)。很明顯,只有多處理器才能支援。

  併發就像我們的大腦思考一樣,同一個時刻只能想一件事,但是在很短的一個時間段內我們可以三心二意。當然如果你長了幾個腦袋,那你就可以並行思考了。

5、同步與非同步,阻塞與非阻塞方式

  下面介紹同步,非同步,阻塞,非阻塞這幾個概念,加深對多執行緒程式設計的理解。 
  有了之前的概念,我們可以想象,當幾個執行緒或者程序在併發執行時,如果我們不加任何干預措施,那麼他們的執行順序是由系統當時的環境來決定的,所以不同時間段不同環境下執行的順序都會不盡相同,這便是非同步(有差異的步驟)。當然,同步肯定就是通過一定的措施,使得幾個執行緒或者程序總是按照一定順序來執行(總是按照相同的步驟)。 
  當一個程序或者執行緒請求某一個資源而不得時,如I/O,便會進入阻塞狀態,一直等待。scanf()便是一個很好的例子,當程式執行到scanf()時,如果輸入快取區為空,那麼程式便會進入阻塞狀態等待我們從鍵盤輸入,這便是以阻塞的方式呼叫scanf()。通過一定方法,我們可以將scanf()變成非阻塞的方式來執行。如給scanf()設定一個超時時間,如果時間到了還是沒有輸入那麼便跳過scanf(),這個時候我們就稱為用非阻塞的方式來呼叫scanf()。 
  對比可以發現,同步即阻塞。想要按照某特定順序來執行一系列過程,在上一個過程完成之前下一個過程必須等待,這就是阻塞在了這個地方。當同步執行的時候,會等待同步操作完成才會返回,否則會一直阻塞在同步操作處。 
  相反的,非同步即非阻塞,當非同步呼叫某個函式時,函式會立刻返回,而不會阻塞在那。

  怎麼判斷非同步操作是否已經完成?通常有3種方式:

1. 狀態:非同步操作完成時會將某個全域性變數置為特定值,可以通過輪詢判斷變數的值以確定是否操作完成;

2. 通知:非同步操作完成會給呼叫者傳送特定訊號;

3. 回撥:非同步操作完成時會呼叫回撥函式。

  所以同步即阻塞,非同步即非阻塞。

6、執行緒阻塞的常見情況

1. 呼叫sleep()進入睡眠狀態,釋放CPU,不釋放鎖;

2. 用wait()暫停了執行緒,釋放CPU,釋放鎖。收到notify()或notifyAll()喚醒執行緒;詳細見:https://mp.csdn.net/postedit/81045401

3. 執行緒正在等待一些IO操作;

4. 執行緒正在試圖呼叫被鎖起來了的物件。

二、執行緒的生命週期

執行緒在一定條件下,狀態會發生變化。執行緒一共有以下幾種狀態:

1. 新建狀態(New):新建立了一個執行緒物件;

2. 就緒狀態(Runnable):執行緒物件建立後,其他執行緒呼叫了該物件的start()方法。該狀態的執行緒位於“可執行執行緒池”中,變得可執行,只等待獲取CPU的使用權。即在就緒狀態的程序除CPU之外,其它的執行所需資源都已全部獲得;

3. 執行狀態(Running):就緒狀態的執行緒獲取了CPU,執行程式 
程式碼;

4. 阻塞狀態(Blocked):阻塞狀態是執行緒因為某種原因放棄CPU使用權,暫時停止執行。直到執行緒進入就緒狀態,才有機會轉到執行狀態。

5. 死亡狀態(dead):執行緒執行完了或者因異常退出了run()方法,該執行緒結束生命週期。

這裡寫圖片描述

三、多執行緒與單執行緒

1、多執行緒與單執行緒的區別

  單執行緒,顧名思義即是隻有一條執行緒在執行任務,這種情況在我們日常的工作學習中很少遇到,所以我們只是簡單做一下了解。

  多執行緒,建立多條執行緒同時執行任務,這種方式在我們的日常生活中比較常見。但是,在多執行緒的使用過程中,還有許多需要我們瞭解的概念。比如,在理解上並行和併發的區別,以及在實際應用的過程中多執行緒的安全問題,對此,我們需要進行詳細的瞭解。

  多執行緒和傳統的單執行緒在程式設計上最大的區別在於,由於各個執行緒的控制流彼此獨立,使得各個執行緒之間的程式碼是亂序執行的,由此帶來的執行緒排程,同步等問題

2、多執行緒是否一定比單執行緒效率高?

  一提到多執行緒一般大家的第一感覺就是可以提升程式效能,在實際的操作中往往遇到效能的問題,都嘗試使用多執行緒來解決問題,但多執行緒程式並不是在任何情況下都能提升效率,在一些情況下恰恰相反,反而會降低程式的效能。對於單核CPU計算密集型任務,多執行緒反而並不能帶來效率的提升。執行緒本身由於建立和切換的開銷,採用多執行緒不會提高程式的執行速度,反而會降低速度。但是對於頻繁IO操作的程式,多執行緒可以有效的併發對於包含不同任務的程式,可以考慮每個任務使用一個執行緒。這樣的程式在設計上相對於單執行緒做所有事的程式來說,更為清晰明瞭,比如生產、消費者問題。

  在實際的開發中對於效能優化的問題需要考慮到具體的場景來考慮是否使用多執行緒技術。

四、為什麼要使用多執行緒

1、什麼時候使用多執行緒?

  執行緒必然不是越多越好,執行緒切換也是要開銷的,當你增加一個執行緒的時候,增加的額外開銷要小於該執行緒能夠消除的阻塞時間,這才叫物有所值。

  什麼時候該使用多執行緒呢?這要分四種情況討論:

1. 多核CPU——計算密集型任務。此時要儘量使用多執行緒,可以提高任務執行效率,例如加密解密,資料壓縮解壓縮(視訊、音訊、普通資料),否則只能使一個核心滿載,而其他核心閒置;

2. 單核CPU——計算密集型任務。此時的任務已經把CPU資源100%消耗了,就沒必要也不可能使用多執行緒來提高計算效率了;相反,如果要做人機互動,最好還是要用多執行緒,避免使用者沒法對計算機進行操作;

3. 單核CPU——IO密集型任務,使用多執行緒還是為了人機互動方便;

4. 多核CPU——IO密集型任務,這就更不用說了,跟單核時候原因一樣。

2、什麼時候不使用多執行緒?

  知道什麼情況下不使用併發同樣重要。從根本上來說,不使用併發的唯一原因就是併發帶來的效益小於它帶來的代價。在許多情況下,使用併發會使程式碼難以理解,編寫、維護併發程式碼需要更多的腦力成本,併發帶來的複雜性可能會增加bug。除非併發帶來效能的提升足夠打,或者模組劃分足夠清楚,否則不要使用併發。

  使用併發帶來效能上的提升可能不如預期。併發程式設計也需要額外的開銷,在建立一個執行緒時,系統要分配核心資源、棧空間,然後把新執行緒加入到任務佇列。如果執行緒執行時間小於執行緒的建立時間,這時使用多執行緒可能會使效能變差。

  進一步來說,執行緒資源是有限的。如果同時有太多執行緒,會佔用太多系統資源,會使整個系統變慢。使用太多執行緒會消耗盡記憶體或處理器的地址空間,因為執行緒需要獨立的棧空間。

  在C/S架構下,如果為每個連線建立一個執行緒,在連線比較少時效能很好,但是如果要同時處理太多連線的話會建立太多執行緒。這時使用執行緒池(thread pools)可以提高效能。在Linux下可以使用I/O多路複用:select、poll、epoll。

  執行緒的切換也需要時間,如果執行緒切換時間比執行緒執行時間還短,就會降低整體效能。

五、多執行緒優缺點

1、優點

1. 提高CPU的使用率:例如朋友圈發表圖片,當你上傳9張圖片的時候,如果開啟一個執行緒用同步的方式一張張上傳圖片,假設每次上傳圖片的執行緒只佔用了CPU 1%d的資源,剩下的99%資源就浪費了。但是如果你開啟9個執行緒同時上傳圖片,CPU就可以使用9%的資源了;

2. 提高程式的工作效率:還是拿朋友圈發表圖片來說,假設開啟一個執行緒上傳一張圖片的時間是1秒,那麼同步的方式上傳9張就需要9秒,但是你開啟9個執行緒同時上傳圖片,那麼就只需要1秒就完成了。

2、缺點

1. 如果有大量的執行緒,會影響效能,因為CPU需要在它們之間切換

2. 更多的執行緒需要更多的記憶體空間

3. 多執行緒操作可能會出現執行緒安全或者死鎖等問題。

六、執行緒安全及解決方法

執行緒安全:簡單的來說,就是在多個執行緒訪問一個類的時候,該類始終保持著正確的執行行為。

1、執行緒安全出現的根本原因

      1. 存在兩個或者兩個以上的執行緒物件共享同一個資源;

      2. 多執行緒操作共享資原始碼有多個語句。

2、執行緒安全的解決方法

     1. 無狀態物件永遠是執行緒安全的;

     2. 原子性與競爭條件:原子性,顧名思義,指的就是不可分割的操作,多個操作要麼一起執行,要麼就都不執行。原子性保證了程式的執行不會因為執行的時序問題而引發的執行緒安全問題。而經常引起原子性問題的就是競爭條件。比如常見的檢查再執行(check-then-act),當我建立一個資料夾時,會先判斷資料夾是否存在,不存在再建立。這個在單執行緒的情況下不會出現問題,但是在多執行緒下,就可能會因為當我檢查資料夾不存在後,另一個執行緒先建立了該資料夾,從而導致此執行緒建立檔案錯誤。