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實現併發的一種方式。