【連載 12】執行緒安全的集合類

FunTester發表於2025-01-18

2.7 執行緒安全的集合類

集合類是 Java 程式語言中的一組資料結構,用於儲存和運算元據。集合類提供了一種組織和管理資料的方式,可以用於實現各種程式設計需求。Java 的集合類非常豐富,包括多種不同型別的集合,每種都適用於不同的使用場景。在 Java 基礎中學習的幾種集合類都不是執行緒安全的,因此我們需要重新學習幾種執行緒安全的集合類。

雖說如此,但學習執行緒安全集合類是非常容易的。因為它們都能從 Java 基礎集合類中找到對應,而且它們的操作方法幾乎是一模一樣的。

下面介紹幾種在 Java 效能測試中常見的執行緒安全的集合類。

2.7.1 List 列表

java.util.List 是 Java 基礎中集合框架中的一個介面。它用於儲存有序的、可重複的元素集合,支援對集合中的增、刪、改、查操作。Java JDK 中提供了很多實現了 List 介面的功能類,其中最常見的是 java.util.ArrayList,相信你肯定不會陌生。下面要介紹的是它線上程安全平行時空的好兄弟 java.util.Vector

Vector 的功能與 ArrayList 是一模一樣的,唯一的區別就是 Vector 是執行緒安全的。Vector 的執行緒安全表現在多個執行緒可以同時修改 Vector 內容時,Vector 儲存內容不會錯亂,出現非期望的異常。下面介紹 Vector 的基礎功能。

1. 增(add)

向列表中新增元素的方法:

public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}

向列表中新增批次元素的方法:

public boolean addAll(Collection<? extends E> c) {
    boolean modified = false;
    for (E e : c)
        if (add(e))
            modified = true;
    return modified;
}

2. 刪(remove)

從列表中刪除某一個元素:

public boolean remove(Object o) {
    return removeElement(o);
}

其中 removeElement() 方法內容如下:

public synchronized boolean removeElement(Object obj) {
    modCount++;
    int i = indexOf(obj);
    if (i >= 0) {
        removeElementAt(i);
        return true;
    }
    return false;
}

下面是從列表中刪除某個索引對應的元素:

public synchronized E remove(int index) {
    modCount++;
    if (index >= elementCount)
        throw new ArrayIndexOutOfBoundsException(index);
    E oldValue = elementData(index);

    int numMoved = elementCount - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index, numMoved);
    elementData[--elementCount] = null; // Let gc do its work

    return oldValue;
}

批次刪除這裡就不分享了。

3. 改(set)

修改某個索引對應的元素方法:

public synchronized E set(int index, E element) {
    if (index >= elementCount)
        throw new ArrayIndexOutOfBoundsException(index);

    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}

4. 查(get)

從列表中查詢元素的方法:

public synchronized E get(int index) {
    if (index >= elementCount)
        throw new ArrayIndexOutOfBoundsException(index);

    return elementData(index);
}

透過對 Vector 增、刪、改、查原始碼的閱讀和學習,我們會有 2 個發現:

  1. VectorArrayList 方法名稱和引數一模一樣,原因是它們都實現了 java.util.List 介面。
  2. Vector 類採用 synchronized 關鍵字修飾操作方法實現執行緒安全。

掌握這兩點,我們就已經掌握了 Vector 基本功能,實際使用語法與 ArrayList 更是完全通用的。

2.7.2 Map 對映

java.util.Map 是 Java 基礎中集合框架中的一個介面。它用於儲存鍵值對(key-value)資料,每一個鍵(key)都唯一地對映到一個值(value),提供對資料的增、刪、改、查操作。Java SDK 中提供了多個實現了 java.util.Map 介面的功能類,其中最常用的是 java.util.HashMap。下面繼續介紹它線上程安全平行時空的好兄弟。

ConcurrentHashMapHashMap 功能和呼叫方法均是相同的,這一點與上一節 VectorArrayList 是一致的。ConcurrentHashMap 是執行緒安全的,表現在多執行緒修改元素時,不會產生髒資料或者異常資料。

由於 ConcurrentHashMap 實現執行緒安全的設計方案過於複雜,下面僅列舉基本操作方法,不再展示方法體內容。

  • :單個新增 java.util.concurrent.ConcurrentHashMap#put,批次新增 java.util.concurrent.ConcurrentHashMap#putAll
  • :刪除某個鍵(key)及對應值(value)java.util.concurrent.ConcurrentHashMap#remove(java.lang.Object),刪除某個鍵值對(key-value)java.util.concurrent.ConcurrentHashMap#remove(java.lang.Object)
  • :修改某個鍵(key)對應值(value)java.util.concurrent.ConcurrentHashMap#replace(K, V)
  • :查詢某個鍵(key)的值(value)java.util.concurrent.ConcurrentHashMap#get,查詢集合中是否包含某個鍵(key)java.util.concurrent.ConcurrentHashMap#containsKey,查詢集合中是否包含某個值(value)java.util.concurrent.ConcurrentHashMap#containsValue

ConcurrentHashMap 類還擁有以下幾個優點:

  • 高效能低競爭:由於 ConcurrentHashMap 使用的分段鎖的設計,所以在多執行緒讀操作上效能非常高,多個階段鎖降低了執行緒之間競爭。
  • 支援多個原子操作:原子操作是保障執行緒安全的,ConcurrentHashMap 提供多個原子操作的方法,方便使用者編寫執行緒安全的程式碼。
  • 高併發讀寫ConcurrentHashMap 在多執行緒環境下支援高併發的讀寫操作。不同於傳統的 HashTable 或同步的 HashMap,它提供了更好的併發效能,使得多個執行緒可以同時進行讀寫操作而不會造成效能上的嚴重影響。

2.7.3 佇列

在 Java 中,佇列是一種常見的資料結構,用於儲存和管理元素。Java 提供了多種佇列的實現,每種實現都有其特定的用途和特性。在效能測試當中,多多少少都會用到佇列來實現預期的測試方案,下面分享幾種常用的佇列類。

1. LinkedBlockingQueue

java.util.Queue 是 Java 程式語言集合框架中的一個介面。它用於儲存和管理資料,其基本操作有兩種:進佇列,出佇列,常見的順序是先進先出,即先進入佇列的元素最先被取出。Java 提供了多個擴充了 java.util.Queue 介面的介面,以及其實現類。java.util.concurrent.LinkedBlockingQueue 是我在使用 Java 進行效能測試中最常使用的執行緒安全佇列。下面介紹 LinkedBlockingQueue 的基本操作。

新增元素的方法如下

  • add(E e):將元素新增到佇列的尾部。如果佇列已經滿了,則丟擲 IllegalStateException 異常。
  • offer(E e):將元素新增到佇列的尾部。如果成功則返回 true,否則返回 false
  • offer(E e, long timeout, TimeUnit unit):具有超時設定的 offer(E e),在限定時間內,成功返回 true,失敗返回 false
  • put(E e):將元素新增到佇列的尾部。如果佇列已滿,則會阻塞當前執行緒,直至新增成功。

從佇列中獲取元素方法如下

  • remove():從佇列頭獲取一個元素並移除佇列,如果返回 null,則丟擲 NoSuchElementException 異常。
  • poll():從佇列頭獲取一個元素並將其移除佇列。如果佇列為空,則返回 null
  • poll(long timeout, TimeUnit unit):具有超時設定的 poll() 方法,在設定時間內獲取成功則返回元素,否則返回 null
  • take():從佇列頭獲取一個元素並移除佇列。如果佇列為空,則會阻塞當前執行緒,直至獲取到一個元素。
  • peek():從佇列頭部獲取元素,但並不移除該元素。

LinkedBlockingQueue 實現執行緒安全的設計中,用到了大量的 ReentrantLock 物件,是不是有點閉環了。這種情況還是非常普遍的,很多精妙的設計都依靠簡單、可靠的解決方案。

2. DelayQueue

DelayQueue 是 Java SDK 包 java.util.concurrent 包提供的一個阻塞優先順序佇列。DelayQueue 佇列中的元素只有在到達指定的延遲時間之後才能被取出,因此經常用來當作延遲佇列使用。DelayQueue 要求存放的元素物件必須實現 java.util.concurrent.Delayed 介面(該介面繼承介面 java.lang.Comparable),在新元素被新增時,佇列會根據元素的 compareTo() 方法返回值進行排序。DelayQueue 提供阻塞操作,方便從佇列中獲取可用元素。

DelayQueue 基本操作方法與 LinkedBlockingQueue 是一樣的,這是由於兩者均實現了介面 java.util.concurrent.BlockingQueue。由於 DelayQueue 對儲存元素型別的要求,下面寫一個簡單的例子來演示 DelayQueue 如何使用,程式碼如下:

package org.funtester.performance.books.chapter02.section7;

import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;

/**
 * 延遲佇列示例
 */
public class DelayQueueDemo implements Delayed {

    public static void main(String[] args) {
        DelayQueue<DelayQueueDemo> delayQueue = new DelayQueue<>(); // 建立延遲佇列
        delayQueue.add(new DelayQueueDemo()); // 新增元素
        delayQueue.add(new DelayQueueDemo()); // 新增元素
        delayQueue.add(new DelayQueueDemo()); // 新增元素
        System.out.println(System.currentTimeMillis() + "  新增完成"); // 列印新增完成資訊
        while (true) { // 迴圈獲取元素
            DelayQueueDemo demo = delayQueue.poll(); // 獲取元素
            if (demo != null) { // 如果元素不為空
                System.out.println(System.currentTimeMillis() + "  取出成功"); // 列印取出成功資訊
            }
        }
    }

    /**
     * 構造方法, 初始化延遲時間, 設定為 3000 毫秒
     */
    public DelayQueueDemo() {
        this.timestamp = System.currentTimeMillis() + 3000;
    }

    /**
     * 物件到期時間, 單位毫秒, 超過到期時間則能被取出
     */
    long timestamp;

    /**
     * 獲取延遲時間, 單位毫秒, 超過到期時間則能被取出
     *
     * @param unit
     * @return
     */
    @Override
    public long getDelay(TimeUnit unit) {
        return this.timestamp - System.currentTimeMillis();
    }

    /**
     * 比較方法, 用於排序, 按照到期時間升序排列
     *
     * @param o
     * @return
     */
    @Override
    public int compareTo(Delayed o) {
        return (int) (this.timestamp - ((DelayQueueDemo) o).timestamp);
    }
}

控制檯輸出如下:

1713013980178  新增完成
1713013983178  取出成功
1713013983178  取出成功
1713013983178  取出成功

可以看出,新增完成之後,約 3 秒才被取出,符合我們延遲 3 秒的設定。基於 DelayQueue 的特性,我們可以發散一下思路,它完全可以用來做效能測試中日誌回放模型的佇列。我們將日誌的 URL 和時間戳進行繫結,將時間戳加上一個延遲,這樣就可以透過從延遲佇列取出到期日誌 URL,重新傳送請求。

這個日誌回放框架,會在 HTTP 協議效能測試章節中進行實戰,開發日誌回放功能並進行模擬日誌回放測試。

除此以外,在 Java 執行緒池等待佇列一章也介紹了幾個常用的執行緒安全佇列,這裡再提一下它們的名字:SynchronousQueue(長度為零阻塞佇列)、LinkedBlockingDeque(雙端阻塞佇列)和 PriorityBlockingQueue(優先順序阻塞佇列)。它們往往都直接或者間接實現 java.util.concurrent.BlockingQueue 介面,操作的 API 大同小異,掌握一種就能很快舉一反三,學會其他佇列使用。

書的名字:從 Java 開始做效能測試

如果本書內容對你有所幫助,希望各位不吝讚賞,讓我可以貼補家用。讚賞兩位數可以提前閱讀未公開章節。我也會嘗試製作本書的影片教程,包括必要的答疑。

FunTester 原創精華

【連載】從 Java 開始效能測試

  • 混沌工程、故障測試、Web 前端
  • 服務端功能測試
  • 效能測試專題
  • Java、Groovy、Go
  • 白盒、工具、爬蟲、UI 自動化
  • 理論、感悟、影片
如果覺得我的文章對您有用,請隨意打賞。您的支援將鼓勵我繼續創作!
打賞支援
暫無回覆。

相關文章