線程的性能問(wèn)題
在并發(fā)編程中我們要注意的線程風(fēng)險(xiǎn)主要有三個(gè)方面:安全性問(wèn)題、活躍性問(wèn)題和性能問(wèn)題。其中安全性問(wèn)題和活躍性問(wèn)題我們?cè)谇懊嬉呀?jīng)介紹過(guò)了,這里我們主要介紹下使用線程所帶來(lái)的性能問(wèn)題。
一、對(duì)性能問(wèn)題的思考1.為什么使用多線程
線程的最主要目的是提高程序的運(yùn)行性能,使得程序更加充分的發(fā)揮系統(tǒng)的可用處理能力,從而提高系統(tǒng)的資源利用率。此外,線程還可以使程序在運(yùn)行現(xiàn)有任務(wù)的情況下立即開(kāi)始處理新的任務(wù),從而提高系統(tǒng)的響應(yīng)性。多線程所帶來(lái)的優(yōu)勢(shì)如下所示。
- 提高系統(tǒng)的吞吐率。多線程可以使得在一個(gè)進(jìn)程中有多個(gè)線程進(jìn)行并發(fā)操作。當(dāng)一個(gè)線程由于 I/O 阻塞操作處于等待時(shí),其他線程仍然可以執(zhí)行其操作。
- 提高響應(yīng)性。在使用多線程編程的情況下,對(duì)于 web 應(yīng)用程序而言,一個(gè)請(qǐng)求處理響應(yīng)慢了并不會(huì)影響其他請(qǐng)求的處理。
- 充分利用多核處理器資源。如今多核處理器的設(shè)備越來(lái)越普及,使用恰當(dāng)?shù)亩嗑€程有利于充分利用多核處理器的資源,從而避免了浪費(fèi)資源。
- 最小化對(duì)系統(tǒng)資源的利用。一個(gè)進(jìn)程中多個(gè)線程可以共享其所在進(jìn)程所申請(qǐng)的資源,因此使用多個(gè)線程相比于使用多個(gè)進(jìn)程進(jìn)行編程,節(jié)約了對(duì)系統(tǒng)資源的使用。
- 簡(jiǎn)化程序的結(jié)構(gòu)。線程可以簡(jiǎn)化復(fù)雜應(yīng)用程序的結(jié)構(gòu)。
2.多線程與性能問(wèn)題
應(yīng)用程序的性能可以采用多個(gè)指標(biāo)來(lái)衡量,例如服務(wù)時(shí)間、延遲時(shí)間、吞吐率、可伸縮性以及容量等。其中一些指標(biāo)如服務(wù)時(shí)間、等待時(shí)間用于衡量程序的“運(yùn)行速度”,即某個(gè)指定的任務(wù)單元需要“多快”才能處理完成,屬于時(shí)間維度。另一些指標(biāo)如生產(chǎn)量、吞吐量用于程序的“處理能力”,即在計(jì)算資源一定的情況下,能完成“多少”的工作,屬于空間維度。
性能問(wèn)題包含多個(gè)方面,例如服務(wù)時(shí)間過(guò)長(zhǎng)、響應(yīng)不靈敏、吞吐率過(guò)低、資源消耗過(guò)高或可伸縮性較低等。與安全性和活躍性一樣,在多線程程序中不僅存在與單線程程序相同的性能問(wèn)題,而且還存在由于使用線程而引入的其他性能問(wèn)題。
在設(shè)計(jì)良好的多線程應(yīng)用程序中,線程能提升程序的性能。但不管怎樣,使用線程所帶來(lái)的開(kāi)銷是不可避免的。在多線程程序中,當(dāng)線程調(diào)度器臨時(shí)掛起活躍線程并轉(zhuǎn)而運(yùn)行另一個(gè)線程時(shí),就會(huì)頻繁的出現(xiàn)上下文切換操作,這種操作將會(huì)帶來(lái)極大的開(kāi)銷:保存和恢復(fù)執(zhí)行上下文,丟失局部性,并且 CPU 時(shí)間將更多地花在線程調(diào)度而不是線程運(yùn)行上。當(dāng)線程共享數(shù)據(jù)時(shí),必須使用同步機(jī)制,而這些機(jī)制往往會(huì)抑制某些編譯器優(yōu)化,使內(nèi)存緩沖區(qū)中的數(shù)據(jù)無(wú)效,以及增加共享內(nèi)存總線的同步流量。
二、多線程帶來(lái)的開(kāi)銷盡管我們使用多線程的目的是為了提升程序的整體性能,但是與單線程相關(guān)的方法相比,使用多線程也引入了額外的性能開(kāi)銷。造成這些開(kāi)銷的操作有:線程之間的協(xié)調(diào)同步、線程的上下文切換、線程的創(chuàng)建與銷毀以及線程的調(diào)度等。如果過(guò)度的創(chuàng)建并使用多線程,那么這些多線程所帶來(lái)的性能開(kāi)銷甚至可能會(huì)遠(yuǎn)超過(guò)由于提高吞吐量、響應(yīng)性或者計(jì)算能力所帶來(lái)的性能提升。因此,對(duì)于為了提升性能而引入的線程來(lái)說(shuō),并發(fā)所帶來(lái)的性能提升必須超過(guò)并發(fā)導(dǎo)致的開(kāi)銷。這里我們了解下多線程情況下線程的上下文切換、內(nèi)存同步以及阻塞所帶來(lái)的開(kāi)銷。
1.上下文切換
上下文切換在某種程度上可以被看作多個(gè)線程共享同一個(gè)處理器的產(chǎn)物,它是多線程編程中的一個(gè)概念。這里我們所講的上下文切換都是指線程的上下文切換。
1.1 上下文切換及其產(chǎn)生原因
在單處理器上,我們也可以通過(guò)使用多線程的方式實(shí)現(xiàn)并發(fā),即一個(gè)處理器可以在同一時(shí)間段內(nèi)運(yùn)行多個(gè)線程,線程每次占用處理器所運(yùn)行的時(shí)間稱為時(shí)間片。單處理器上的多線程就是通過(guò)這種時(shí)間片輪轉(zhuǎn)的方式實(shí)現(xiàn)的。時(shí)間片決定了一個(gè)線程可以連續(xù)占用處理器運(yùn)行的時(shí)間長(zhǎng)度。當(dāng)一個(gè)進(jìn)程中的一個(gè)線程由于其時(shí)間片用完或其自身的原因被迫或者主動(dòng)暫停運(yùn)行時(shí),另外一個(gè)線程(可能是同一個(gè)進(jìn)程或者其他進(jìn)程中的線程)可以被操作系統(tǒng)的線程調(diào)度器選中占用處理器開(kāi)始運(yùn)行或繼續(xù)運(yùn)行。這種一個(gè)線程被暫停,即被剝奪處理器的使用權(quán),另外一個(gè)線程被選中開(kāi)始或者繼續(xù)運(yùn)行的過(guò)程就叫作線程上下文切換。
相應(yīng)地,一個(gè)線程被剝奪處理器的使用權(quán)而被暫停運(yùn)行就被稱為切出,一個(gè)線程被操作系統(tǒng)選中占用處理器開(kāi)始或者繼續(xù)其運(yùn)行就被稱為切入。
由此可見(jiàn),我們看到的連續(xù)運(yùn)行的線程,實(shí)際上是以斷斷續(xù)續(xù)運(yùn)行的方式使其任務(wù)進(jìn)展的。該方式意味著在切入和切出的時(shí)候操作系統(tǒng)需要保存和恢復(fù)相應(yīng)進(jìn)程的進(jìn)度信息,即要讓操作系統(tǒng)記得在切入和切出的那一刻各個(gè)線程都執(zhí)行到哪里了(如程序執(zhí)行到一半的中間結(jié)果以及執(zhí)行到了哪條指令)。這些需要保存的進(jìn)度信息就被稱為上下文,一般包含通用寄存器的內(nèi)容和程序計(jì)數(shù)器的內(nèi)容。在線程切出時(shí),操作系統(tǒng)需要將其上下文保存到內(nèi)存中,以便被該線程在下次占用處理器切入時(shí)能夠在原先從基礎(chǔ)上繼續(xù)運(yùn)行。在線程切入時(shí),操作系統(tǒng)需要從內(nèi)存中恢復(fù)對(duì)應(yīng)線程的上下文,以便該線程在原來(lái)的基礎(chǔ)上繼續(xù)運(yùn)行。
從 JVM 的角度來(lái)看,一個(gè)線程的生命周期在 RUNNABLE 狀態(tài)與非 RUNNABLE 狀態(tài)(BLOCKED、WAITING 和 TIMED_WAITING)中切換的過(guò)程就是一個(gè)上下文切換的過(guò)程。當(dāng)一個(gè)線程的生命周期狀態(tài)由 RUNNABLE 轉(zhuǎn)換為非 RUNNABLE 時(shí),可以稱這個(gè)線程被暫停。線程的暫停就是相應(yīng)狀態(tài)被切出的過(guò)程,這時(shí)操作系統(tǒng)會(huì)保存相應(yīng)線程的上下文,以便該線程稍后再次進(jìn)入 RUNNABLE 狀態(tài)時(shí)能夠在之前執(zhí)行進(jìn)度的基礎(chǔ)上繼續(xù)進(jìn)展。而一個(gè)線程的狀態(tài)由非 RUNNABLE 狀態(tài)進(jìn)入 RUNNABLE 狀態(tài)時(shí),我們稱這個(gè)線程被喚醒。一個(gè)線程被喚醒僅代表該線程獲得了一個(gè)繼續(xù)運(yùn)行的機(jī)會(huì),而并不代表其可以立刻占用處理器運(yùn)行。因此,當(dāng)被喚醒的線程被操作系統(tǒng)選中占用處理器繼續(xù)運(yùn)行的時(shí)候,操作系統(tǒng)會(huì)恢復(fù)之前為其保存的上下文,以便在此基礎(chǔ)上繼續(xù)運(yùn)行。
1.2 上下文切換的分類及具體誘因
可以按照導(dǎo)致上下文切換的因素,可以分為自發(fā)性上下文切換和非自發(fā)性上下文切換。
1.2.1 自發(fā)性上下文切換
自發(fā)性上下文切換是指線程由于其自身因素導(dǎo)致的切出,從 JVM 的角度看,一個(gè)線程在其運(yùn)行過(guò)程中執(zhí)行下列任意一個(gè)方法都會(huì)都會(huì)引起自發(fā)性的上下文切換。
- Thread.sleep(long millis)
- Object.wait(long timeout)/wait(long timeout, int nanos)
- Thread.yield() ?具體是否執(zhí)行上下文切換還得看線程調(diào)度器的心情
- Thread.join()/Thread.join(long timeout)
- LockSupport.park()
另外,線程發(fā)起 I/O 操作(阻塞式 I/O)或者等待其他線程持有的鎖時(shí)也會(huì)導(dǎo)致自發(fā)性的上下文切換。當(dāng)阻塞式 I/O 完成時(shí),硬盤(pán)會(huì)用一種中斷機(jī)制來(lái)通知 CPU。
1.2.2 非自發(fā)性上下文切換
非自發(fā)性的上下文切換是指線程由于線程調(diào)度器的原因被迫切出。導(dǎo)致非自發(fā)性的上下文切換的因素有:切出線程的時(shí)間片被用完或者有優(yōu)先級(jí)更高的線程需要執(zhí)行。從 JVM 的角度看,JVM 在 GC 過(guò)程中也可能導(dǎo)致非自發(fā)性的上下文切換。這是因?yàn)槔占髟趫?zhí)行 GC 過(guò)程中可能需要停止所有線程(STW,stop the world)才能完成工作。
1.3 上下文切換的開(kāi)銷和測(cè)量
一方面,上下文切換是必要的,就算是在多核處理器中上下文切換也是必要的,因?yàn)橐粋€(gè)系統(tǒng)上運(yùn)行的線程數(shù)量相對(duì)于該系統(tǒng)上所擁有的處理器數(shù)量是大很多的。另一方面,上下文切換帶來(lái)的開(kāi)銷也是巨大的。
1.3.1 定性角度
- 直接開(kāi)銷
- 操作系統(tǒng)保存和恢復(fù)上下文所需的開(kāi)銷,主要是處理器時(shí)間開(kāi)銷
- 線程調(diào)度器調(diào)度線程的開(kāi)銷
- 間接開(kāi)銷
- 處理器高速緩存重新加載的開(kāi)銷。一個(gè)被切出的線程可能之后在另一個(gè)處理器被切入,因?yàn)檫@個(gè)處理器可能之前沒(méi)有運(yùn)行過(guò)該線程,那么該線程運(yùn)行過(guò)程中所需的變量仍然需要被該處理器從主內(nèi)存或者通過(guò)緩存一致性協(xié)議從其他處理器加載到高度緩存中,這也是有一定的時(shí)間消耗的。
- 上下文切換可能導(dǎo)致整個(gè)一級(jí)高速緩存中的內(nèi)容被沖刷到下一級(jí)高速緩存或者主內(nèi)存中。
1.3.2 定量角度
從定量的角度來(lái)看,一次線程上下文切換的時(shí)間消耗是微秒級(jí)的。線程的數(shù)量越多,它可能導(dǎo)致的上下文切換的開(kāi)銷也就越大,計(jì)算效率也就越低。在 Windows 上我們可以用自帶的工具 perfom (C:\Windows\System32\perfom.exe) 來(lái)監(jiān)視 Java 程序在運(yùn)行過(guò)程中的上下文切換情況。
2.內(nèi)存同步
同步操作的性能開(kāi)銷包括多個(gè)方面。在 synchronized 和 volatile 提供的可見(jiàn)性保證中可能會(huì)使用一些特殊指令,即內(nèi)存屏障指令。內(nèi)存屏障可以刷新緩存,使緩存無(wú)效,刷新硬件的寫(xiě)緩沖區(qū)。內(nèi)存屏障也可能同樣會(huì)對(duì)性能帶來(lái)間接的影響,因?yàn)樗鼈儗⒁种埔恍┚幾g器優(yōu)化操作。在內(nèi)存屏障中,大多數(shù)操作是不能被重排序的。
JVM 也會(huì)通過(guò)一些優(yōu)化來(lái)去掉一些不會(huì)發(fā)生競(jìng)爭(zhēng)的鎖,從而減少不必要的同步開(kāi)銷。如果一個(gè)鎖對(duì)象只能由當(dāng)前線程訪問(wèn),那么 JVM 就可以通過(guò)優(yōu)化來(lái)去掉這個(gè)鎖的獲取操作,因?yàn)榱硪粋€(gè)線程無(wú)法與當(dāng)前線程在這個(gè)鎖上發(fā)生同步,這個(gè)編譯器優(yōu)化被稱為鎖消除。除此之外,JVM 也會(huì)執(zhí)行鎖粗化的操作,將鄰近的同步代碼塊用同一個(gè)鎖合并起來(lái),不僅減少了同步的開(kāi)銷,還能使優(yōu)化器處理更大的代碼塊,從而可能實(shí)現(xiàn)進(jìn)一步的優(yōu)化。
3.阻塞
當(dāng)多線程情況下在鎖上發(fā)生競(jìng)爭(zhēng)時(shí),競(jìng)爭(zhēng)失敗的線程會(huì)進(jìn)入“阻塞狀態(tài)”。JVM 在實(shí)現(xiàn)“阻塞行為“時(shí),可以通過(guò)自旋等待的方式直到成功獲取到鎖或者通過(guò)操作系統(tǒng)掛起被阻塞的線程。這兩種方式的效率高低,取決于上下文切換的開(kāi)銷以及在成功獲取鎖之前需要等待的時(shí)間。如果等待的時(shí)間比較短,則可以采用自旋等待的方式,而如果等待的時(shí)間較長(zhǎng),則適合采用將等待線程掛起的方式。
當(dāng)線程無(wú)法獲取到鎖或者在進(jìn)行 I/O 操作阻塞時(shí),需要被掛起,在此過(guò)程中會(huì)包含兩次額外的上下文切換以及必要的操作系統(tǒng)操作和緩存操作:被阻塞的線程在其執(zhí)行時(shí)間片還未用完之前就被掛起切出,而在隨后當(dāng)要獲取鎖或者其他可用資源時(shí),又再次切入回來(lái)。由于鎖競(jìng)爭(zhēng)導(dǎo)致的阻塞時(shí),線程在持有鎖時(shí)會(huì)有一定的開(kāi)銷,當(dāng)它釋放鎖時(shí),必須告訴操作系統(tǒng)恢復(fù)運(yùn)行被阻塞的線程。
三、降低多線程的開(kāi)銷1.減少鎖的競(jìng)爭(zhēng)
1.1 無(wú)鎖的算法與數(shù)據(jù)結(jié)構(gòu)
既然使用鎖會(huì)帶來(lái)性能問(wèn)題,那么最好的方案就是使用無(wú)鎖化編程,在這方面有很多相關(guān)的技術(shù),如線程本地存儲(chǔ)(Thread Local Storage,TLS)、寫(xiě)時(shí)復(fù)制(Copy-on-write)、樂(lè)觀鎖等;J.U.C 包中的原子類就采用了無(wú)鎖的數(shù)據(jù)結(jié)構(gòu),底層使用了處理器提供的 cmpxchg 指令;Disruptor 是一個(gè)無(wú)鎖的內(nèi)有界存隊(duì)列,在優(yōu)化并發(fā)性能方面可謂是做到了極致。
1.2 減少鎖的持有時(shí)間
互斥鎖本質(zhì)上是將并行的程序串行化,所以要增加并行度,一定要減少鎖的持有時(shí)間。
縮小鎖的范圍:將一些與鎖無(wú)關(guān)的代碼移出同步代碼塊,尤其是那些開(kāi)銷較大的操作以及可能被阻塞的操作減少鎖的代碼塊范圍來(lái)減小鎖的持有時(shí)間。
使用細(xì)粒度的鎖:如通過(guò)鎖分段技術(shù),將采用多個(gè)相互獨(dú)立的鎖來(lái)保護(hù)獨(dú)立的狀態(tài)變量,從而改變這些變量在之前由單個(gè)鎖來(lái)保護(hù)的情況。這些技術(shù)能減小鎖操作的粒度來(lái)減少鎖的持有時(shí)間。鎖分段的一個(gè)劣勢(shì):與采用單個(gè)鎖來(lái)實(shí)現(xiàn)獨(dú)占訪問(wèn)相比,要獲取多個(gè)鎖來(lái)實(shí)現(xiàn)獨(dú)占訪問(wèn)將更加困難并且開(kāi)銷更高。通常,在執(zhí)行一個(gè)操作時(shí)最多只需要獲取一個(gè)鎖,但在某些情況下需要加鎖整個(gè)容器,ConcurrentHashMap 就是采用了所謂分段鎖的技術(shù)。
2.減少上下文切換的開(kāi)銷
在服務(wù)器應(yīng)用程序中,發(fā)生阻塞的原因之一就是在處理請(qǐng)求時(shí)產(chǎn)生各種日志消息。下面會(huì)通過(guò)對(duì)兩種日志方法的調(diào)度進(jìn)行分析來(lái)說(shuō)明如何通過(guò)減少上下文切換的次數(shù)來(lái)提高吞吐量。
2.1 日志操作方法
大多數(shù)日志框架都是簡(jiǎn)單地對(duì) println 進(jìn)行包裝,當(dāng)需要記錄某個(gè)消息時(shí),只需要將其寫(xiě)入日志文件中。其他方法也有,如記錄日志的工作由一個(gè)專門的后臺(tái)線程完成,而不是由發(fā)出請(qǐng)求的線程完成。從開(kāi)發(fā)人員角度看,這兩種方法基本上是差不多的,但二者在性能上可能存在差異,這取決于日志操作的工作量,即打印日志線程的數(shù)量等其他一些因素,例如上下文切換的開(kāi)銷等。
2.2 日志操作的開(kāi)銷
日志操作的服務(wù)時(shí)間包括處理 I/O 流的計(jì)算時(shí)間,如果 I/O 操作被阻塞,那么也包含線程被阻塞的時(shí)間。操作系統(tǒng)會(huì)把這個(gè)被阻塞的線程從調(diào)度隊(duì)列中移走,直到 I/O 操作執(zhí)行結(jié)束,這會(huì)消耗比實(shí)際阻塞更長(zhǎng)的時(shí)間。I/O 操作執(zhí)行結(jié)束的時(shí)候,當(dāng)前的處理器可能在執(zhí)行其他線程的調(diào)度時(shí)間片,對(duì)于被阻塞的線程,在調(diào)度隊(duì)列中,也可能會(huì)有其他線程在其之前,從而進(jìn)一步增加服務(wù)時(shí)間。如果有多個(gè)線程在同時(shí)記錄日志,為了使得日志按順序被記錄,在輸入流上也會(huì)有鎖競(jìng)爭(zhēng)的開(kāi)銷。該情況與 I/O 阻塞一樣,線程的加鎖操作也會(huì)導(dǎo)致上下文切換的次數(shù)增多,以及服務(wù)時(shí)間的增加。
2.3 降低日志操作的開(kāi)銷
- 降低請(qǐng)求服務(wù)時(shí)間
服務(wù)時(shí)間會(huì)影響服務(wù)質(zhì)量,服務(wù)時(shí)間越長(zhǎng),意味著有程序在獲得結(jié)果時(shí)需要等待更長(zhǎng)的時(shí)間,也意味著存在著更多的鎖競(jìng)爭(zhēng)。因?yàn)殒i被持有的時(shí)間越長(zhǎng),那么在這個(gè)鎖上發(fā)生競(jìng)爭(zhēng)的可能性就越大。如果一個(gè)線程由于等待 I/O 操作而阻塞,同時(shí)它還持有這個(gè)鎖,那么在等待的期間可能會(huì)有另外一個(gè)線程也想要獲得這個(gè)鎖,從而再次進(jìn)入等待。如果在大多數(shù)獲得鎖的操作中不存在競(jìng)爭(zhēng),那么該并發(fā)系統(tǒng)的執(zhí)行效率就越高,因?yàn)楂@取鎖的競(jìng)爭(zhēng)意味著會(huì)發(fā)生更多的上下文切換。上下文切換的次數(shù)越多,吞吐量就越低。
- 分離日志操作
通過(guò)將 I/O 操作從處理請(qǐng)求的線程中分離出來(lái),可以縮短處理請(qǐng)求的平均服務(wù)時(shí)間。在調(diào)用 log 方法時(shí)將不會(huì)因?yàn)榈却敵隽鞯逆i或者 I/O 完成而被阻塞,只需要將相關(guān)操作放入消息隊(duì)列中進(jìn)入處理,然后返回各自原來(lái)的任務(wù)即可。雖然在消息隊(duì)列中可能發(fā)生競(jìng)爭(zhēng),但該操作相對(duì)于記錄日志的 I/O 操作來(lái)說(shuō)是一種更輕量級(jí)的操作,因此在實(shí)際使用過(guò)程中只要隊(duì)列沒(méi)有被填滿,則發(fā)生阻塞的概率很小。通過(guò)把 I/O 操作從處理請(qǐng)求的線程轉(zhuǎn)移到一個(gè)專門的線程,不僅使得處理請(qǐng)求的線程被阻塞的概率降低,還消除了輸出流上的競(jìng)爭(zhēng),將會(huì)提升整體的吞吐量。因?yàn)樵摬僮魇沟镁€程在調(diào)度中消耗的資源更少,上下文切換的次數(shù)也更少,而且對(duì)于鎖的管理也更為簡(jiǎn)單了。
3.設(shè)置線程的最佳數(shù)量
提升性能意味著使用更少的資源做更多的事情,“資源”的定義和廣泛,對(duì)于一個(gè)給定的操作,通常會(huì)缺乏某種特定的資源,如 CPU 時(shí)間片、內(nèi)存、網(wǎng)絡(luò)帶款、I/O 帶寬、數(shù)據(jù)庫(kù)請(qǐng)求以及磁盤(pán)空間等。當(dāng)操作性能由于某種特定的資源而收到限制時(shí),我們通常將該操作稱為資源密集型操作,例如 CPU 密集型、I/O 密集型、數(shù)據(jù)庫(kù)密集型。
根據(jù)多線程具體的應(yīng)用場(chǎng)景來(lái)選擇合適的線程數(shù)量。我們的程序一般都是 CPU 計(jì)算和 I/O 操作交叉執(zhí)行的,因?yàn)?I/O 設(shè)備的速度比起 CPU 來(lái)說(shuō)是很慢的,所以大部分情況下,I/O 操作執(zhí)行的時(shí)間相對(duì)于 CPU 計(jì)算時(shí)間來(lái)說(shuō)都是漫長(zhǎng)的,這種場(chǎng)景一般被稱為 I/O 密集型,和 I/O 密集型相對(duì)的就是 CPU 密集型了,CPU 密集型大部分情況下都是純 CPU 計(jì)算。對(duì)于 I/O 密集型和 CPU 密集型的程序,計(jì)算最佳線程的方法是不一樣的。
3.1 CPU 密集型
對(duì)于 CPU 密集型,多線程的本質(zhì)是提升多核 CPU ?的利用率,所以對(duì)于一個(gè) 2 核的 CPU 來(lái)說(shuō),一般是每個(gè)核一個(gè)線程,理論上只需要?jiǎng)?chuàng)建 2 個(gè)線程就可以發(fā)揮 2 核 CPU 的最大處理能力了,再創(chuàng)建線程也只是增加線程切換的成本。因此,對(duì)于 CPU 密集型的場(chǎng)景,理論上“線程的數(shù)量=CPU 核數(shù)”是最合適的,不過(guò)在項(xiàng)目中,線程的數(shù)量一般會(huì)被設(shè)置為 CPU 核數(shù) + 1,這樣的話,當(dāng)線程因?yàn)榕紶柕膬?nèi)存頁(yè)失效或其他原因?qū)е伦枞麜r(shí),第三條線程可以頂上,保證 CPU 的利用率。
3.2 I/O 密集型
對(duì)于 I/O 密集型的場(chǎng)景,假設(shè) CPU 只有一個(gè)核,如果 CPU 計(jì)算和 I/O 操作的耗時(shí)是 1:1,那么 2 個(gè)線程是最合適的。如果 CPU 計(jì)算和 I/O 操作的耗時(shí)是 1:2,那么 3 個(gè)線程是最合適的,CPU 在 A、B、C 三個(gè)線程之間切換,每次執(zhí)行保證有一條線程正在執(zhí)行 CPU 計(jì)算,其他兩條在執(zhí)行 I/O 操作。
通過(guò)以上例子,可以發(fā)現(xiàn),對(duì)于 I/O 密集型的計(jì)算場(chǎng)景,最佳的線程數(shù)量是與程序中的 CPU 計(jì)算和 I/O 操作的耗時(shí)比相關(guān)的,可以總結(jié)得出下列這個(gè)公式:
最佳線程數(shù) = 1 + (I/O 耗時(shí) / CPU 耗時(shí))
針對(duì)單核 CPU,令 R = I/O 耗時(shí) / CPU 耗時(shí),我們可以這樣理解:當(dāng)一個(gè)線程在執(zhí)行 CPU 操作時(shí),另外的 R 個(gè)線程都正好在執(zhí)行各自的 I/O 操作部分。因?yàn)閱魏?CPU ?的話每一時(shí)刻只能有一條線程在執(zhí)行 CPU 計(jì)算,其余線程都在執(zhí)行 I/O 計(jì)算,這些其余線程的數(shù)量為 I/O 耗時(shí) / CPU 耗時(shí),也就 R,所以最佳線程數(shù)就是 1 + R。
針對(duì)多核 CPU,只需要等比擴(kuò)大就行了,計(jì)算公式如下:
最佳線程數(shù) = CPU 核心數(shù) * [1 + (I/O 耗時(shí) / CPU 耗時(shí))]
3.3 理論 pk 經(jīng)驗(yàn)
設(shè)置合適的線程數(shù)量目的就是為了將硬件的性能發(fā)揮到極致,其實(shí)最佳線程數(shù)最終還是要通過(guò)壓測(cè)來(lái)確定的。實(shí)際工作中面臨的系統(tǒng),“I/O 耗時(shí) / CPU 耗時(shí)”往往都大于 1,所以基本都是在這個(gè)初始值的基礎(chǔ)上增加。在增加的過(guò)程中,應(yīng)關(guān)注線程數(shù)是如何影響吞吐量和延遲的。一般來(lái)說(shuō),隨著線程數(shù)量的增加,吞吐量會(huì)增加,延遲也會(huì)緩慢增加;但是當(dāng)線程增加到一定程度時(shí),吞吐量就會(huì)開(kāi)始下降,延遲會(huì)迅速增加,此時(shí)基本上就是線程能夠設(shè)置的最大值了。
實(shí)際工作中,不同 I/O 模型對(duì)最佳線程數(shù)的影響也非常大,如 Nginx 用的是非阻塞 I/O 模型,采用的是多進(jìn)程單線程結(jié)構(gòu),Nginx 本來(lái)是一個(gè) I/O 密集型系統(tǒng),但是最佳線程數(shù)量設(shè)置的卻是 CPU 核心數(shù),完全參考的是 CPU 密集型的做法,因此,對(duì)于理論的最佳線程數(shù)設(shè)置,還是得根據(jù)實(shí)際情況來(lái)。
四、總結(jié)一味的使用多線程并不一定能提高程序的性能,相反,由于使用多線程引入的額外性能開(kāi)銷甚至可能會(huì)遠(yuǎn)超過(guò)由于提高吞吐量、響應(yīng)性或者計(jì)算能力所帶來(lái)的性能提升。因此,遇到具體問(wèn)題,還是得具體分析,根據(jù)特定場(chǎng)景選擇的合適數(shù)據(jù)結(jié)構(gòu)和算法、合適的線程數(shù)量等操作來(lái)提高程序的性能。
參考資料
《Java 并發(fā)編程實(shí)戰(zhàn)》
《Java 多線程編程實(shí)戰(zhàn)指南 核心篇》
極客時(shí)間《Java 并發(fā)編程實(shí)戰(zhàn)》
往期精選
長(zhǎng)按掃碼關(guān)注公眾號(hào)
