日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

性能,可伸缩性和活力

發布時間:2023/12/3 编程问答 38 豆豆
生活随笔 收集整理的這篇文章主要介紹了 性能,可伸缩性和活力 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

本文是我們學院課程中名為Java Concurrency Essentials的一部分 。

在本課程中,您將深入探討并發的魔力。 將向您介紹并發和并發代碼的基礎知識,并學習諸如原子性,同步和線程安全之類的概念。 在這里查看 !

目錄

1.簡介 2.表現
2.1。 阿姆達爾定律 2.2。 線程對性能的影響 2.3。 鎖爭用

1.簡介

本文討論了多線程應用程序的性能主題。 在定義了性能和可伸縮性這兩個術語之后,我們將仔細研究阿姆達爾定律。 在本課程的進一步內容中,我們將看到如何通過應用不同的技術來減少鎖爭用,如代碼示例所示。

2.表現

線程可用于提高應用程序的性能。 其背后的原因可能是我們有多個可用的處理器或CPU內核。 每個CPU內核都可以執行自己的任務,因此將大任務劃分為一系列相互獨立運行的較小任務,可以改善應用程序的總運行時間。 這種性能提高的一個示例可以是調整硬盤上文件夾結構中的圖像大小的應用程序。 單線程方法將僅遍歷所有文件并逐個縮放每個圖像。 如果我們的CPU具有多個內核,則調整大小過程將僅利用可用內核之一。 例如,多線程方法可以讓生產者線程掃描文件系統,并將所有找到的文件添加到隊列中,該隊列由一堆工作線程處理。 當我們擁有與CPU內核一樣多的工作線程時,我們確保每個CPU內核都有所要做的事情,直到處理完所有映像為止。

多線程可以提高應用程序整體性能的另一個示例是具有大量I / O等待時間的用例。 假設我們要編寫一個應用程序,以HTML文件的形式將完整的網站鏡像到我們的硬盤上。 從一頁開始,應用程序必須遵循指向同一域(或URL部分)的所有鏈接。 從向遠程Web服務器發出請求直到收到所有數據之間的時間可能很長,我們可以將工作分配到幾個線程上。 一個或多個線程可以解析接收到HTML頁面并將找到的鏈接放入隊列,而其他線程可以將請求發送到Web服務器,然后等待答案。 在這種情況下,我們將等待時間用于新請求的頁面以及已接收頁面的解析。 與前面的示例相比,如果我們添加的線程數超過了CPU內核的數量,則此應用程序甚至可能會獲得性能。

這兩個例子表明,性能意味著可以在更短的時間內完成更多的工作。 當然,這是對術語“性能”的經典理解。 但是線程的使用也可以提高我們應用程序的響應速度。 想象一下簡單的GUI應用程序,它帶有一個輸入表單和一個“ Process”按鈕。 當用戶按下按鈕時,應用程序必須呈現被按下的按鈕(按鈕應像被按下并在釋放鼠標時再次升起一樣鎖定),并且必須完成輸入數據的實際處理。 如果此處理需要更長的時間,則單線程應用程序將無法對進一步的用戶輸入做出反應,即,我們需要一個附加線程來處理來自操作系統的事件,例如鼠標單擊或鼠標指針移動。

可伸縮性是指程序通過向其添加更多資源來提高性能的能力。 想象一下,我們將不得不調整大量圖像的大小。 由于當前計算機的CPU內核數量有限,因此添加更多線程并不能提高性能。 由于調度程序必須管理更多的線程,因此性能甚至可能下降,并且線程的創建和關閉也會消耗CPU功率。

阿姆達爾定律

最后一部分顯示,在某些情況下,添加新資源可以提高應用程序的整體性能。 為了能夠計算出當我們添加更多資源時應用程序可以獲得多少性能,我們需要確定程序中必須串行化/同步運行的部分以及程序中可以并行運行的部分。 如果我們表示必須與B同步運行的程序部分(例如,已同步執行的行數),并且如果我們表示具有n的可用處理器數,那么阿姆達爾定律就可以計算出加速的上限我們的應用程序可能能夠實現:

圖1

如果我們讓n接近無窮大,則項(1-B)/ n收斂于零。 因此,我們可以忽略該術語,并且提速上限針對1 / B收斂,其中B是優化之前程序運行時在不可并行代碼中花費的分數。 例如,如果B為0.5,則意味著程序的一半不能并行化,則0.5的倒數為2;如果B為0.5,則倒數為2。 因此,即使我們向應用程序中添加無限數量的處理器,我們也只能獲得大約2倍的加速。 現在,我們假設可以重寫代碼,以便僅0.25的程序運行時花費在同步塊中。 現在,倒數0.25為4,這意味著我們構建了一個可以在大量處理器上運行的應用程序,其運行速度比僅一個處理器快四倍。

反之,我們也可以使用阿姆達爾定律來計算程序運行時必須同步執行以達到給定加速比的分數。 如果我們想實現約100的加速,則倒數是0.01,這意味著我們應該只在同步代碼中花費大約1%的運行時。

總結來自阿姆達爾定律的發現,我們可以得出結論,通過使用附加處理器可以使程序獲得的最大速度受到程序花費在同步代碼部分中的時間的倒數的限制。 盡管在實踐中計算該分數并不總是那么容易,即使您考慮大型商業應用程序也不是一件容易的事,但法律給我們的提示是,我們必須非常仔細地考慮同步,并且必須保留程序運行時的各個部分。小,必須序列化。

線程對性能的影響

到目前為止,本文的著作表明,向應用程序添加更多線程可以提高性能和響應能力。 但是,另一方面,這不是免費的。 線程本身總是會對性能產生影響。

對性能的第一個影響是線程本身的創建。 這需要花費一些時間,因為JVM必須從底層操作系統中獲取線程的資源并準備調度程序中的數據結構,該調度程序決定下一步執行哪個線程。

如果使用與處理器內核一樣多的線程,則每個線程都可以在自己的處理器上運行,并且不會經常被中斷。 實際上,在您的應用程序運行時,操作系統當然可能需要其自己的計算。 因此即使在這種情況下,線程也會中斷,并且必須等到操作系統讓它們再次運行。 當您必須使用比CPU內核更多的線程時,情況變得更糟。 在這種情況下,調度程序可以中斷您的線程,以便讓另一個線程執行其代碼。 在這種情況下,必須保存正在運行的線程的當前狀態,必須還原應該在接下來運行的調度線程的狀態。 除此之外,調度程序本身還必須對其內部數據結構執行一些更新,這些更新再次使用CPU功能。 總而言之,這意味著每個上下文從一個線程切換到另一個線程會消耗CPU能力,因此與單線程解決方案相比會導致性能下降。

具有多個線程的另一個成本是需要同步對共享數據結構的訪問。 除了使用關鍵字sync,我們還可以使用volatile在多個線程之間共享數據。 如果有多個線程爭用結構化的共享數據,那么我們就有爭執。 然后,JVM必須決定下一步執行哪個線程。 如果這不是當前線程,則會引入上下文切換的成本。 然后,當前線程必須等待,直到可以獲取鎖為止。 JVM可以自行決定如何實現此等待。 與掛起線程并讓另一個線程占用CPU時所需的上下文切換相比,當直到可以獲取該鎖的預期時間很小時,自旋等待(即嘗試一次又一次地獲取鎖)可能比效率更高。 使等待線程重新執行需要另一個上下文切換,并增加了鎖爭用的額外成本。

因此,減少由于鎖爭用而必需的上下文切換的數量是合理的。 以下部分描述了兩種減少此爭用的方法。

鎖爭用

如上一節所述,爭用一個鎖的兩個或多個線程引入了額外的時鐘周期,因為爭用可能迫使調度程序要么讓一個線程旋轉等待鎖,要么讓另一個線程以占用處理器的代價占用處理器。兩個上下文切換。 在某些情況下,可以通過應用以下技術之一來減少鎖爭用:

  • 鎖的范圍減小了。
  • 減少獲取某個鎖的次數。
  • 使用硬件支持的樂觀鎖定操作而不是同步。
  • 盡可能避免同步
  • 避免對象池

2.3.1縮小范圍

當鎖的保持時間超過必要時間時,可以應用第一種技術。 通常,可以通過從同步塊中移出一條或多條行來減少當前線程保持鎖的時間來實現。 執行當前線程越早執行的代碼行數越少,則可以離開同步塊,從而讓其他線程獲得鎖。 這也符合阿姆達爾定律,因為我們減少了在同步塊中花費的運行時間的比例。

為了更好地理解該技術,請看下面的源代碼:

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");} }

在此示例應用程序中,我們讓五個線程競爭訪問共享Map。 為了一次只允許一個線程訪問Map,將訪問Map并添加新的鍵/值對的代碼放入同步塊中。 當我們仔細查看該塊時,我們看到密鑰的計算以及原始整數42到Integer對象的轉換必須不同步。 從概念上講,它們屬于訪問Map的代碼,但它們在當前線程本地,并且實例未被其他線程修改。 因此,我們可以將它們移出同步塊:

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();}}

減少同步塊會對可以測量的運行時間產生影響。 在我的機器上,使用最小化同步塊的版本將整個應用程序的運行時間從420ms減少到370ms。 僅通過將三行代碼移出同步塊,就可以使運行時間總共減少11%。 引入Thread.yield()語句是為了引起更多的上下文切換,因為此方法調用告訴JVM當前線程愿意將處理器提供給另一個等待線程。 這又引發了更多的鎖爭用,否則一個線程可能在沒有任何競爭線程的情況下在處理器上運行太長時間。

2.3.2鎖拆分

減少鎖爭用的另一種技術是將一個鎖拆分為多個較小范圍的鎖。 如果您有一個鎖來保護應用程序的不同方面,則可以應用此技術。 假定我們要收集有關應用程序的一些統計數據,并實現一個簡單的計數器類,該計數器類在每個方面都保留一個原始計數器變量。 由于我們的應用程序是多線程的,因此必須同步訪問這些變量,因為它們是從不同的并發線程訪問的。 最簡單的方法是在Counter的每個方法的方法簽名中使用synced關鍵字:

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;}}

這種方法還意味著計數器的每個增量都會鎖定Counter的整個實例。 其他要增加其他變量的線程必須等待,直到釋放此單個鎖。 在這種情況下,更有效的方法是為每個計數器使用單獨的鎖,如下例所示:

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;}}}

此實現引入了兩個單獨的同步對象,每個計數器一個。 因此,試圖增加我們系統中的客戶數量的線程只需要與其他線程競爭,而其他線程也可以增加客戶數量,但是它不必與試圖增加發貨數量的線程競爭。

通過使用以下類,我們可以輕松衡量此鎖拆分的影響:

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");} }

在我的機器上,使用一個鎖的實現平均大約需要56ms,而使用兩個鎖的實現大約需要38ms。 這減少了約32%。

另一個可能的改進是通過區分讀和寫鎖來進一步分離鎖。 例如, Counter類提供用于讀取和寫入計數器值的方法。 雖然讀取當前值可以由多個線程并行完成,但所有寫入操作都必須序列化。 java.util.concurrent包提供了此類ReadWriteLock的即用型實現。

ReentrantReadWriteLock實現管理兩個單獨的鎖。 一種用于讀訪問,一種用于寫訪問。 讀鎖定和寫鎖定都提供了用于鎖定和解鎖的方法。 僅當沒有讀鎖時才獲取寫鎖。 只要不獲取寫鎖,就可以在讀取器線程上獲取讀鎖。 為了演示起見,以下顯示了使用ReadWriteLock的計數器類的實現:

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;}}

所有讀訪問都通過獲取讀鎖來保護,而所有寫訪問都通過相應的寫鎖來保護。 如果應用程序使用的讀取訪問次數比寫入訪問次數多,則這種實現甚至可以比以前的實現獲得更多的性能改進,因為所有讀取線程都可以并行訪問getter方法。

2.3.3鎖條

前面的示例演示了如何將一個鎖分為兩個單獨的鎖。 這允許競爭線程僅獲取保護他們要操縱的數據結構的鎖。 另一方面,如果未正確實施,此技術還會增加復雜性和死鎖的風險。

另一方面,鎖條是一種類似于鎖拆分的技術。 我們沒有拆分一個保護不同代碼部分或方面的鎖,而是對不同的值使用了不同的鎖。 JDK的java.util.concurrent包中的ConcurrentHashMap類使用此技術來提高嚴重依賴HashMap的應用程序的性能。 與java.util.HashMap的同步版本相反, ConcurrentHashMap使用16個不同的鎖。 每個鎖僅保護可用哈希桶的1/16。 這允許希望將數據插入可用哈希桶的不同部分的不同線程同時執行此操作,因為它們的操作由不同的鎖保護。 另一方面,它也引入了為特定操作獲取多個鎖的問題。 例如,如果要復制整個地圖,則必須獲取所有16個鎖。

2.3.4原子操作

減少鎖爭用的另一種方法是使用所謂的原子操作。 以下文章之一將詳細解釋和評估此原理。 java.util.concurrent包為某些原始數據類型提供了對原子操作的支持。 原子操作是使用處理器提供的所謂的“比較和交換”(CAS)操作實現的。 如果當前值等于提供的值,則CAS指令僅更新某個寄存器的值。 僅在這種情況下,舊值才被新值替換。

該原理可用于樂觀地增加變量。 如果我們假設線程知道當前值,那么它可以嘗試使用CAS操作將其遞增。 如果事實證明,另一個線程同時增加了該值,而我們的值不再是當前值,則我們請求當前值,然后重試。 這可以完成,直到我們成功增加計數器。 盡管我們可能需要一些旋轉,但此實現的優點是我們不需要任何類型的同步。

Counter類的以下實現使用原子變量方法,并且不使用任何同步塊:

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類相比,平均總運行時間從39ms減少到16ms。 運行時間減少了約58%。

2.3.5避免熱點

列表的典型實現將在內部管理一個計數器,該計數器保存列表中的項目數。 每當有新項目添加到列表或從列表中刪除時,此計數器都會更新。 如果在單線程應用程序中使用,則此優化是合理的,因為列表上的size()操作將直接返回先前計算的值。 如果列表不包含列表中的項目數,則size()操作將必須遍歷所有項目才能進行計算。

在許多數據結構中常見的優化可能會在多線程應用程序中成為問題。 假設我們想與一堆線程共享該列表的實例,這些線程可以從列表中插入和刪除項目,并查詢其大小。 現在,counter變量也是共享資源,必須同步對其值的所有訪問。 計數器已成為實施中的熱點。

下面的代碼段演示了此問題:

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實現包含兩個列表:一個用于汽車,一個用于卡車。 它還提供了一種返回兩個列表中當前汽車和卡車數量的方法。 作為優化,每次將新車添加到兩個列表之一時,它都會增加內部計數器。 該操作必須與專用的carCountSync實例同步。 返回計數值時,將使用相同的同步。

為了擺脫這種額外的同步中, CarRepository本來也可以實現通過省略額外的計數器和每個值是通過調用查詢時間計算總的汽車數量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();}}}}

現在,我們需要與getCarCount()方法中的汽車和卡車列表進行同步并計算大小,但是getCarCount()添加新汽車期間的額外同步。

2.3.6避免對象池

在Java VM對象的第一個版本中,使用new運算符創建仍然是一項昂貴的操作。 這使許多程序員采用了對象池的通用模式。 他們沒有一次又一次地創建某些對象,而是構造了這些對象的池,每次需要一個實例時,都會從池中獲取一個實例。 使用完對象后,將其放回池中,并可以由另一個線程使用。

乍看之下,在多線程應用程序中使用時可能會遇到問題。 現在,對象池在所有線程之間共享,并且必須同步對池中對象的訪問。 現在,這種額外的同步開銷可能大于對象創建本身的開銷。 當您考慮垃圾收集器收集新創建的對象實例的額外費用時,甚至是這樣。

與所有性能優化一樣,此示例再次說明,在應用每種可能的改進之前,應仔細評估它們。 乍一看似乎很有意義的優化在沒有正確實施的情況下甚至可能成為性能瓶頸。

翻譯自: https://www.javacodegeeks.com/2015/09/performance-scalability-and-liveness.html

創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎

總結

以上是生活随笔為你收集整理的性能,可伸缩性和活力的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。