高性能 高可用 可弹性伸缩_性能,可伸缩性和活力
高性能 高可用 可彈性伸縮
本文是我們名為Java Concurrency Essentials的學(xué)院課程的一部分。
在本課程中,您將深入探討并發(fā)的魔力。 將向您介紹并發(fā)和并發(fā)代碼的基礎(chǔ)知識(shí),并學(xué)習(xí)諸如原子性,同步和線程安全性的概念。 在這里查看 !
目錄
1.簡(jiǎn)介 2.表現(xiàn)1.簡(jiǎn)介
本文討論了多線程應(yīng)用程序的性能主題。 在定義性能和可伸縮性這兩個(gè)術(shù)語(yǔ)之后,我們將仔細(xì)研究阿姆達(dá)爾定律。 在本課程的進(jìn)一步內(nèi)容中,我們將看到如何通過(guò)應(yīng)用不同的技術(shù)來(lái)減少鎖爭(zhēng)用,如代碼示例所示。
2.表現(xiàn)
線程可用于提高應(yīng)用程序的性能。 其背后的原因可能是我們有多個(gè)處理器或CPU內(nèi)核可用。 每個(gè)CPU內(nèi)核都可以執(zhí)行自己的任務(wù),因此將大任務(wù)劃分為一系列相互獨(dú)立運(yùn)行的較小任務(wù),可以改善應(yīng)用程序的總運(yùn)行時(shí)間。 這種性能改善的一個(gè)示例可以是調(diào)整硬盤上文件夾結(jié)構(gòu)中的圖像大小的應(yīng)用程序。 單線程方法將僅遍歷所有文件并逐個(gè)縮放每個(gè)圖像。 如果我們的CPU具有多個(gè)內(nèi)核,則調(diào)整大小過(guò)程將僅利用可用內(nèi)核之一。 例如,多線程方法可以讓生產(chǎn)者線程掃描文件系統(tǒng),并將所有找到的文件添加到隊(duì)列中,該隊(duì)列由一堆工作線程處理。 當(dāng)我們擁有與CPU內(nèi)核一樣多的工作線程時(shí),我們確保每個(gè)CPU內(nèi)核都有事情要做,直到處理完所有映像為止。
使用多線程可以提高應(yīng)用程序整體性能的另一個(gè)示例是具有大量I / O等待時(shí)間的用例。 假設(shè)我們要編寫一個(gè)應(yīng)用程序,以HTML文件的形式將完整的網(wǎng)站鏡像到我們的硬盤上。 從一頁(yè)開始,應(yīng)用程序必須遵循指向同一域(或URL部分)的所有鏈接。 從向遠(yuǎn)程Web服務(wù)器發(fā)出請(qǐng)求直到收到所有數(shù)據(jù)之間的時(shí)間可能很長(zhǎng),我們可以將工作分配到幾個(gè)線程上。 一個(gè)或多個(gè)線程可以解析收到HTML頁(yè)面并將找到的鏈接放入隊(duì)列,而其他線程可以將請(qǐng)求發(fā)送到Web服務(wù)器,然后等待答案。 在這種情況下,我們將等待時(shí)間用于新請(qǐng)求的頁(yè)面以及已接收頁(yè)面的解析。 與前面的示例相比,如果我們添加的線程數(shù)超過(guò)了CPU內(nèi)核的數(shù)量,則此應(yīng)用程序甚至可能會(huì)獲得性能。
這兩個(gè)例子表明,性能意味著可以在更短的時(shí)間內(nèi)完成更多的工作。 當(dāng)然,這是對(duì)性能一詞的經(jīng)典理解。 但是線程的使用也可以提高我們應(yīng)用程序的響應(yīng)速度。 想象一下簡(jiǎn)單的GUI應(yīng)用程序,它帶有一個(gè)輸入表單和一個(gè)“ Process”按鈕。 當(dāng)用戶按下按鈕時(shí),應(yīng)用程序必須呈現(xiàn)被按下的按鈕(按鈕應(yīng)像被按下并在釋放鼠標(biāo)時(shí)再次升起一樣鎖定),并且必須完成輸入數(shù)據(jù)的實(shí)際處理。 如果此處理需要更長(zhǎng)的時(shí)間,則單線程應(yīng)用程序?qū)o(wú)法對(duì)進(jìn)一步的用戶輸入做出React,即我們需要一個(gè)附加線程來(lái)處理來(lái)自操作系統(tǒng)的事件,例如鼠標(biāo)單擊或鼠標(biāo)指針移動(dòng)。
可伸縮性是指程序通過(guò)向其添加更多資源來(lái)提高性能的能力。 想象一下,我們將不得不調(diào)整大量圖像的大小。 由于我們當(dāng)前計(jì)算機(jī)的CPU內(nèi)核數(shù)量有限,因此添加更多線程并不能提高性能。 由于調(diào)度程序必須管理更多的線程,因此性能甚至可能下降,并且線程的創(chuàng)建和關(guān)閉也要消耗CPU功率。
阿姆達(dá)爾定律
最后一部分表明,在某些情況下,添加新資源可以提高應(yīng)用程序的整體性能。 為了能夠計(jì)算出當(dāng)我們添加更多資源時(shí)我們的應(yīng)用程序可以獲得多少性能,我們需要確定程序中必須串行化/同步運(yùn)行的部分以及程序中可以并行運(yùn)行的部分。 如果我們表示必須與B同步運(yùn)行的程序部分(例如,已同步執(zhí)行的行數(shù)),并且如果我們表示具有n的可用處理器數(shù),則阿姆達(dá)爾定律可讓我們計(jì)算加速的上限我們的應(yīng)用程序可能能夠?qū)崿F(xiàn):
圖1
如果我們讓n接近無(wú)窮大,則項(xiàng)(1-B)/ n收斂于零。 因此,我們可以忽略該術(shù)語(yǔ),并且提速的上限與1 / B收斂,其中B是優(yōu)化之前程序運(yùn)行時(shí)在不可并行代碼中花費(fèi)的分?jǐn)?shù)。 如果B例如為0.5,則意味著程序的一半不能并行化,則0.5的倒數(shù)為2;而B的倒數(shù)為2。 因此,即使我們?cè)趹?yīng)用程序中添加無(wú)限數(shù)量的處理器,我們也只能獲得大約2倍的加速。 現(xiàn)在,我們假設(shè)可以重寫代碼,以便僅0.25的程序運(yùn)行時(shí)花費(fèi)在同步塊中。 現(xiàn)在0.25的倒數(shù)是4,這意味著我們已經(jīng)構(gòu)建了一個(gè)可以在大量處理器上運(yùn)行的應(yīng)用程序,它的運(yùn)行速度比僅一個(gè)處理器快四倍。
反過(guò)來(lái)說(shuō),我們也可以使用阿姆達(dá)爾定律來(lái)計(jì)算必須同步執(zhí)行以達(dá)到給定加速比的程序運(yùn)行時(shí)間的分?jǐn)?shù)。 如果我們想實(shí)現(xiàn)約100的加速,則倒數(shù)是0.01,這意味著我們應(yīng)該只在同步代碼中花費(fèi)大約1%的運(yùn)行時(shí)。
總結(jié)一下阿姆達(dá)爾定律的發(fā)現(xiàn),我們可以得出結(jié)論,通過(guò)使用附加處理器可以使程序獲得的最大速度受到程序花費(fèi)在同步代碼部分中的時(shí)間的倒數(shù)的限制。 盡管在實(shí)踐中計(jì)算該分?jǐn)?shù)并不總是那么容易,即使您考慮大型商業(yè)應(yīng)用程序也不是一件容易的事,但法律給我們的提示是,我們必須非常仔細(xì)地考慮同步,并且必須保留程序運(yùn)行時(shí)的各個(gè)部分。小,必須序列化。
線程對(duì)性能的影響
到目前為止,本文的著作表明,向應(yīng)用程序添加更多線程可以提高性能和響應(yīng)能力。 但是,另一方面,這不是免費(fèi)的。 線程本身總是會(huì)對(duì)性能產(chǎn)生一些影響。
對(duì)性能的第一個(gè)影響是線程本身的創(chuàng)建。 這需要一些時(shí)間,因?yàn)镴VM必須從基礎(chǔ)操作系統(tǒng)中獲取線程的資源并準(zhǔn)備調(diào)度程序中的數(shù)據(jù)結(jié)構(gòu),該調(diào)度程序決定下一步執(zhí)行哪個(gè)線程。
如果使用與處理器內(nèi)核一樣多的線程,則每個(gè)線程都可以在自己的處理器上運(yùn)行,并且不會(huì)經(jīng)常被中斷。 實(shí)際上,在您的應(yīng)用程序運(yùn)行時(shí),操作系統(tǒng)當(dāng)然可能需要其自己的計(jì)算。 因此即使在這種情況下,線程也會(huì)中斷,并且必須等到操作系統(tǒng)讓它們?cè)俅芜\(yùn)行。 當(dāng)您必須使用比CPU內(nèi)核更多的線程時(shí),情況變得更糟。 在這種情況下,調(diào)度程序可以中斷您的線程,以便讓另一個(gè)線程執(zhí)行其代碼。 在這種情況下,必須保存正在運(yùn)行的線程的當(dāng)前狀態(tài),必須恢復(fù)應(yīng)該在下一次運(yùn)行的調(diào)度線程的狀態(tài)。 除此以外,調(diào)度程序本身還必須對(duì)其內(nèi)部數(shù)據(jù)結(jié)構(gòu)執(zhí)行一些更新,從而再次使用CPU功能。 總而言之,這意味著每個(gè)上下文從一個(gè)線程切換到另一個(gè)線程會(huì)消耗CPU能力,因此與單線程解決方案相比會(huì)導(dǎo)致性能下降。
具有多個(gè)線程的另一個(gè)成本是需要同步對(duì)共享數(shù)據(jù)結(jié)構(gòu)的訪問(wèn)。 除了使用關(guān)鍵字sync,我們還可以使用volatile在多個(gè)線程之間共享數(shù)據(jù)。 如果有多個(gè)線程爭(zhēng)用結(jié)構(gòu)化的共享數(shù)據(jù),那么我們就有爭(zhēng)執(zhí)。 然后,JVM必須決定下一步執(zhí)行哪個(gè)線程。 如果這不是當(dāng)前線程,那么將引入上下文切換成本。 然后,當(dāng)前線程必須等待,直到可以獲取鎖為止。 JVM可以自行決定如何實(shí)現(xiàn)此等待。 與掛起線程并讓另一個(gè)線程占用CPU時(shí)所需的上下文切換相比,當(dāng)直到可以獲取鎖為止的預(yù)期時(shí)間很小時(shí),自旋等待(即嘗試一次又一次地獲取鎖)可能比效率更高。 使等待線程重新執(zhí)行需要另一個(gè)上下文切換,并增加了鎖爭(zhēng)用的額外成本。
因此,減少由于鎖爭(zhēng)用而必需的上下文切換的數(shù)量是合理的。 下節(jié)描述了兩種減少此爭(zhēng)用的方法。
鎖爭(zhēng)用
如上一節(jié)所述,爭(zhēng)用一個(gè)鎖的兩個(gè)或多個(gè)線程引入了額外的時(shí)鐘周期,因?yàn)闋?zhēng)用可能迫使調(diào)度程序要么讓一個(gè)線程旋轉(zhuǎn)等待該鎖,要么讓另一個(gè)線程占用處理器的成本。兩個(gè)上下文切換。 在某些情況下,可以通過(guò)應(yīng)用以下技術(shù)之一來(lái)減少鎖爭(zhēng)用:
- 鎖的范圍減小了。
- 減少獲取某個(gè)鎖的次數(shù)。
- 使用硬件支持的樂(lè)觀鎖定操作而不是同步。
- 盡可能避免同步
- 避免對(duì)象池
2.3.1縮小范圍
當(dāng)鎖的保持時(shí)間超過(guò)必要時(shí)間時(shí),可以應(yīng)用第一種技術(shù)。 通常,這可以通過(guò)將一條或多條線移出同步塊來(lái)實(shí)現(xiàn),以減少當(dāng)前線程保持鎖的時(shí)間。 執(zhí)行當(dāng)前線程越早執(zhí)行的代碼行數(shù)越少,則可以離開同步塊,從而讓其他線程獲得鎖。 這也符合阿姆達(dá)爾定律,因?yàn)槲覀儨p少了在同步塊中花費(fèi)的運(yùn)行時(shí)間的比例。
為了更好地理解此技術(shù),請(qǐng)看以下源代碼:
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");} }在此示例應(yīng)用程序中,我們讓五個(gè)線程競(jìng)爭(zhēng)訪問(wèn)共享Map。 為了一次只允許一個(gè)線程訪問(wèn)Map,將訪問(wèn)Map并添加新的鍵/值對(duì)的代碼放入同步塊中。 當(dāng)我們仔細(xì)查看該塊時(shí),我們看到密鑰的計(jì)算以及原始整數(shù)42到Integer對(duì)象的轉(zhuǎn)換必須不同步。 從概念上講,它們屬于訪問(wèn)Map的代碼,但它們?cè)诋?dāng)前線程本地,并且實(shí)例未被其他線程修改。 因此,我們可以將它們移出同步塊:
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();}}減少同步塊會(huì)對(duì)可以測(cè)量的運(yùn)行時(shí)間產(chǎn)生影響。 在我的機(jī)器上,使用最小化同步塊的版本將整個(gè)應(yīng)用程序的運(yùn)行時(shí)間從420ms減少到370ms。 僅通過(guò)將三行代碼移出同步塊,就可以使運(yùn)行時(shí)間總共減少11%。 引入Thread.yield()語(yǔ)句是為了引起更多上下文切換,因?yàn)榇朔椒ㄕ{(diào)用告訴JVM當(dāng)前線程愿意將處理器提供給另一個(gè)等待線程。 這再次引發(fā)了更多的鎖爭(zhēng)用,否則一個(gè)線程可能在沒(méi)有任何競(jìng)爭(zhēng)線程的情況下在處理器上運(yùn)行太長(zhǎng)時(shí)間。
2.3.2鎖拆分
減少鎖爭(zhēng)用的另一種技術(shù)是將一個(gè)鎖拆分為多個(gè)較小范圍的鎖。 如果您有一個(gè)鎖來(lái)保護(hù)應(yīng)用程序的不同方面,則可以應(yīng)用此技術(shù)。 假設(shè)我們想收集有關(guān)應(yīng)用程序的一些統(tǒng)計(jì)數(shù)據(jù),并實(shí)現(xiàn)一個(gè)簡(jiǎn)單的計(jì)數(shù)器類,該計(jì)數(shù)器類在每個(gè)方面都包含一個(gè)原始計(jì)數(shù)器變量。 由于我們的應(yīng)用程序是多線程的,因此必須同步訪問(wèn)這些變量,因?yàn)樗鼈兪菑牟煌牟l(fā)線程訪問(wèn)的。 最簡(jiǎn)單的方法是在Counter的每個(gè)方法的方法簽名中使用synced關(guān)鍵字:
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;}}這種方法還意味著計(jì)數(shù)器的每個(gè)增量都會(huì)鎖定Counter的整個(gè)實(shí)例。 其他要增加其他變量的線程必須等待,直到釋放此單個(gè)鎖。 在這種情況下,更有效的方法是為每個(gè)計(jì)數(shù)器使用單獨(dú)的鎖,如下例所示:
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è)單獨(dú)的同步對(duì)象,每個(gè)計(jì)數(shù)器一個(gè)。 因此,試圖增加我們系統(tǒng)中客戶數(shù)量的線程只需要與其他線程競(jìng)爭(zhēng),后者也可以增加客戶數(shù)量,而不必與試圖增加發(fā)貨數(shù)量的線程競(jìng)爭(zhēng)。
通過(guò)使用以下類,我們可以輕松衡量此鎖拆分的影響:
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ī)器上,使用一個(gè)鎖的實(shí)現(xiàn)平均大約需要56ms,而使用兩個(gè)鎖的實(shí)現(xiàn)大約需要38ms。 這減少了約32%。
另一個(gè)可能的改進(jìn)是通過(guò)區(qū)分讀和寫鎖來(lái)進(jìn)一步分離鎖。 例如, Counter類提供用于讀取和寫入計(jì)數(shù)器值的方法。 雖然讀取當(dāng)前值可以由多個(gè)線程并行完成,但所有寫入操作都必須序列化。 java.util.concurrent包提供了此類ReadWriteLock的即用型實(shí)現(xiàn)。
ReentrantReadWriteLock實(shí)現(xiàn)管理兩個(gè)單獨(dú)的鎖。 一種用于讀訪問(wèn),一種用于寫訪問(wèn)。 讀鎖定和寫鎖定都提供了用于鎖定和解鎖的方法。 如果沒(méi)有讀取鎖,則僅獲取寫入鎖。 只要不獲取寫鎖,就可以在讀取器線程上獲取讀鎖。 出于演示目的,下面顯示了使用ReadWriteLock實(shí)現(xiàn)計(jì)數(shù)器類的方法:
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;}}所有讀訪問(wèn)都通過(guò)獲取讀鎖來(lái)保護(hù),而所有寫訪問(wèn)都通過(guò)相應(yīng)的寫鎖來(lái)保護(hù)。 如果應(yīng)用程序使用的讀取訪問(wèn)次數(shù)比寫入訪問(wèn)次數(shù)多,則這種實(shí)現(xiàn)甚至可以比以前的實(shí)現(xiàn)獲得更多的性能改進(jìn),因?yàn)樗凶x取線程都可以并行訪問(wèn)getter方法。
2.3.3鎖條
前面的示例演示了如何將一個(gè)鎖分為兩個(gè)單獨(dú)的鎖。 這允許競(jìng)爭(zhēng)線程僅獲取保護(hù)它們要操縱的數(shù)據(jù)結(jié)構(gòu)的鎖。 另一方面,如果實(shí)施不當(dāng),該技術(shù)還會(huì)增加復(fù)雜性和死鎖的風(fēng)險(xiǎn)。
另一方面,鎖條是一種類似于鎖拆分的技術(shù)。 我們沒(méi)有拆分一個(gè)保護(hù)不同代碼部分或方面的鎖,而是對(duì)不同的值使用了不同的鎖。 JDK的java.util.concurrent包中的ConcurrentHashMap類使用此技術(shù)來(lái)提高嚴(yán)重依賴HashMap的應(yīng)用程序的性能。 與java.util.HashMap的同步版本相反, ConcurrentHashMap使用16個(gè)不同的鎖。 每個(gè)鎖僅保護(hù)可用哈希桶的1/16。 這允許希望將數(shù)據(jù)插入可用哈希桶的不同部分的不同線程同時(shí)執(zhí)行此操作,因?yàn)樗鼈兊牟僮饔刹煌逆i保護(hù)。 另一方面,它也引入了為特定操作獲取多個(gè)鎖的問(wèn)題。 例如,如果要復(fù)制整個(gè)地圖,則必須獲取所有16個(gè)鎖。
2.3.4原子操作
減少鎖爭(zhēng)用的另一種方法是使用所謂的原子操作。 以下文章之一將詳細(xì)解釋和評(píng)估此原理。 java.util.concurrent包為某些原始數(shù)據(jù)類型提供了對(duì)原子操作的支持。 原子操作是使用處理器提供的所謂的比較和交換(CAS)操作實(shí)現(xiàn)的。 如果當(dāng)前值等于提供的值,則CAS指令僅更新某個(gè)寄存器的值。 僅在這種情況下,舊值才被新值替換。
該原理可用于樂(lè)觀地增加變量。 如果我們假設(shè)線程知道當(dāng)前值,那么它可以嘗試使用CAS操作將其遞增。 如果事實(shí)證明,另一個(gè)線程同時(shí)增加了該值,而我們的值不再是當(dāng)前值,則我們請(qǐng)求當(dāng)前值,然后重試。 這可以完成,直到我們成功增加計(jì)數(shù)器。 盡管我們可能需要一些旋轉(zhuǎn),但此實(shí)現(xiàn)的優(yōu)點(diǎn)是我們不需要任何類型的同步。
Counter類的以下實(shí)現(xiàn)使用原子變量方法,并且不使用任何同步塊:
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。 運(yùn)行時(shí)間減少了約58%。
2.3.5避免熱點(diǎn)
列表的典型實(shí)現(xiàn)將在內(nèi)部管理一個(gè)計(jì)數(shù)器,該計(jì)數(shù)器保存列表中的項(xiàng)目數(shù)。 每次將新項(xiàng)目添加到列表或從列表中刪除時(shí),此計(jì)數(shù)器都會(huì)更新。 如果在單線程應(yīng)用程序中使用,則此優(yōu)化是合理的,因?yàn)榱斜砩系膕ize()操作將直接返回先前計(jì)算的值。 如果列表不包含列表中的項(xiàng)目數(shù),則size()操作將必須遍歷所有項(xiàng)目才能進(jìn)行計(jì)算。
在許多數(shù)據(jù)結(jié)構(gòu)中常見(jiàn)的優(yōu)化可能會(huì)在多線程應(yīng)用程序中成為問(wèn)題。 假設(shè)我們想與一堆線程共享該列表的實(shí)例,這些線程可以從列表中插入和刪除項(xiàng)目,并查詢其大小。 現(xiàn)在,計(jì)數(shù)器變量也是共享資源,必須同步對(duì)其值的所有訪問(wèn)。 計(jì)數(shù)器已成為實(shí)施中的熱點(diǎn)。
下面的代碼段演示了此問(wèn)題:
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;}}}CarRepository實(shí)現(xiàn)包含兩個(gè)列表:一個(gè)用于汽車,一個(gè)用于卡車。 它還提供了一種返回兩個(gè)列表中當(dāng)前汽車和卡車數(shù)量的方法。 作為優(yōu)化,每次將新車添加到兩個(gè)列表之一時(shí),它都會(huì)增加內(nèi)部計(jì)數(shù)器。 該操作必須與專用的carCountSync實(shí)例同步。 返回計(jì)數(shù)值時(shí),將使用相同的同步。
為了擺脫這種額外的同步中, CarRepository本來(lái)也可以實(shí)現(xiàn)通過(guò)省略額外的計(jì)數(shù)器和每個(gè)值是通過(guò)調(diào)用查詢時(shí)間計(jì)算總的汽車數(shù)量getCarCount()
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()方法中的汽車和卡車列表進(jìn)行同步并計(jì)算大小,但是getCarCount()添加新汽車期間的額外同步。
2.3.6避免對(duì)象池
在Java VM對(duì)象的第一個(gè)版本中,使用new運(yùn)算符創(chuàng)建仍然是一項(xiàng)昂貴的操作。 這導(dǎo)致許多程序員采用了對(duì)象池的通用模式。 他們沒(méi)有一次又一次地創(chuàng)建某些對(duì)象,而是構(gòu)造了這些對(duì)象的池,并且每次需要一個(gè)實(shí)例時(shí),都會(huì)從池中獲取一個(gè)實(shí)例。 使用完對(duì)象后,將其放回池中,并可以由另一個(gè)線程使用。
乍看之下,在多線程應(yīng)用程序中使用時(shí)可能會(huì)遇到問(wèn)題。 現(xiàn)在,對(duì)象池在所有線程之間共享,并且必須同步對(duì)池中對(duì)象的訪問(wèn)。 現(xiàn)在,這種額外的同步開銷可能大于對(duì)象創(chuàng)建本身的開銷。 當(dāng)您考慮垃圾收集器收集新創(chuàng)建的對(duì)象實(shí)例的額外費(fèi)用時(shí),甚至是這樣。
與所有性能優(yōu)化一樣,此示例再次顯示,在應(yīng)用每個(gè)可能的改進(jìn)之前,應(yīng)仔細(xì)評(píng)估它們。 乍一看似乎很有意義的優(yōu)化在沒(méi)有正確實(shí)施的情況下甚至可能成為性能瓶頸。
翻譯自: https://www.javacodegeeks.com/2015/09/performance-scalability-and-liveness.html
高性能 高可用 可彈性伸縮
總結(jié)
以上是生活随笔為你收集整理的高性能 高可用 可弹性伸缩_性能,可伸缩性和活力的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: ddos程序(编程ddos是什么)
- 下一篇: 安卓手机容量在哪看(安卓手机容量)