溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Java并發(fā)編程性能詳解

發(fā)布時(shí)間:2020-07-31 20:13:03 來源:網(wǎng)絡(luò) 閱讀:218 作者:sxt程序猿 欄目:編程語言

一、介紹

本文重點(diǎn)討論多線程應(yīng)用程序的性能問題。如用何種技術(shù)方法來減少鎖競(jìng)爭(zhēng),以及如何用代碼來實(shí)現(xiàn)。

二、性能

我們都知道,多線程可以提高線程的性能。性能提升的根本原因在于我們有多核的CPU或多個(gè)CPU。每個(gè)CPU的內(nèi)核都可以自己完成任務(wù),因此把一個(gè)大的任務(wù)分解成一系列的可彼此獨(dú)立運(yùn)行的小任務(wù)就可以提高程序的整體性能了??梢耘e個(gè)例子,比如有個(gè)程序用來將硬盤上某個(gè)文件夾下的所有圖片的尺寸進(jìn)行修改,應(yīng)用多線程技術(shù)就可以提高它的性能。使用單線程的方式只能依次遍歷所有圖片文件并且執(zhí)行修改,如果我們的CPU有多個(gè)核心的話,毫無疑問,它只能利用其中的一個(gè)核。使用多線程的方式的話,我們可以讓一個(gè)生產(chǎn)者線程掃描文件系統(tǒng)把每個(gè)圖片都添加到一個(gè)隊(duì)列中,然后用多個(gè)工作線程來執(zhí)行這些任務(wù)。如果我們的工作線程的數(shù)量和CPU總的核心數(shù)一樣的話,我們就能保證每個(gè)CPU核心都有活可干,直到任務(wù)被全部執(zhí)行完成。

對(duì)于另外一種需要較多IO等待的程序來說,利用多線程技術(shù)也能提高整體性能。假設(shè)我們要寫這樣一個(gè)程序,需要抓取某個(gè)網(wǎng)站的所有HTML文件,并且將它們存儲(chǔ)到本地磁盤上。程序可以從某一個(gè)網(wǎng)頁開始,然后解析這個(gè)網(wǎng)頁中所有指向本網(wǎng)站的鏈接,然后依次抓取這些鏈接,這樣周而復(fù)始。因?yàn)閺奈覀儗?duì)遠(yuǎn)程網(wǎng)站發(fā)起請(qǐng)求到接收到所有的網(wǎng)頁數(shù)據(jù)需要等待一段時(shí)間,所以我們可以將此任務(wù)交給多個(gè)線程來執(zhí)行。讓一個(gè)或稍微更多一點(diǎn)的線程來解析已經(jīng)收到的HTML網(wǎng)頁以及將找到的鏈接放入隊(duì)列中,讓其他所有的線程負(fù)責(zé)請(qǐng)求獲取頁面。

高性能就是在短的時(shí)間窗口內(nèi)做盡量多的事情。這個(gè)當(dāng)然是對(duì)性能一詞的最經(jīng)典解釋了。但是同時(shí),使用線程也能很好地提升我們程序的響應(yīng)速度。想象我們有這樣一個(gè)圖形界面的應(yīng)用程序,上方有一個(gè)輸入框,輸入框下面有一個(gè)名字叫“處理”的按鈕。當(dāng)用戶按下這個(gè)按鈕的時(shí)候,應(yīng)用程序需要重新對(duì)按鈕的狀態(tài)進(jìn)行渲染(按鈕看起來被按下了,當(dāng)松開鼠標(biāo)左鍵時(shí)又恢復(fù)原狀),并且開始對(duì)用戶的輸入進(jìn)行處理。如果處理用戶輸入的這個(gè)任務(wù)比較耗時(shí)的話,單線程的程序就無法繼續(xù)響應(yīng)用戶其他的輸入動(dòng)作了,

可擴(kuò)展性(Scalability)的意思是程序具備這樣的能力:通過添加計(jì)算資源就可以獲得更高的性能。想象我們需要調(diào)整很多圖片的大小,因?yàn)槲覀儥C(jī)器的CPU核心數(shù)是有限的,所以增加線程數(shù)量并不總能相應(yīng)提高性能。相反,因?yàn)檎{(diào)度器需要負(fù)責(zé)更多線程的創(chuàng)建和關(guān)閉,也會(huì)占用CPU資源,反而有可能降低性能。

1、對(duì)性能的影響

寫到這里,我們已經(jīng)取得這樣一個(gè)觀點(diǎn):增加更多的線程可以提高程序的性能和響應(yīng)速度。但是另一方面,想要取得這些好處卻并非輕而易舉,也需要付出一些代價(jià)。線程的使用對(duì)性能的提升也會(huì)有所影響。

首先,第一個(gè)影響來自線程創(chuàng)建的時(shí)候。線程的創(chuàng)建過程中,JVM需要從底層操作系統(tǒng)申請(qǐng)相應(yīng)的資源,并且在調(diào)度器中初始化數(shù)據(jù)結(jié)構(gòu),以便決定執(zhí)行線程的順序。

如果你的線程的數(shù)量和CPU的核心數(shù)量一樣的話,每個(gè)線程都會(huì)運(yùn)行在一個(gè)核心上,這樣或許他們就不會(huì)經(jīng)常被打斷了。但是事實(shí)上,在你的程序運(yùn)行的時(shí)候,操作系統(tǒng)也會(huì)有些自己的運(yùn)算需要CPU去處理。所以,即使這種情形下,你的線程也會(huì)被打斷并且等待操作系統(tǒng)來重新恢復(fù)它的運(yùn)行。當(dāng)你的線程數(shù)量超過CPU的核心數(shù)量的時(shí)候,情況有可能變得更壞。在這種情況下,JVM的進(jìn)程調(diào)度器會(huì)打斷某些線程以便讓其他線程執(zhí)行,線程切換的時(shí)候,剛才正在運(yùn)行的線程的當(dāng)前狀態(tài)需要被保存下來,以便等下次運(yùn)行的時(shí)候可以恢復(fù)數(shù)據(jù)狀態(tài)。不僅如此,調(diào)度器也會(huì)對(duì)它自己內(nèi)部的數(shù)據(jù)結(jié)構(gòu)進(jìn)行更新,而這也需要消耗CPU周期。所有這些都意味著,線程之間的上下文切換會(huì)消耗CPU計(jì)算資源,因此帶來相比單線程情況下沒有的性能開銷。

多線程程序所帶來的另外一個(gè)開銷來自對(duì)共享數(shù)據(jù)的同步訪問保護(hù)。我們可以使用synchronized關(guān)鍵字來進(jìn)行同步保護(hù),也可以使用Volatile關(guān)鍵字來在多個(gè)線程之間共享數(shù)據(jù)。如果多于一個(gè)線程想要去訪問某一個(gè)共享數(shù)據(jù)結(jié)構(gòu)的話,就發(fā)生了爭(zhēng)用的情形,這時(shí),JVM需要決定哪個(gè)進(jìn)程先,哪個(gè)進(jìn)程后。如果決定該要執(zhí)行的線程不是當(dāng)前正在運(yùn)行的線程,那么就會(huì)發(fā)生線程切換。當(dāng)前線程需要等待,直到它成功獲得了鎖對(duì)象。JVM可以自己決定如何來執(zhí)行這種“等待”,假如JVM預(yù)計(jì)離成功獲得鎖對(duì)象的時(shí)間比較短,那JVM可以使用激進(jìn)等待方法,比如,不停地嘗試獲得鎖對(duì)象,直到成功,在這種情況下這種方式可能會(huì)更高效,因?yàn)楸容^進(jìn)程上下文切換來說,還是這種方式更快速一些。把一個(gè)等待狀態(tài)的線程挪回到執(zhí)行隊(duì)列也會(huì)帶來額外的開銷。

因此,我們要盡力避免由于鎖競(jìng)爭(zhēng)而帶來的上下文切換。

下面將具體闡述兩種降低這種競(jìng)爭(zhēng)發(fā)生的方法。

2、鎖競(jìng)爭(zhēng)

兩個(gè)或更多線程對(duì)鎖的競(jìng)爭(zhēng)訪問會(huì)帶來額外的運(yùn)算開銷,因?yàn)楦?jìng)爭(zhēng)的發(fā)生逼迫調(diào)度器來讓一個(gè)線程進(jìn)入激進(jìn)等待狀態(tài),或者讓它進(jìn)行等待狀態(tài)而引發(fā)兩次上下文切換。有某些情況下,鎖競(jìng)爭(zhēng)的惡果可以通過以下方法來減輕:

1.少鎖的作用域;

2.少需要獲取鎖的頻率;

3.量使用由硬件支持的樂觀鎖操作,而不是synchronized;

4.量少用synchronized;

5.少使用對(duì)象緩存

  

2.1 縮減同步域

  如果代碼持有鎖超過必要的時(shí)間,那么可以應(yīng)用這第一種方法。通常我們可以將一行或多行代碼移出同步區(qū)域來降低當(dāng)前線程持有鎖的時(shí)間。在同步區(qū)域里運(yùn)行的代碼數(shù)量越少,當(dāng)前線程就會(huì)越早地釋放鎖,從而讓其他線程更早地獲得鎖。這與Amdahl法則相一致的,因?yàn)檫@樣做減少了需要同步執(zhí)行的代碼量。

2.2 分拆鎖

另外一種減少鎖競(jìng)爭(zhēng)的方法是將一塊被鎖定保護(hù)的代碼分散到多個(gè)更小的保護(hù)塊中。如果你的程序中使用了一個(gè)鎖來保護(hù)多個(gè)不同對(duì)象的話,這種方式會(huì)有用武之地。假設(shè)我們想要通過程序來統(tǒng)計(jì)一些數(shù)據(jù),并且實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的計(jì)數(shù)類來持有多個(gè)不同的統(tǒng)計(jì)指標(biāo),并且分別用一個(gè)基本計(jì)數(shù)變量來表示(long類型)。因?yàn)槲覀兊某绦蚴嵌嗑€程的,所以我們需要對(duì)訪問這些變量的操作進(jìn)行同步保護(hù),因?yàn)檫@些操作動(dòng)作來自不同的線程。要達(dá)到這個(gè)目的,最簡(jiǎn)單的方式就是對(duì)每個(gè)訪問了這些變量的函數(shù)添加synchronized關(guān)鍵字。

2.3 分離鎖

上面一個(gè)例子展示了如何將一個(gè)單獨(dú)的鎖分開為多個(gè)單獨(dú)的鎖,這樣使得各線程僅僅獲得他們將要修改的對(duì)象的鎖就可以了。但是另一方面,這種方式也增加了程序的復(fù)雜度,如果實(shí)現(xiàn)不恰當(dāng)?shù)脑捯部赡茉斐伤梨i。

分離鎖是與分拆鎖類似的一種方法,但是分拆鎖是增加鎖來保護(hù)不同的代碼片段或?qū)ο?,而分離鎖是使用不同的鎖來保護(hù)不同范圍的數(shù)值。JDK的java.util.concurrent包里的ConcurrentHashMap即使用了這種思想來提高那些嚴(yán)重依賴HashMap的程序的性能。在實(shí)現(xiàn)上,ConcurrentHashMap內(nèi)部使用了16個(gè)不同的鎖,而不是封裝一個(gè)同步保護(hù)的HashMap。16個(gè)鎖每一個(gè)負(fù)責(zé)保護(hù)其中16分之一的桶位(bucket)的同步訪問。這樣一來,不同的線程想要向不同的段插入鍵的時(shí)候,相應(yīng)的操作會(huì)受到不同的鎖來保護(hù)。但是反過來也會(huì)帶來一些不好的問題,比如,某些操作的完成現(xiàn)在需要獲取多個(gè)鎖而不是一個(gè)鎖。如果你想要復(fù)制整個(gè)Map的話,這16個(gè)鎖都需要獲得才能完成。

2.4 原子操作

另外一種減少鎖競(jìng)爭(zhēng)的方法是使用原子操作。java.util.concurrent包對(duì)一些常用基礎(chǔ)數(shù)據(jù)類型提供了原子操作封裝的類。原子操作類的實(shí)現(xiàn)基于處理器提供的“比較置換”功能(CAS),CAS操作只在當(dāng)前寄存器的值跟操作提供的舊的值一樣的時(shí)候才會(huì)執(zhí)行更新操作。

這個(gè)原理可以用來以樂觀的方式來增加一個(gè)變量的值。如果我們的線程知道當(dāng)前的值的話,就會(huì)嘗試使用CAS操作來執(zhí)行增加操作。如果期間別的線程已經(jīng)修改了變量的值,那么線程提供的所謂的當(dāng)前值已經(jīng)跟真實(shí)的值不一樣了,這時(shí)JVM來嘗試重新獲得當(dāng)前值,并且再嘗試一次,反反復(fù)復(fù)直到成功為止。雖然循環(huán)操作會(huì)浪費(fèi)一些CPU周期,但是這樣做的好處是,我們不需要任何形式的同步控制。

2.5 避免熱點(diǎn)代碼段

一個(gè)典型的LIST實(shí)現(xiàn)通過會(huì)在內(nèi)容維護(hù)一個(gè)變量來記錄LIST自身所包含的元素個(gè)數(shù),每一次從列表里刪除或增加元素的時(shí)候,這個(gè)變量的值都會(huì)改變。如果LIST在單線程應(yīng)用中使用的話,這種方式無可厚非,每次調(diào)用size()時(shí)直接返回上一次計(jì)算之后的數(shù)值就行了。如果LIST內(nèi)部不維護(hù)這個(gè)計(jì)數(shù)變量的話,每次調(diào)用size()操作都會(huì)引發(fā)LIST重新遍歷計(jì)算元素個(gè)數(shù)。

這種很多數(shù)據(jù)結(jié)構(gòu)都使用了的優(yōu)化方式,到了多線程環(huán)境下時(shí)卻會(huì)成為一個(gè)問題。假設(shè)我們?cè)诙鄠€(gè)線程之間共享一個(gè)LIST,多個(gè)線程同時(shí)地去向LIST里面增加或刪除元素,同時(shí)去查詢大的長(zhǎng)度。這時(shí),LIST內(nèi)部的計(jì)數(shù)變量成為一個(gè)共享資源,因此所有對(duì)它的訪問都必須進(jìn)行同步處理。因此,計(jì)數(shù)變量成為整個(gè)LIST實(shí)現(xiàn)中的一個(gè)熱點(diǎn)。

本文所講述的這些優(yōu)化方案再一次的表明,每一種優(yōu)化方式在真正應(yīng)用的時(shí)候一定需要多多仔細(xì)觀測(cè)。不成熟的優(yōu)化方案表面看起來好像很有道理,但是事實(shí)上很有可能會(huì)反過來成為性能的瓶頸。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI