java并發(fā)編程之進程和線程調(diào)度基礎詳解
1.什么是進程
進程(process)是計算機中的程序關于某數(shù)據(jù)集合上的一次運行活動,是系統(tǒng)進行資源分配和調(diào)度的基本單位,是操作系統(tǒng)結構的基礎。
在早期面向進程設計的計算機結構中,進程是程序的基本執(zhí)行實體;在當代面向線程設計的計算機結構中,進程是線程的容器。
——摘自百度百科
該怎么理解這句話呢,首先進程和線程都是操作系統(tǒng)層面的概念,不是某種編程語言提出的。進程是操作系統(tǒng)進行資源分配的最小單位,其中資源包括:CPU、內(nèi)存空間、磁盤IO等。進程是程序在計算機上的一次執(zhí)行活動。顯然,程序是死的、靜態(tài)的,進程是活的、動態(tài)的。
進程可以分為系統(tǒng)進程和用戶進程,凡是用于完成操作系統(tǒng)的各種功能的進程就是系統(tǒng)進程,它們就是處于運行狀態(tài)下的操作系統(tǒng)本身,用戶進程就是所有由你啟動的進程。
當你運行一個程序,你就啟動了一個進程。例如我們使用java語言在windows系統(tǒng)中啟動一個main方法,就會系統(tǒng)就會創(chuàng)建一個名為java.exe的進程。
windows系統(tǒng)中使用直接使用任務管理器查看進程
windows系統(tǒng)中dos窗口鍵入 tasklist 命令查看進程
linux系統(tǒng)中可以使用ps命令查看進程
2.什么是線程
線程(thread)是操作系統(tǒng)能夠進行運算調(diào)度的最小單位。
它被包含在進程之中,是進程中的實際運作單位。
一條線程指的是進程中一個單一順序的控制流,一個進程中可以并發(fā)多個線程,每條線程并行執(zhí)行不同的任務。
線程自己基本上不擁有系統(tǒng)資源,只擁有一點在運行中必不可少的資源(如程序計數(shù)器,一組寄存器和棧),但是它可與同屬一個進程的其他的線程共享進程所擁有的全部資源。
3.進程和線程的區(qū)別與聯(lián)系
聯(lián)系:
- 進程是線程的容器,一個進程可以創(chuàng)建多個線程分支,兩者之間存在包含關系。
- 兩者都是多任務編程方式,都能使用計算機的多核資源。
- 線程具有進程的許多特征,故又稱輕型進程,傳統(tǒng)進程稱重型進程。
- 一個程序至少有一個進程,一個進程至少有一個線程。
- 線程是CPU調(diào)度的最小單位,必須依賴于進程而存在
區(qū)別:
- 其實從概念上看的出,進程主要用于操作系統(tǒng)資源分配,線程主要在于CPU運算調(diào)度
- 進程和進程之間是相互獨立的,每個進程有獨立的地址空間,一個進程崩潰后,在保護模式下不會對其它進程產(chǎn)生影響。而同一個進程下的多個線程共享資源。
- 線程的劃分尺度小于進程,使得多線程程序的并發(fā)性高。
- 在執(zhí)行過程中,每個獨立的線程有一個程序運行的入口、順序執(zhí)行序列和程序的出口。但是線程不能夠獨立執(zhí)行,必須依存在應用程序中,由應用程序提供多個線程執(zhí)行控制。
- 相對進程而言,線程是一個更加接近于執(zhí)行體的概念,它可以與同進程中的其他線程共享數(shù)據(jù),但擁有自己的??臻g,擁有獨立的執(zhí)行序列。
4.CPU內(nèi)核數(shù)和線程數(shù)的關系
通常我們在選購電腦的時候,CPU是一個需要考慮到核心因素,因為它決定了電腦的性能等級。
CPU從早期的單核,發(fā)展到現(xiàn)在的雙核,多核。在介紹CPU屬性時經(jīng)常會說“幾核幾線程”。
那么這個參數(shù)到底代表什么意思呢?
“幾核”,就是指CPU核心數(shù),“幾線程”,就是線程數(shù),也稱邏輯CPU個數(shù)。
CPU的核心數(shù) 是指物理上,也就是硬件上存在著幾個核心。比如,雙核就是包括2個相對獨立的CPU核心單元組,四核就包含4個相對獨立的CPU核心單元組。
線程數(shù) 表示該CPU能同一時刻能夠執(zhí)行的最大線程數(shù)量 也稱邏輯CPU數(shù) 是一種邏輯的概念,簡單地說,就是模擬出的CPU核心數(shù)。一個核心最少對應一個線程,但引入超線程技術后一個核心可以對應兩個線程,也就是說它可以同時運行兩個線程。
對于win7操作系統(tǒng)來說
我們從任務管理器的性能標簽頁,以及設備管理器中處理器看到的就是線程數(shù)。
DOS窗口中輸入“wmic”
然后在出現(xiàn)的新窗口中輸入“cpu get * ”也可查看物理CPU數(shù)、CPU核心數(shù)、線程數(shù)。
- Name:物理CPU名稱
- NumberOfCores:表示CPU核心數(shù)
- NumberOfLogicalProcessors:表示CPU線程數(shù)
所以我這臺win7主機CPU配置的就是 2核心 4線程
而對于win10系統(tǒng)來說,任務管理器中已經(jīng)標明了線程數(shù)。
在Linux系統(tǒng)中,可以根據(jù)下面命令查看
查詢CPU個數(shù):
cat /proc/cpuinfo| grep "physical id"| sort| uniq| wc -l
查詢核心數(shù):
cat /proc/cpuinfo| grep "cpu cores"| uniq
查詢邏輯CPU總數(shù)(線程數(shù)):
cat /proc/cpuinfo| grep "processor"| wc -l
當然我們java也可以查看CPU線程數(shù)
public static void main(String[] args) { int i = Runtime.getRuntime().availableProcessors(); System.out.println("當前主機CPU線程數(shù):"+i); }
5.CPU時間片輪轉調(diào)度機制
不知道大家有沒有疑問,一般高配置服務器的CPU 核心數(shù)也僅僅有32核,使用超線程技術同一時刻能執(zhí)行的最大線程數(shù)也就是64,而服務器卻能承載一秒10w多的訪問量。而且我們平時在開發(fā)的時候,感覺并沒有受 CPU 核心數(shù)的限制,想啟動線程就啟動線程,哪怕是在單核 CPU 上,為什么?
其實首先我們得明確兩個概念,一個是并行 一個是并發(fā)
并行 是指兩個或者多個事件在同一時刻發(fā)生。
并發(fā) 是指兩個或多個事件在同一時間間隔發(fā)生,當談論并發(fā)的時候一定要加個單位時間,也就是說單位時間內(nèi)并發(fā)量是多少?離開了單位時間其實是沒有意義的。
所以可以說,上面那個服務器 并行數(shù)是64,而每秒能承載的并發(fā)量是10w
那么CPU怎么做到每秒執(zhí)行10w線程數(shù)的呢,這是由于操作系統(tǒng)提供了一種CPU時間片輪轉調(diào)度機制。
操作系統(tǒng)一般是按照一定策略,定期給每個活動的進程執(zhí)行其內(nèi)部程序的機會,并且每次只執(zhí)行一小段時間,然后操作系統(tǒng)利用中斷強行退出執(zhí)行,將當前程序信息壓棧,然后開始執(zhí)行下一個進程的一小段程序。通過這樣不斷快速的循環(huán)切換,每個程序都獲得執(zhí)行,在用戶看來,感覺到很多程序都在平行的執(zhí)行。
當然在自己的程序運行時不是獨一無二的,我們看似很順暢的工作,其實是由一個個的執(zhí)行片段構成的,我們眼中相鄰的兩條語句甚至同一個語句中兩個不同的運算符之間,都有可能插入其他線程或進程的動作。
時間片輪轉調(diào)度是一種最古老、最簡單、最公平且使用最廣的算法,又稱RR(Round-Robin)調(diào)度。
每個進程被分配一個時間段,稱作它的時間片,即該進程允許運行的時間。
百度百科對CPU時間片輪轉機制原理解釋如下:
如果在時間片結束時進程還在運行,則CPU將被剝奪并分配給另一個進程。
如果進程在時間片結束前阻塞或結束,則CPU當即進行切換。
調(diào)度程序所要做的就是維護一張就緒進程列表,當進程用完它的時間片后,它被移到隊列的末尾。
時間片輪轉算法的基本思想是,系統(tǒng)將所有的就緒進程按先來先服務算法的原則,排成一個隊列,每次調(diào)度時,系統(tǒng)把處理機分配給隊列首進程,并讓其執(zhí)行一個時間片。
當執(zhí)行的時間片用完時,由一個計時器發(fā)出時鐘中斷請求,調(diào)度程序根據(jù)這個請求停止該進程的運行,將它送到就緒隊列的末尾,再把處理機分給就緒隊列中新的隊列首進程,同時讓它也執(zhí)行一個時間片。
6.上下文切換
而這種時間片輪轉是有代價的,往往還會伴隨著上下文切換。
任何對進程或者線程的調(diào)度,都會引入額外的開銷,這個開銷中就包括上下文切換(Context Switch),有時也稱做進程切換或任務切換,是指CPU 從一個進程或線程切換到另一個進程或線程。
在上下文切換過程中,CPU會停止處理當前運行的程序,并保存當前程序運行的具體位置以便之后繼續(xù)運行。從這個角度來看,上下文切換有點像我們同時閱讀幾本書,在來回切換書本的同時我們需要記住每本書當前讀到的頁碼。
例如我們經(jīng)常用做緩存的redis,采用的就是單線程的方式,大大減少上下文切換產(chǎn)生的額外開銷。
上下文切換通常是計算密集型的。也就是說上下文切換對系統(tǒng)來說意味著消耗大量的 CPU 時間,事實上,可能是操作系統(tǒng)中時間消耗最大的操作。
根據(jù)Tsuna的測試報告,每次上下文切換都需要幾十納秒到數(shù)微妙的CPU時間。這個時間還是相當可觀的,特別是上下文切換次數(shù)較多的情況下,很容易導致CPU將大量的時間耗費在寄存器、內(nèi)核棧、以及虛擬內(nèi)存等資源的保存和恢復上,進而大大縮短了真正運行的時間。
引起線程上下文切換的原因大概有以下幾種:
- 當前執(zhí)行任務的時間片用完之后,系統(tǒng)CPU正常調(diào)度下一個任務。
- 當前執(zhí)行任務碰到IO阻塞,調(diào)度器將此任務掛起,繼續(xù)下一任務。
- 多個任務搶占鎖資源,當前任務沒有搶到鎖資源,被調(diào)度器掛起,繼續(xù)下一任務。
- 用戶代碼掛起當前任務,讓出CPU時間。
7.并發(fā)編程的意義、好處
由于多核多線程的CPU的誕生,多線程、高并發(fā)的編程越來越受重視和關注。多線程可以給程序帶來如下好處。
(1)充分利用CPU的資源
從上面的CPU的介紹,可以看的出來,現(xiàn)在市面上沒有CPU的內(nèi)核不使用多線程并發(fā)機制的,特別是服務器還不止一個CPU,如果還是使用單線程的技術做思路,明顯就out了。因為程序的基本調(diào)度單元是線程,并且一個線程也只能在一個CPU的一個核的一個線程跑,如果你是個i3的CPU的話,最差也是雙核心4線程的運算能力:如果是一個線程的程序的話,那是要浪費3/4的CPU性能:如果設計一個多線程的程序的話,那它就可以同時在多個CPU的多個核的多個線程上跑,可以充分地利用CPU,減少CPU的空閑時間,發(fā)揮它的運算能力,提高并發(fā)量。
就像我們平時坐地鐵一樣,很多人坐長線地鐵的時候都在認真看書,而不是到家了再去看書,這樣你的時間就相當于有了兩倍。這就是為什么有些人時間很充裕,而有些人老是說沒時間的一個原因,工作也是這樣,有的時候可以并發(fā)地去做幾件事情,充分利用我們的時間,CPU也是一樣,也要充分利用。
(2)加快響應用戶的時間
比如我們經(jīng)常用的迅雷下載,都喜歡多開幾個線程去下載,誰都不愿意用一個線程去下載,為什么呢?答案很簡單,就是多個線程下載快啊。
(3)可以使你的代碼模塊化,異步化,簡單化
例如我們實現(xiàn)電商系統(tǒng),下訂單和給用戶發(fā)送短信、郵件就可以進行拆分,將給用戶發(fā)送短信、郵件這兩個步驟獨立為單獨的模塊,并交給其他線程去執(zhí)行。這樣既增加了異步的操作,提升了系統(tǒng)性能,又使程序模塊化,清晰化和簡單化。
多線程應用開發(fā)的好處還有很多,大家在日后的代碼編寫過程中可以慢慢體會它的魅力。
8.并發(fā)編程需要注意事項
1.線程之間的安全性
從前面的章節(jié)中我們都知道,在同一個進程里面的多線程是資源共享的,也就是都可以訪問同一個內(nèi)存地址當中的一個變量。
例如:若每個線程中對全局變量、靜態(tài)變量只有讀操作,而無寫操作,一般來說,這個全局變量是線程安全的:若有多個線程同時執(zhí)行寫操作,一般都需要考慮線程同步,否則就可能影響線程安全。
2.線程之間的死鎖
為了解決線程之間的安全性引入了Java的鎖機制,而一不小心就會產(chǎn)生Java線程死鎖的多線程問題,因為不同的線程都在等待那些根本不可能被釋放的鎖,從而導致所有的工作都無法完成。假設有兩個線程,分別代表兩個饑餓的人,他們必須共享刀叉并輪流吃飯。他們都需要獲得兩個鎖:共享刀和共享叉的鎖。
假如線程A獲得了刀,而線程B獲得了叉。線程A就會進入阻塞狀態(tài)來等待獲得叉,而線程B則阻塞來等待線程A所擁有的刀。這只是人為設計的例子,但盡管在運行時很難探測到,這類情況卻時常發(fā)生
3.線程太多了會將服務器資源耗盡形成死機宕機
線程數(shù)太多有可能造成系統(tǒng)創(chuàng)建大量線程而導致消耗完系統(tǒng)內(nèi)存以及CPU的“過渡切換”,造成系統(tǒng)的死機,那么我們該如何解決這類問題呢?
某些系統(tǒng)資源是有限的,如文件描述符。多線程程序可能耗盡資源,因為每個線程都可能希望有一個這樣的資源。如果線程數(shù)相當大,或者某個資源的侯選線程數(shù)遠遠超過了可用的資源數(shù)則最好使用資源池。一個最好的示例是數(shù)據(jù)庫連接池。只要線程需要使用一個數(shù)據(jù)庫連接,它就從池中取出一個,使用以后再將它返回池中。資源池也稱為資源庫。
多線程應用開發(fā)的注意事項很多,希望大家在日后的工作中可以慢慢體會它的危險所在。
總結
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關文章
spring boot @PathVariable傳遞帶反斜杠參數(shù) / 的處理
這篇文章主要介紹了spring boot @PathVariable傳遞帶反斜杠參數(shù) / 的處理操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-02-02Spring?@bean和@component注解區(qū)別
本文主要介紹了Spring?@bean和@component注解區(qū)別,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-01-01SpringSecurity JWT基于令牌的無狀態(tài)認證實現(xiàn)
Spring Security中實現(xiàn)基于JWT的無狀態(tài)認證是一種常見的做法,本文就來介紹一下SpringSecurity JWT基于令牌的無狀態(tài)認證實現(xiàn),感興趣的可以了解一下2025-04-04Java中JSONObject與JSONArray的使用區(qū)別詳解
這篇文章主要介紹了Java中JSONObject與JSONArray的使用區(qū)別詳解,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-11-11Springboot優(yōu)化內(nèi)置服務器Tomcat優(yōu)化方式(underTow)
本文詳細介紹了Spring Boot中Tomcat和Undertow服務器的配置和優(yōu)化,包括初始線程數(shù)、最大線程數(shù)、最小備用線程數(shù)、最大請求數(shù)等參數(shù)的優(yōu)化建議,以及在高并發(fā)場景下Undertow相對于Tomcat的優(yōu)勢2024-12-12