【JAVA並發第一篇】Java的進程與線程


1、進程與線程

1.1、進程

進程可以看作是程序的執行過程。一個程序的運行需要CPU時間、內存空間、文件以及I/O等資源。操作系統就是以進程為單位來分配這些資源的,所以說進程是分配資源的基本單位。

(1)、進程是動態的,程序是靜態的

程序是靜態的,它本身作為一種軟件資源可以長期保存在磁盤(常說的硬盤)中。比如QQ,QQ作為一個程序,其本身保存在計算機的磁盤上。此時,它並沒有得到CPU、內存、I/O等資源。因此當前的QQ程序只是一個靜態的程序並不能給我們實現視頻、語音等功能。

但當QQ程序開始執行時,操作系統就會將QQ程序從磁盤中裝入內存,同時也會在操作系統中創建屬於QQ的進程,這些新創建的QQ進程會得到操作系統分配的CPU、內存、I/O等資源,得到這些資源后,創建出來的QQ進程就可以實現視頻和語音的功能。而當我們點擊退出QQ后,這些進程就會立刻消亡,分配得到的資源也會被釋放。

由此可以看出,QQ進程是QQ程序的執行過程,它是動態的,有一定的生命周期,會動態的產生和消亡。進程是資源分配的單位。

(2)、程序與進程並不是一 一對應的關系
雖然進程可以看作是程序的執行過程,但並非一個程序對應一個進程,即二者並不是一 一對應的關系。程序與進程的關系可能有以下幾種:

一個程序產生一個進程:比如Win10的記事本程序(notepad.exe),每打開一個txt文本文件,就只會啟動一個記事本進程。

一個程序產生多個進程:比如瀏覽器啟動時,一般都會產生多個進程,這些進程相互配合,互相影響,共同實現瀏覽器的功能。

一個程序可以被多個進程共用:比如一個記事本程序在執行時,就只會產生一個進程。但當我們再用記事本程序打開一個文件時,此時就會再次在操作系統中創建一個新的進程,這個新的進程同樣也會調用記事本程序。即在此刻,計算機磁盤中只有一個記事本程序,但是操作系統中卻有兩個記事本進程在共用這個程序,且這兩個進程互不影響。

一個進程又可能要用到多個程序:比如,用C語言寫了一個helloword.c的程序。此時,輸入命令gcc helloword.c。那么操作系統會創建一個進程,它調用c編譯程序,對helloword.c文件進行編譯。這個進程在執行編譯的過程中,除了調用c編譯程序和我們編寫的helloword程序外,還會用到c預處理程序、連接程序、結果輸出程序等。

1.2、線程

線程從屬於進程,只能在進程的內部活動,多個線程共享進程所擁有的的資源。如果把進程看作是完成許多功能的任務的集合,那么線程就是集合中的一個任務元素,負責具體的功能。雖然CPU、內存、I/O等資源分配給了進程,但實際上真正利用這些資源並在CPU上執行的卻是線程,即真正完成程序功能的是線程。

因為進程作為這些資源的擁有者,它的負載很重,在進程的創建、切換、刪除過程中的時間和空間開銷都很大。所以目前主流的操作系統都只將進程作為資源的擁有者,而把CPU調度和運行的屬性賦予了線程。

比如打開瀏覽器程序,會產生相應的進程,瀏覽器進程中包含有許多線程,如HTTP請求線程,I/O線程,渲染線程,事件響應線程等。瀏覽器進程擁有着內存和I/O資源等,但當我們在瀏覽器中輸入文字時,真正使用I/O資源接收我們輸入的文字,並在CPU處理文字的卻是瀏覽器進程中的I/O線程。即真正完成瀏覽器文字輸入功能的是線程。
在這里插入圖片描述

現代很多操作系統支持讓一個進程包含多個線程,從而提高程序的並行程度和資源的利用率。

1.3、線程與進程的關系

①一個進程可以有多個線程,但至少要有一個線程,並且一個線程只能在一個進程的地址空間內活動。
②資源分配給進程,而一個進程內的所有線程共享該進程的所有資源。
③CPU分配給的是線程,即真正在CPU上運行的是線程。
④進程間通信較為復雜,同一台計算機的進程通信稱為 IPC(Inter-process communication)。
而不同計算機之間的進程通信,則需要通過網絡,並遵守共同的協議,例如 HTTP等。
⑤線程通信相對簡單,因為它們共享進程內的內存,一個例子是多個線程可以訪問同一個共享變量。
⑥線程更輕量,線程上下文切換成本一般上要比進程上下文切換低。

2、Java中的進程與線程

2.1、JVM進程

我們知道Java語言是需要運行在JVM上的。實際上,JVM也是一個軟件程序,這就意味着它執行起來也會在操作系統中創建進程,即JVM進程,通常又叫JVM實例。而我們所寫的main方法,實際上就是JVM進程中主線程的所在。

從操作系統的角度來看,我們常說的Java程序,應該包括JVM和我們編寫的Java代碼。

當我們寫完Java代碼,並編譯成class文件后,使用Java命令執行main方法;或者直接在IDE啟動main方法時,JVM程序就會執行,操作系統會將其從磁盤中裝入內存,並創建一個JVM進程,隨后啟動主線程,主線程會去調用某個類的 main 方法,因此這個主線程就是我們寫的main方法所在。

實際上,JVM本身就是一個多線程應用,即使我們在代碼中並沒有手動的創建線程,JVM進程也並不是只有一個主線程,而是也會有其他線程。這些線程完成着JVM的功能,如GC線程負責回收JVM使用過程中的垃圾對象。JVM進程啟動完成后,必然會有的線程如下:

線程 作用
main 主線程,執行我們指定的啟動類的main方法
Reference Handler 處理引用的線程
Finalizer 調用對象的finalize方法的線程
Signal Dispatcher 分發處理發送給JVM信號的線程
Attach Listener 負責接收外部的命令的線程

至此,我們知道了,啟動一個Java程序,本質上就是啟動JVM程序,並在操作系統中創建一個JVM進程。這個JVM進程會由操作系統分配許多資源,如內存、I/O等。JVM進程中包含有許多線程,這些線程共享JVM進程分配到的資源,同時這些線程也是CPU核心上執行的實體,它們完成着JVM所具有的功能。

那么如果我們啟動兩個Java程序,會生成多少個JVM進程呢?

我們編寫兩個Java程序,其有代碼如下:

processTest01程序

public class processTest01 {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("我是測試01");
        byte[] a = new byte[1024*1024*50]; //在堆中占50MB
        processTest02 test02 = new processTest02();
        Thread.sleep(1000*60*30);   //休眠三十分鍾
    }
}

processTest02程序

public class processTest02 {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("我是測試02");
        byte[] a = new byte[1024*1024*900]; //在堆中占900MB
        Thread.sleep(1000*60*30);   //休眠三十分鍾
    }
}

我們將編譯這兩個程序,並分別用Java命令啟動它們。看看兩個Java程序會在操作系統中創建了多少個進程。
在這里插入圖片描述
打開JDK自帶的jvisualvm.exe,這是JDK提供的查看Java進程和線程相關信息的工具程序,在自己電腦上的JDK目錄下(Win10):Java\jdk1.8.0_131\bin。如圖所示:
在這里插入圖片描述
可以繼續使用jvisualvm,查看進程中線程的相關情況:

pid(進程號)為2146的進程:
在這里插入圖片描述
pid(進程號)為4196的進程:
在這里插入圖片描述
可以看出,啟動多少個Java程序,就會創建多少個JVM進程,也稱之為JVM實例。而每一個JVM實例都是獨立的,它們互不影響。這也是前面所說的一個程序可以被多個進程共用的情況。

一個JVM進程就是一個JVM的實例。JVM的實例在執行Java程序的過程中會把它管理的內存划分為不同區域,稱之為運行時數據區,如下所示:

在這里插入圖片描述

2.2、Java的線程

我們知道,線程從屬於進程,是CPU調度執行的單位,各個線程共享進程內的資源。目前主流的操作系統都支持了線程。在實現了線程的操作系統中,一個進程中必然有至少一個操作系統的線程,這種屬於操作系統的線程被稱為內核線程(kernel-Level Thread,KLT)。

而各個應用程序實現多線程的方式主要有三種:

①內核線程1:1實現
內核線程即操作系統本身的線程,1:1意味着程序中的線程與操作系統中的內核線程是直接對應的。這種線程的創建是由操作系統來完成的,同時也是由操作系統來負責調度的。這種內核線程的切換需要硬件支持,切換所需的時間也較長,但其優點是一個線程阻塞了,其他線程也可以執行,則進程就能繼續工作。但一般來說,程序中的線程不會直接使用內核線程,而是使用它提供的高級接口,稱之為輕量級進程(Light Weight Process,LWP)。雖然名稱變了,但其本質上還是操作系統的內核線程。每個輕量級進程,都由一個內核線程支持,所以他們都可以獨立調度,由操作系統的調度器(Scheduler)負責調度。總的來說就是,程序中的線程是操作系統的內核線程。

在這里插入圖片描述

可以看出,使用內核線程1:1實現的程序可以同時在多個CPU核心上跑。也就是說,程序執行產生的一個進程中的多個線程在同一時刻可能會在不同的CPU核心上運行。這對於一個程序來說,大大加快了運行效率。

②用戶線程1:N實現

用戶線程指的是由用戶程序自主實現,不需要操作系統來實現的線程,一個線程不是內核線程,就可以認為是用戶線程。用戶線程雖然不需要操作系統來實現,但在實現了線程的操作系統中,一個進程中必然要有一個內核線程來支持運行。1:N中的1就是一個內核線程的意思。而N指的是用戶程序自主實現的多用戶線程,操作系統無法得知這些用戶線程的存在,因為這些用戶線程都是在用戶程序內部建立、切換和銷毀的。由於用戶線程不需要操作系統的幫助,所以對於用戶線程的操作可以非常快,消耗低,且不需要硬件的支持。同時,用戶線程的的數量不受操作系統的限制。在沒有實現多線程的操作系統中也可以實現多線程程序。但由於用戶線程需要映射為內核線程才能執行,所以如果一個線程阻塞,那么所有的線程都將阻塞,進程也無法繼續工作。線程的調度也是由用戶程序自主實現。總的來說,用戶線程就是用戶的程序自主實現的線程,多個用戶線程對應着一個操作系統的內核線程
在這里插入圖片描述
可以看出,用戶線程1:N實現的程序,一個進程中的多用戶線程在同一時刻只能在一個CPU核心上運行,因為只有一個內核線程支持着這個進程。從操作系統角度來看,這就是一個單線程的進程。當然,如果一個實現了用戶線程的程序執行產生了多個進程,那么實際上這個程序也可能在多個CPU核心上跑。目前很少有程序實現這種用戶線程了。

③混合N:M實現
混合實現即用戶線程和內核線程一起使用的實現方式。在這種混合實現下,即存在用戶線程,又存在輕量級進程(內核線程)。用戶線程還是由用戶程序自主實現,這樣用戶線程的創建、切換、銷毀依然快速且消耗低。而一個用戶線程的集合(包含一個或多個用戶線程)又與一個內核線程映射。多個用戶線程的集合,就是N:M實現中的N,而M自然指的是多個內核線程。這樣的情況下,也可以繼續使用操作系統的調度功能,而且由於一個內核線程支持着一個用戶線程的集合,所以一個用戶線程阻塞,並不會阻塞其他用戶線程,進程也能繼續工佐。總的來說,混合實現就是一個用戶線程的集合對應着一個內核線程,一個進程中會存在多個用戶線程集合,則會有多個內核線程來支持運行。

在這里插入圖片描述
可以看出,由於用戶線程集合映射到了一個內核線程上,而一個進程又有多個用戶線程集合。所以使用混合實現多線程的程序,進程中也可能存在多個用戶線程在不同CPU核心上執行的情況

Java線程的實現

介紹完程序實現多線程的三種方式,那么Java是如何實現多線程的呢?

首先,Java虛擬機規范並未規定要如何實現多線程,所以Java的線程都是由虛擬機來具體實現,不同的虛擬機實現線程的方式可能都不相同。不過,在JDK1.2之前,早期的虛擬機都采用的是用戶線程1:N的實現方式。而在JDK1.3之后,大部分的虛擬機都采用了內核線程1:1實現的方式,包括我們最常用的HostSpot虛擬機。

這就意味着,我們在平常的開發中,不論是JVM程序自己創建的線程,還是我們手動編碼創建的線程,實際上都是直接1:1映射到了操作系統的內核線程。 這一內核線程由操作系統來創建,且虛擬機不會去干涉線程調度。Java的線程何時交給CPU核心去執行,交給哪個CPU核心,線程有多少CPU核心的執行時間,線程何時凍結、喚醒等等,都交給操作系統去完成,也都是操作系統全權決定(不過Java虛擬機也可以設置線程優先級來給操作系統的線程調度提供建議)。

3、多線程與並行、並發

兩個或兩個以上的線程在同一時刻發生就稱為並行,如兩個線程在同一時刻在兩個不同的CPU核心上執行,則可以說這兩個線程是並行執行。

兩個或兩個以上的線程在同一時間段內發生則稱為並發,比如兩個線程在一個極短的時間段上分別在同一個CPU核心上執行,則可以說這兩個線程是並發執行。

並行與並發的關鍵就在於是否為同一時刻執行,並行是在同一時刻執行,而並發則是在極短的時間內執行。

在一個CPU核心中,線程實際是並發執行的,操作系統中有一個組件叫做任務調度器,將cpu核心的時間片(windows下時間片最小約為 15 毫秒)分給不同的程序使用,只是由於cpu核心在線程間(時間片很短)的切換非常快,給人的感覺是同時運行的,但實際上一個CPU核心同一時刻只能支持一個線程運行。即如果是在單個CPU核心的操作系統中,Java程序(包含JVM)本身雖然是多線程的,但實際上,同一時刻只能有一個Java線程在執行。

在這里插入圖片描述
但目前的計算機已經很少有單個核心的CPU了,目前即使是個人使用的計算機都是多個核心的CPU了,每個核心都可以獨立調度運行線程,這就意味着線程之間可以並行執行。

在這里插入圖片描述
可以看出,在多核心的CPU下,線程之間是可以並行執行的。但即使是擁有多個CPU核心的計算機,CPU核心的數量始終是有限的,而一個操作系統中的線程數遠遠多於CPU核心數,所以線程之間大部分情況下是屬於並發狀態的。即線程之間是在極短時間下交替在CPU核心上執行的。

需要注意的是,在單個CPU核心下,多線程其實是沒有太大意義的,因為始終只能有一個線程在CPU核心上執行,而線程間的切換是需要耗費時間和資源的。但多核CPU可以同時執行線程,如果在多核CPU中還是使用單線程,無疑是對CPU的巨大浪費。並發的最主要的目的就是最大限度利用CPU資源。

但並發並不是線程特有的,進程之間也可以並發。有些語言實現並發就是使用進程來進行並發,如PHP。不過Java的並發依然是依賴於多線程,即多線程是Java實現並發的一種方式。

使用多線程的好處:
(1)、使用更多的CPU核心
一個多線程程序,可以將計算任務分配到多個CPU核心上,則可以顯著減少程序的處理時間,提升程序的效率,並且也是對CPU資源的合理利用。而如果只是單線程程序,則是浪費了多核CPU。
(2)、獲得更快的響應時間
在一個系統中,如果用戶點擊某個按鈕后,需要在程序中執行十幾個操作,才能返回結果給用戶。而如果使用多線程,可以將一些不是必須按順序執行的操作交個其他線程處理,這可以縮短響應時間,提升用戶體驗。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM