激情久久久_欧美视频区_成人av免费_不卡视频一二三区_欧美精品在欧美一区二区少妇_欧美一区二区三区的

服務(wù)器之家:專注于服務(wù)器技術(shù)及軟件下載分享
分類導(dǎo)航

PHP教程|ASP.NET教程|JAVA教程|ASP教程|

服務(wù)器之家 - 編程語(yǔ)言 - JAVA教程 - Java并發(fā)編程之性能、擴(kuò)展性和響應(yīng)

Java并發(fā)編程之性能、擴(kuò)展性和響應(yīng)

2020-04-01 13:54lijiao JAVA教程

這篇文章主要介紹了Java并發(fā)編程之性能、擴(kuò)展性和響應(yīng),重點(diǎn)在于多線程應(yīng)用程序的性能問(wèn)題,給性能和擴(kuò)展性下一個(gè)定義,然后再仔細(xì)學(xué)習(xí)一下Amdahl法則,感興趣的小伙伴們可以參考一下

本文討論的重點(diǎn)在于多線程應(yīng)用程序的性能問(wèn)題。我們會(huì)先給性能和擴(kuò)展性下一個(gè)定義,然后再仔細(xì)學(xué)習(xí)一下Amdahl法則。下面的內(nèi)容我們會(huì)考察一下如何用不同的技術(shù)方法來(lái)減少鎖競(jìng)爭(zhēng),以及如何用代碼來(lái)實(shí)現(xiàn)。

1、性能

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

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

上面這兩個(gè)例子告訴我們,高性能就是在短的時(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)行渲染(按鈕看起來(lái)被按下了,當(dāng)松開(kāi)鼠標(biāo)左鍵時(shí)又恢復(fù)原狀),并且開(kāi)始對(duì)用戶的輸入進(jìn)行處理。如果處理用戶輸入的這個(gè)任務(wù)比較耗時(shí)的話,單線程的程序就無(wú)法繼續(xù)響應(yīng)用戶其他的輸入動(dòng)作了,比如,來(lái)自操作系統(tǒng)傳送過(guò)來(lái)的用戶單擊鼠標(biāo)事件或鼠標(biāo)指針移動(dòng)事件等等,這些事件的響應(yīng)需要有獨(dú)立的線程來(lái)響應(yīng)。

可擴(kuò)展性(Scalability)的意思是程序具備這樣的能力:通過(guò)添加計(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.1 Amdahl法則

上一段提到了在某些情形下,添加額外的運(yùn)算資源可以提高程序的整體性能。為了能夠計(jì)算出當(dāng)我們添加了額外的資源的時(shí)候到底能獲得多少性能提升,我們有必要來(lái)檢查一下程序有哪些部分是串行運(yùn)行(或同步運(yùn)行),有哪些部分是并行運(yùn)行的。如果我們把需要同步執(zhí)行的代碼占比量化為B(例如,需要同步執(zhí)行的代碼的行數(shù)),把CPU的總核心數(shù)記為n,那么,根據(jù)Amdahl法則,我們可以獲得的性能提升的上限是:

Java并發(fā)編程之性能、擴(kuò)展性和響應(yīng)

如果n趨于無(wú)窮大的話,(1-B)/n就收斂于0。因此,我們可以忽略這個(gè)表達(dá)式的值,因此性能提升位數(shù)收斂于1/B,這里面的B代表是那些必須同步運(yùn)行的代碼比例。如果B等于0.5的話,那意味著程序的一半代碼無(wú)法并行運(yùn)行,0.5的倒數(shù)是2,因此,即使我們添加無(wú)數(shù)個(gè)CPU核心,我們獲得的性能提升也最多是2倍。假設(shè)我們現(xiàn)在把程序修改了一下,修改之后只有0.25的代碼必須同步運(yùn)行,現(xiàn)在1/0.25=4,表示我們的程序如果在具有大量CPU的硬件上運(yùn)行時(shí)速度將會(huì)比在單核的硬件上快大概4倍。

另一方面,通過(guò)Amdahl法則,我們也能根據(jù)我們想獲得的提速的目標(biāo)計(jì)算出程序應(yīng)該的同步代碼的比例。如果我們想要達(dá)到100倍的提速,而1/100=0.01,意味著,我們程序同步執(zhí)行的代碼的數(shù)量最多不能超過(guò)1%。

總結(jié)Amdahl法則我們可以看出,我們通過(guò)添加額外CPU來(lái)獲得性能提升的最大值取決于程序同步執(zhí)行部分代碼所占的比例有多小。雖然在實(shí)際中,想要計(jì)算出這個(gè)比例并不總是那么容易,更別說(shuō)面對(duì)一些大型的商業(yè)系統(tǒng)應(yīng)用了,但是Amdahl法則給了我們很重要的啟示,那就是,我們必須非常仔細(xì)地去考慮那些必須同步執(zhí)行的代碼,并且力圖減少這部分代碼。

1.2 對(duì)性能的影響

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

首先,第一個(gè)影響來(lái)自線程創(chuàng)建的時(shí)候。線程的創(chuàng)建過(guò)程中,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)來(lái)重新恢復(fù)它的運(yùn)行。當(dāng)你的線程數(shù)量超過(guò)CPU的核心數(shù)量的時(shí)候,情況有可能變得更壞。在這種情況下,JVM的進(jìn)程調(diào)度器會(huì)打斷某些線程以便讓其他線程執(zhí)行,線程切換的時(shí)候,剛才正在運(yùn)行的線程的當(dāng)前狀態(tài)需要被保存下來(lá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ì)算資源,因此帶來(lái)相比單線程情況下沒(méi)有的性能開(kāi)銷。

多線程程序所帶來(lái)的另外一個(gè)開(kāi)銷來(lái)自對(duì)共享數(shù)據(jù)的同步訪問(wèn)保護(hù)。我們可以使用synchronized關(guān)鍵字來(lái)進(jìn)行同步保護(hù),也可以使用Volatile關(guān)鍵字來(lái)在多個(gè)線程之間共享數(shù)據(jù)。如果多于一個(gè)線程想要去訪問(wèn)某一個(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可以自己決定如何來(lái)執(zhí)行這種“等待”,假如JVM預(yù)計(jì)離成功獲得鎖對(duì)象的時(shí)間比較短,那JVM可以使用激進(jìn)等待方法,比如,不停地嘗試獲得鎖對(duì)象,直到成功,在這種情況下這種方式可能會(huì)更高效,因?yàn)楸容^進(jìn)程上下文切換來(lái)說(shuō),還是這種方式更快速一些。把一個(gè)等待狀態(tài)的線程挪回到執(zhí)行隊(duì)列也會(huì)帶來(lái)額外的開(kāi)銷。

因此,我們要盡力避免由于鎖競(jìng)爭(zhēng)而帶來(lái)的上下文切換。下面一節(jié)將闡述兩種降低這種競(jìng)爭(zhēng)發(fā)生的方法。

1.3 鎖競(jìng)爭(zhēng)

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

1、減少鎖的作用域;

2、減少需要獲取鎖的頻率;

3、盡量使用由硬件支持的樂(lè)觀鎖操作,而不是synchronized;

4、盡量少用synchronized;

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

1.3.1 縮減同步域

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

為了更好地理解,看下面的源碼:

 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class ReduceLockDuration implements Runnable {
 private static final int NUMBER_OF_THREADS = 5;
 private static final Map<String, Integer> map = new HashMap<String, Integer>();
 
 public void run() {
 for (int i = 0; i < 10000; i++) {
  synchronized (map) {
  UUID randomUUID = UUID.randomUUID();
  Integer value = Integer.valueOf(42);
  String key = randomUUID.toString();
  map.put(key, value);
  }
  Thread.yield();
 }
 }
 
 public static void main(String[] args) throws InterruptedException {
 Thread[] threads = new Thread[NUMBER_OF_THREADS];
 for (int i = 0; i < NUMBER_OF_THREADS; i++) {
  threads[i] = new Thread(new ReduceLockDuration());
 }
 long startMillis = System.currentTimeMillis();
 for (int i = 0; i < NUMBER_OF_THREADS; i++) {
  threads[i].start();
 }
 for (int i = 0; i < NUMBER_OF_THREADS; i++) {
  threads[i].join();
 }
 System.out.println((System.currentTimeMillis()-startMillis)+"ms");
 }
}

在上面的例子中,我們讓五個(gè)線程來(lái)競(jìng)爭(zhēng)訪問(wèn)共享的Map實(shí)例,為了在同一時(shí)刻只有一個(gè)線程可以訪問(wèn)到Map實(shí)例,我們將向Map中添加Key/Value的操作放到了synchronized保護(hù)的代碼塊中。當(dāng)我們仔細(xì)察看這段代碼的時(shí)候,我們可以看到,計(jì)算key和value的幾句代碼并不需要同步執(zhí)行,key和value只屬于當(dāng)前執(zhí)行這段代碼的線程,僅僅對(duì)當(dāng)前線程有意義,并且不會(huì)被其他線程所修改。因此,我們可以把這幾句移出同步保護(hù)。如下:

?
1
2
3
4
5
6
7
8
9
10
11
public void run() {
 for (int i = 0; i < 10000; i++) {
 UUID randomUUID = UUID.randomUUID();
 Integer value = Integer.valueOf(42);
 String key = randomUUID.toString();
 synchronized (map) {
  map.put(key, value);
 }
 Thread.yield();
 }
}

降低同步代碼所帶來(lái)的效果是可以測(cè)量的。在我的機(jī)器上,整個(gè)程序的執(zhí)行時(shí)間從420ms降低到了370ms。看看吧,僅僅把三行代碼移出同步保護(hù)塊就可以將程序運(yùn)行時(shí)間減少11%。Thread.yield()這句代碼是為了誘發(fā)線程上下文切換的,因?yàn)檫@句代碼會(huì)告訴JVM當(dāng)前線程想要交出當(dāng)前使用的計(jì)算資源,以便讓其他等待運(yùn)行的線程運(yùn)行。這樣也會(huì)帶來(lái)更多的鎖競(jìng)爭(zhēng)的發(fā)生,因?yàn)椋绻蝗绱说脑捘骋粋€(gè)線程就會(huì)更久地占用某個(gè)核心繼而減少了線程上下文切換。

1.3.2 分拆鎖

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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static class CounterOneLock implements Counter {
 private long customerCount = 0;
 private long shippingCount = 0;
 
 public synchronized void incrementCustomer() {
 customerCount++;
 }
 
 public synchronized void incrementShipping() {
 shippingCount++;
 }
 
 public synchronized long getCustomerCount() {
 return customerCount;
 }
 
 public synchronized long getShippingCount() {
 return shippingCount;
 }
}

這種方式也就意味著,對(duì)這些變量的每次修改都會(huì)引發(fā)對(duì)其他Counter實(shí)例的鎖定。其他線程如果想要對(duì)另外一個(gè)不同的變量調(diào)用increment方法,那也只能等待前一個(gè)線程釋放了鎖控制之后才能有機(jī)會(huì)去完成。在此種情況下,對(duì)每個(gè)不同的變量使用單獨(dú)的synchronized保護(hù)將會(huì)提高執(zhí)行效率。

 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public static class CounterSeparateLock implements Counter {
 private static final Object customerLock = new Object();
 private static final Object shippingLock = new Object();
 private long customerCount = 0;
 private long shippingCount = 0;
 
 public void incrementCustomer() {
 synchronized (customerLock) {
  customerCount++;
 }
 }
 
 public void incrementShipping() {
 synchronized (shippingLock) {
  shippingCount++;
 }
 }
 
 public long getCustomerCount() {
 synchronized (customerLock) {
  return customerCount;
 }
 }
 
 public long getShippingCount() {
 synchronized (shippingLock) {
  return shippingCount;
 }
 }
}

這種實(shí)現(xiàn)為每個(gè)計(jì)數(shù)指標(biāo)引入了一個(gè)單獨(dú)synchronized對(duì)象,因此,一個(gè)線程想要增加Customer計(jì)數(shù)的時(shí)候,它必須等待另一個(gè)正在增加Customer計(jì)數(shù)的線程完成,而并不用等待另一個(gè)正在增加Shipping計(jì)數(shù)的線程完成。

使用下面的類,我們可以非常容易地計(jì)算分拆鎖所帶來(lái)的性能提升。

 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class LockSplitting implements Runnable {
 private static final int NUMBER_OF_THREADS = 5;
 private Counter counter;
 
 public interface Counter {
 void incrementCustomer();
 
 void incrementShipping();
 
 long getCustomerCount();
 
 long getShippingCount();
 }
 
 public static class CounterOneLock implements Counter { ... }
 
 public static class CounterSeparateLock implements Counter { ... }
 
 public LockSplitting(Counter counter) {
 this.counter = counter;
 }
 
 public void run() {
 for (int i = 0; i < 100000; i++) {
  if (ThreadLocalRandom.current().nextBoolean()) {
  counter.incrementCustomer();
  } else {
  counter.incrementShipping();
  }
 }
 }
 
 public static void main(String[] args) throws InterruptedException {
 Thread[] threads = new Thread[NUMBER_OF_THREADS];
 Counter counter = new CounterOneLock();
 for (int i = 0; i < NUMBER_OF_THREADS; i++) {
  threads[i] = new Thread(new LockSplitting(counter));
 }
 long startMillis = System.currentTimeMillis();
 for (int i = 0; i < NUMBER_OF_THREADS; i++) {
  threads[i].start();
 }
 for (int i = 0; i < NUMBER_OF_THREADS; i++) {
  threads[i].join();
 }
 System.out.println((System.currentTimeMillis() - startMillis) + "ms");
 }
}

在我的機(jī)器上,單一鎖的實(shí)現(xiàn)方法平均花費(fèi)56ms,兩個(gè)單獨(dú)鎖的實(shí)現(xiàn)是38ms。耗時(shí)大約降低了大概32%。

另外一種提升方式是,我們甚至可以更進(jìn)一步地將讀寫分開(kāi)用不同的鎖來(lái)保護(hù)。原來(lái)的Counter類提供了對(duì)計(jì)數(shù)指標(biāo)分別提供了讀和寫的方法,但是事實(shí)上,讀操作并不需要同步保護(hù),我們可以放心讓多個(gè)線程并行讀取當(dāng)前指標(biāo)的數(shù)值,同時(shí),寫操作必須得到同步保護(hù)。java.util.concurrent包里提供了有對(duì)ReadWriteLock接口的實(shí)現(xiàn),可以方便地實(shí)現(xiàn)這種區(qū)分。

ReentrantReadWriteLock實(shí)現(xiàn)維護(hù)了兩個(gè)不同的鎖,一個(gè)保護(hù)讀操作,一個(gè)保護(hù)寫操作。這兩個(gè)鎖都有獲取鎖和釋放鎖的操作。僅僅當(dāng)在沒(méi)有人獲取讀鎖的時(shí)候,寫鎖才能成功獲得。反過(guò)來(lái),只要寫鎖沒(méi)有被獲取,讀鎖可以被多個(gè)線程同時(shí)獲取。為了演示這種方法,下面的Counter類使用了ReadWriteLock,如下:

 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public static class CounterReadWriteLock implements Counter {
 private final ReentrantReadWriteLock customerLock = new ReentrantReadWriteLock();
 private final Lock customerWriteLock = customerLock.writeLock();
 private final Lock customerReadLock = customerLock.readLock();
 private final ReentrantReadWriteLock shippingLock = new ReentrantReadWriteLock();
 private final Lock shippingWriteLock = shippingLock.writeLock();
 private final Lock shippingReadLock = shippingLock.readLock();
 private long customerCount = 0;
 private long shippingCount = 0;
 
 public void incrementCustomer() {
 customerWriteLock.lock();
 customerCount++;
 customerWriteLock.unlock();
 }
 
 public void incrementShipping() {
 shippingWriteLock.lock();
 shippingCount++;
 shippingWriteLock.unlock();
 }
 
 public long getCustomerCount() {
 customerReadLock.lock();
 long count = customerCount;
 customerReadLock.unlock();
 return count;
 }
 
 public long getShippingCount() {
 shippingReadLock.lock();
 long count = shippingCount;
 shippingReadLock.unlock();
 return count;
 }
}

所有的讀操作都被讀鎖保護(hù),同時(shí),所有的寫操作都被寫鎖所保護(hù)。如果程序中執(zhí)行的讀操作要遠(yuǎn)大于寫操作的話,這種實(shí)現(xiàn)可以帶來(lái)比前一節(jié)的方式更大的性能提升,因?yàn)樽x操作可以并發(fā)進(jìn)行。

1.3.3 分離鎖

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

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

1.3.4 原子操作

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

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

下面的Counter類的實(shí)現(xiàn)就利用了原子操作的方式,你可以看到,并沒(méi)有使用任何synchronized的代碼。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static class CounterAtomic implements Counter {
 private AtomicLong customerCount = new AtomicLong();
 private AtomicLong shippingCount = new AtomicLong();
 
 public void incrementCustomer() {
 customerCount.incrementAndGet();
 }
 
 public void incrementShipping() {
 shippingCount.incrementAndGet();
 }
 
 public long getCustomerCount() {
 return customerCount.get();
 }
 
 public long getShippingCount() {
 return shippingCount.get();
 }
}

與CounterSeparateLock類相比,平均運(yùn)行時(shí)間從39ms降低到了16ms,大約降低了58%。

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

一個(gè)典型的LIST實(shí)現(xiàn)通過(guò)會(huì)在內(nèi)容維護(hù)一個(gè)變量來(lái)記錄LIST自身所包含的元素個(gè)數(shù),每一次從列表里刪除或增加元素的時(shí)候,這個(gè)變量的值都會(huì)改變。如果LIST在單線程應(yīng)用中使用的話,這種方式無(wú)可厚非,每次調(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)化方式,當(dāng)?shù)搅硕嗑€程環(huán)境下時(shí)卻會(huì)成為一個(gè)問(wèn)題。假設(shè)我們?cè)诙鄠€(gè)線程之間共享一個(gè)LIST,多個(gè)線程同時(shí)地去向LIST里面增加或刪除元素,同時(shí)去查詢大的長(zhǎng)度。這時(shí),LIST內(nèi)部的計(jì)數(shù)變量成為一個(gè)共享資源,因此所有對(duì)它的訪問(wèn)都必須進(jìn)行同步處理。因此,計(jì)數(shù)變量成為整個(gè)LIST實(shí)現(xiàn)中的一個(gè)熱點(diǎn)。

下面的代碼片段展示了這個(gè)問(wèn)題:

 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public static class CarRepositoryWithCounter implements CarRepository {
 private Map<String, Car> cars = new HashMap<String, Car>();
 private Map<String, Car> trucks = new HashMap<String, Car>();
 private Object carCountSync = new Object();
 private int carCount = 0;
 
 public void addCar(Car car) {
 if (car.getLicencePlate().startsWith("C")) {
  synchronized (cars) {
  Car foundCar = cars.get(car.getLicencePlate());
  if (foundCar == null) {
   cars.put(car.getLicencePlate(), car);
   synchronized (carCountSync) {
   carCount++;
   }
  }
  }
 } else {
  synchronized (trucks) {
  Car foundCar = trucks.get(car.getLicencePlate());
  if (foundCar == null) {
   trucks.put(car.getLicencePlate(), car);
   synchronized (carCountSync) {
   carCount++;
   }
  }
  }
 }
 }
 
 public int getCarCount() {
 synchronized (carCountSync) {
  return carCount;
 }
 }
}

上面這個(gè)CarRepository的實(shí)現(xiàn)內(nèi)部有兩個(gè)LIST變量,一個(gè)用來(lái)放洗車元素,一個(gè)用來(lái)放卡車元素,同時(shí),提供了查詢這兩個(gè)LIST總共的大小的方法。采用的優(yōu)化方式是,每次添加一個(gè)Car元素的時(shí)候,都會(huì)增加內(nèi)部的計(jì)數(shù)變量的值,同時(shí)增加的操作受synchronized保護(hù),返回計(jì)數(shù)值的方法也是一樣。

為了避免帶來(lái)這種額外的代碼同步開(kāi)銷,看下面另外一種CarRepository的實(shí)現(xiàn):它不再使用一個(gè)內(nèi)部的計(jì)數(shù)變量,而是在返回汽車總數(shù)的方法里實(shí)時(shí)計(jì)數(shù)這個(gè)數(shù)值。如下:

 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public static class CarRepositoryWithoutCounter implements CarRepository {
 private Map<String, Car> cars = new HashMap<String, Car>();
 private Map<String, Car> trucks = new HashMap<String, Car>();
 
 public void addCar(Car car) {
 if (car.getLicencePlate().startsWith("C")) {
  synchronized (cars) {
  Car foundCar = cars.get(car.getLicencePlate());
  if (foundCar == null) {
   cars.put(car.getLicencePlate(), car);
  }
  }
 } else {
  synchronized (trucks) {
  Car foundCar = trucks.get(car.getLicencePlate());
  if (foundCar == null) {
   trucks.put(car.getLicencePlate(), car);
  }
  }
 }
 }
 
 public int getCarCount() {
 synchronized (cars) {
  synchronized (trucks) {
  return cars.size() + trucks.size();
  }
 }
 }
}

現(xiàn)在,僅僅在getCarCount()方法里,兩個(gè)LIST的訪問(wèn)需要同步保護(hù),像上一種實(shí)現(xiàn)那樣每次添加新元素時(shí)的同步開(kāi)銷已經(jīng)不存在了。

1.3.6 避免對(duì)象緩存復(fù)用

在JAVA VM的第一版里,使用new關(guān)鍵字來(lái)創(chuàng)建新對(duì)象的開(kāi)銷比較大,因此,很多開(kāi)發(fā)人員習(xí)慣了使用對(duì)象復(fù)用模式。為了避免一次又一次重復(fù)創(chuàng)建對(duì)象,開(kāi)發(fā)人員維護(hù)一個(gè)緩沖池,每次創(chuàng)建完對(duì)象實(shí)例之后可以把它們保存在緩沖池里,下次其他線程再需要使用的時(shí)候就可以直接從緩沖池里去取。

乍一看,這種方式是很合理的,但是這種模式在多線程應(yīng)用程序里會(huì)出現(xiàn)問(wèn)題。因?yàn)閷?duì)象的緩沖池在多個(gè)線程之間共享,因此所有線程在訪問(wèn)其中的對(duì)象時(shí)的操作需要同步保護(hù)。而這種同步所帶來(lái)的開(kāi)銷已經(jīng)大過(guò)了創(chuàng)建對(duì)象本身了。當(dāng)然了,創(chuàng)建過(guò)多的對(duì)象會(huì)加重垃圾回收的負(fù)擔(dān),但是即便把這個(gè)考慮在內(nèi),避免同步代碼所帶來(lái)的性能提升仍然要好過(guò)使用對(duì)象緩存池的方式。

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

延伸 · 閱讀

精彩推薦
主站蜘蛛池模板: 国产午夜精品久久久久久久蜜臀 | 日韩毛片毛片久久精品 | 国产99久久精品一区二区 | 91精品国产91久久久 | 免费嗨片首页中文字幕 | 激情五月少妇a | 亚洲va国产va | 中文字幕在线播放不卡 | 国产精品久久久久久模特 | 精品久久久久久久久久中出 | 有兽焉免费动画 | 成人一级免费视频 | 国产午夜精品视频免费不卡69堂 | 国产亚洲精品久久久久婷婷瑜伽 | 免看黄大片aa | 在线观看国产一区二区 | 色人阁导航 | 91成人久久| 一级网站片 | 日本精品中文字幕 | 一区二区久久精品66国产精品 | 九九久久视频 | 91极品视频在线观看 | 久久精品视频6 | 夜夜夜精品视频 | 免费一级毛片在线播放不收费 | 久久精品女人天堂av | 成人在线视频一区 | 99热99精品 | av在线视 | 亚洲午夜免费 | 黄视频在线网站 | 国产精品成人一区二区三区电影毛片 | 日韩视频一区在线 | 国产电影精品 | 国产精品视频二区不卡 | 毛片a片免费看 | 国产成人高清成人av片在线看 | 久久av一区二区 | 免费黄色入口 | 爽成人777777婷婷 |