Q : 線程和進程的區別?
Q : 多線程的優缺點?java
進程的由來涉及到操做系統的發展歷史,早期的計算機只能用來解決數學計算問題。由於不少大量的計算經過人力去完成是很耗時間和人力成本的。最初的計算機,只能接受一些特定指令,用戶輸入一個指令,計算機就作一個操做,假設用戶輸入指令和讀取數據須要10s,計算可能只須要0.01s,計算機絕大多數都處於等待用戶輸入的狀態,顯然這樣效率很低。 git
那麼能不能把一系列須要輸入的指令都提早寫好,造成一個清單,而後一次性交給計算機,這樣計算機就能夠不斷讀取指令來進行相應的操做,因而,批處理操做系統就誕生了。這樣就提升了任務處理的便捷性,減小用戶輸入指令時間。面試
可是仍然存在一個問題:數據讀取(I/O操做)所須要的CPU資源很是少。大部分工做是分派給DMA(Direct Memory Access)直接內存完成的。在DMA讀取數據的時候,CPU是空閒的,只能等待當前的任務讀取完數據才能繼續執行,這樣就白白浪費了CPU資源,因而人們在想,可否讓CPU在等待A任務讀取數據期間,去執行B任務,當A任務讀取完後,暫停B任務,繼續執行A任務?編程
能夠打開Windows的任務管理器,複製一個大文件,你會發現,磁盤利用率會持續增大,而CPU的利用率則會稍微增大一些,而後恢復正常,這個變化過程就是CPU給DMA分派任務
這樣就有一個新的問題,原來每次都是一個程序在計算機裏面運行,也就說內存中始終只有一個程序的運行數據。而若是想要任務A執行I/O操做的時候,讓任務B去執行,必然內存中要裝入多個程序,那麼如何處理呢?多個程序使用的數據如何進行辨別呢?而且當一個程序運行暫停後,後面如何恢復到它以前執行的狀態呢?windows
這個時候人們就發明了進程,用進程來對應一個程序,每一個進程對應必定的內存地址空間,而且只能使用它本身的內存空間,各個進程間互不干擾。而且進程保存了程序每一個時刻的運行狀態,這樣就爲進程切換提供了可能。當進程暫停時,它會保存當前進程的狀態(好比進程標識、進程的使用的資源等),在下一次從新切換回來時,便根據以前保存的狀態進行恢復,而後繼續執行。瀏覽器
進程的出現,使得操做系統的併發成爲可能,注意這裏說的是 併發 而不是 並行 。這二者在概念上大相徑庭。
併發: 從宏觀上看起來同一時間段,多個任務都在執行,但具體的某一時間點,只有一個任務在使用CPU(針對單核CPU來講),cpu把這個時間段分片給多個任務,因爲整個時間段很小,因此咱們感受CPU好像在同時運行這些任務。
並行: 同一時間點,多個任務同時執行,單核CPU沒法作到,而多核CPU能夠。安全
進程是程序的一次執行過程,是操做系統分配資源的基本單位。 服務器
在現代的操做系統好比 Windows、Linux、UNIX、Mac OS X等,都是支持多任務的操做系統。意味着操做系統能夠同時運行多個任務,不管你的CPU是單核單線程仍是多核多線程,你均可以一邊聽歌,一邊玩遊戲。這個時候至少有2個任務(能夠理解爲2個進程,但實際可能會多於2個進程,例如Chrome瀏覽器,你每打開一個標籤頁,Chrome瀏覽器應用都會建立一個新的進程)同時在運行。還有不少任務悄悄地在後臺同時運行着,只是桌面上沒有顯示而已。這就是多任務的併發。多線程
固然如今的CPU大多都是多核多線程,有的還支持超線程技術(將一個物理處理器在軟件層變成兩個邏輯處理器),使一個CPU核心能夠並行兩個線程,但系統所運行的任務數遠遠多於CPU的核心數,因此,操做系統也會自動把不少任務輪流調度到每一個核心上執行,因此併發和並行在系統運行時是一直存在的。併發
打開Windows任務管理器,能夠看到操做系統上運行的任務,以下:
Google Chrome(10),10就表明着這個任務下有10個進程
後臺進程(98),表明着有98個後臺進程在默默運行着
進程的出現,解決了操做系統的併發問題,使得操做系統的性能獲得了大大的提高。有新的問題出現了,由於一個進程在一個時間段內只能作一件事情,若是一個進程有多個子任務,只能逐個地去執行這些子任務。好比對於一個監控系統來講,它不只要把圖像數據顯示在畫面上,還要與服務端進行通訊獲取圖像數據,還要處理人們的交互操做。若是某一個時刻該系統正在與服務器通訊獲取圖像數據,而用戶又在監控系統上點擊了某個按鈕,那麼該系統就要等待獲取完圖像數據以後才能處理用戶的操做,若是獲取圖像數據須要耗費10s,那麼用戶就只有一直在等待。顯然,對於這樣的系統,人們是沒法知足的。
那麼可不能夠將這些子任務分開執行呢?即在系統獲取圖像數據的同時,若是用戶點擊了某個按鈕,則會暫停獲取圖像數據,而先去響應用戶的操做(由於用戶的操做每每執行時間很短),在處理完用戶操做以後,再繼續獲取圖像數據。人們就發明了線程,讓一個線程去執行一個子任務,這樣一個進程就包括了多個線程,每一個線程負責一個獨立的子任務,這樣在用戶點擊按鈕的時候,就能夠暫停獲取圖像數據的線程,讓UI線程響應用戶的操做,響應完以後再切換回來,讓獲取圖像的線程獲得CPU資源。從而讓用戶感受系統是同時在作多件事情的,知足了用戶對實時性的要求。
換句話說,進程讓操做系統的併發性成爲可能,而線程讓進程的內部併發成爲可能。可是要注意,一個進程雖然包括多個線程,可是這些線程是共同享有進程佔有的資源和地址空間的。進程是操做系統進行資源分配的基本單位,而線程是操做系統進行調度的執行單位。
Java語言內置了多線程支持,一個Java程序其實是一個JVM進程(也能夠稱爲JVM實例),通常來講名字默認爲java.exe或者javaw.exe(windows下能夠經過任務管理器查看)。
Java採用的是單線程編程模型,JVM進程用一個主線程來執行main()方法。 main方法所在的主線程只是其中的一個線程,JVM進程在啓動時,同時會建立不少其餘的線程。
咱們能夠經過 JMX 來看一下一個普通的 Java 程序有哪些線程,代碼以下:
public class MultiThread { public static void main(String[] args) { // 獲取 Java 線程管理 MXBean ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); // 不須要獲取同步的 monitor 和 synchronizer 信息,僅獲取線程和線程堆棧信息 ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false); // 遍歷線程信息,僅打印線程 ID 和線程名稱信息 for (ThreadInfo threadInfo : threadInfos) { System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.getThreadName()); } } } 上述程序輸出以下(輸出內容可能不一樣,不用太糾結下面每一個線程的做用,只用知道 main 線程執行 main 方法便可): [5] Attach Listener //添加事件 [4] Signal Dispatcher // 分發處理給 JVM 信號的線程 [3] Finalizer //調用對象 finalize 方法的線程 [2] Reference Handler //清除 reference 線程 [1] main //main 線程,程序入口 從上面的輸出內容能夠看出:一個 Java 程序的運行是 main 線程和多個其餘線程同時運行。
在main()方法內部,咱們還能夠啓動多個本身的線程。這就是多線程的由來。同類的多個線程共享進程的堆和方法區資源,但每一個線程有本身的程序計數器、虛擬機棧和本地方法棧。因此係統在產生一個線程,或是在各個線程之間做切換工做時,負擔要比進程小得多,也正由於如此,線程也被稱爲輕量級進程。
堆和方法區:堆和方法區是全部線程共享的資源,其中堆是進程中最大的一塊內存,主要用於存放新建立的對象 (全部對象都在這裏分配內存)和成員變量,方法區主要用於存放已被加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。
程序計數器:在多線程的狀況下,經過線程私有的程序計數器用於記錄當前線程執行的位置,從而當線程被切換回來的時候可以知道該線程上次運行到哪兒了,程序計數器私有主要是爲了線程切換後能恢復到正確的執行位置。
虛擬機棧: 每一個 Java 方法在執行的同時會建立一個棧幀用於存儲局部變量表、操做數棧、常量池引用等信息。從方法調用直至執行完成的過程,就對應着一個棧幀在 Java 虛擬機棧中入棧和出棧的過程。
本地方法棧: 和虛擬機棧所發揮的做用很是類似,區別是: 虛擬機棧爲虛擬機執行 Java 方法 (也就是字節碼)服務,而本地方法棧則爲虛擬機使用到的 Native 方法服務。 在 HotSpot 虛擬機中和 Java 虛擬機棧合二爲一。
因此,爲了保證線程中的局部變量不被別的線程訪問到,虛擬機棧和本地方法棧是線程私有的。
當下,單核CPU的經過提升時鐘頻率來提高性能已經愈來愈難,既然單核CPU的性能已經很難提高,那不妨嘗試經過提高CPU核心的數量,處理器廠商在單個芯片上放置多個處理器核,以橫向擴展來提高計算機的總體性能,再日後可能就是增長CPU的數量以及優化CPU之間的協做。
操做系統的基本調度單位是線程,多核處理器的出現,使得同一個程序的多個線程能夠被調度到多個 CPU 上同時運行。所以,多線程的程序能夠經過提升處理器資源的利用率來提高系統的吞吐率。其實,多線程程序也有助於在單處理器系統上得到更高的吞吐率,若是程序的一個線程在等待 I/O 操做的完成,另外一個線程能夠繼續運行,使程序可以在 I/O 阻塞期間繼續運行。(關於阻塞的理解,後邊會談到)
若是在程序中只包含一種類型的任務,那麼比包含多種不一樣類型任務的程序要更容易編寫,錯誤更少,也更容易測試。
在程序中,若是咱們爲每種類型的任務都分配一個專門的線程,那麼能夠造成一種串行執行的假象,並將程序的執行邏輯與調度機制的細節,交替執行的操做,異步 I/O 以及資源等待等問題分離開來。經過使用線程,能夠將複雜而且異步的工做流進一步分解爲一組簡單而且同步的工做流,每一個工做流在一個單獨的線程中運行,並在特定的同步位置進行交互。
Servlet和RMI(Remote Method Invocation) 框架就是一個很好的例子。框架負責解決一些細節問題,包括請求管理、線程建立、負載均衡等,並在正確的時刻將請求分發給正確的應用程序組件(對應的一個具體Servlet)。編寫 Servlet 的開發人員不須要了解有多少請求在同一時刻被處理,也不須要了解套接字的輸入(出)流是否被阻塞。當調用 Servlet 的 service 方法來響應 Web請求時,能夠以同步方式來處理這個請求,就好像它是一個單線程的程序。這種方式簡化了組件的開發,大大下降框架學習門檻。
同步與異步是關於指令執行順序的。
同步是指代碼調用IO操做時,必須等待IO操做完成才返回的調用方式。
異步是指代碼調用IO操做時,沒必要等IO操做完成就返回的調用方式。
異步則須要多線程,多CPU或者非阻塞IO的支持。
借鑑一個例子,來理解同步和異步:
同步:你媽讓你燒壺水,因而你一直在旁邊等着水開 這個時候你什麼都不能作
異步:仍是燒一壺水,你找一個小A來幫你盯着,你就能夠去作別的事了
在這個場景下,你是負責處理請求的線程,小A就是一個新的線程來執行燒水的任務
在線程安全性的定義中,最核心的概念就是正確性。當多個線程訪問某個類時,無論運行時環境採用何種調度方式或者這些線程將如何交替執行,而且在主調代碼中不須要任何額外的同步或協同,這個類都能表現出正確的行爲,那麼這個類就是線程安全的。
// 線程不安全類示例: @NotThreadSafe public class UnsafeSequence { private int value; /** Returns a unique value. */ public int getNext() { return value++; } }
雖然 遞增運算 「value++」 看上去是單個操做,但實際上它包含三個獨立的操做:讀取 value, 將 value 加 1,並將計算結果寫入 value。
開始value的值爲9,A,B 兩個線程都執行getNext()方法,預期的返回值應該是11,由於執行了兩次++操做。
可是在多線程的環境下,A線程從進程讀取 value=9後,發生了線程切換,B線程開始執行,而且也讀到了value=9
A線程開始執行,此處value=9已經記錄到A線程內部,它把線程內部的9進行+1,變成了10,B線程也進行了一樣的操做
此時A線程繼續執行,在執行value=10前,進程裏堆中的value仍是9,執行value=10後,堆中的value就變成10
線程B執行最後的操做,將堆中的value也修改成10,但其實這個時候value已經被A線程修改成10。
這樣AB兩個線程的getNext()都執行完了,可是堆中的value並非預期的11,而是10,這就是線程安全問題。
活躍性問題的關注目標在於 某件正確的事情最終會發生,我片面理解爲程序會不會卡住,從而沒法執行後邊的內容,例如你代碼中無心形成死循環,從而使循環以後的代碼沒法獲得執行。
線程將帶來一些其餘活躍性問題包括死鎖、活鎖和飢餓。這些問題都會讓你的程序卡住,沒法進行下去。
下面簡單描述一下這三個問題,在後邊的篇章會有具體的內容。
死鎖:你要上廁所,但裏面有人,並且把廁所門從裏邊鎖住了,若是他一直不出來,你一直等待,這樣就發生死鎖了。
活鎖:你走在路上,迎面走來一我的,你想給他讓路,結果他也想給你讓路,你倆都作了這個讓路操做後,發現他仍是在
你面前,因而你又讓路,他的想法也和你同樣。因而乎,你倆就處在一直給對方讓路的操做中,誰也沒法經過,這個就是活鎖問題。
飢餓:線程獲取到CPU的時間分片才能執行,CPU分配時間分片是隨機的,哪一個線程搶到哪一個就運行,若是這個線程運氣比較差,永遠搶不到。這個就是飢餓問題。
性能問題關注的是:正確的事情可以儘快發生。性能問題包括多個方面,例如響應不靈敏,吞吐率太低,資源消耗太高等。在多線程程序中,當線程調度器掛起活躍線程並轉而運行另外一個線程時,就會頻繁出現上下文切換操做(Context Switch),這種操做會致使 CPU 時間更多的花在線程調度上而非線程的運行上。
上下文切換操做
多線程編程中通常線程的個數都大於 CPU 核心的個數,而一個 CPU 核心在任意時刻只能被一個線程使用,爲了讓這些線程都能獲得有效執行,CPU 採起的策略是爲每一個線程分配時間片並輪轉的形式。當一個線程的時間片用完的時候就會從新處於就緒狀態讓給其餘線程使用,這個過程就屬於一次上下文切換。
歸納來講就是:當前任務在執行完 CPU 時間片切換到另外一個任務以前會先保存本身的狀態,以便下次再切換會這個任務時,能夠再加載這個任務的狀態。任務從保存到再加載的過程就是一次上下文切換。
上下文切換一般是計算密集型的。也就是說,它須要至關可觀的處理器時間,在每秒幾十上百次的切換中,每次切換都須要納秒量級的時間。
因此,上下文切換對系統來講意味着消耗大量的 CPU 時間,事實上,多是操做系統中時間消耗最大的操做。
Linux 相比與其餘操做系統(包括其餘類 Unix 系統)有不少的優勢,其中有一項就是,其上下文切換和模式切換的時間消耗很是少。
《Java 併發編程實戰》
《Java 編程思想(第4版)》
https://blog.csdn.net/justlov...
https://snailclimb.gitee.io/j...
感謝閱讀!
萬丈高樓平地起,勿在浮沙築高臺。
與君共勉