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 個發現:
-
Vector
和ArrayList
方法名稱和引數一模一樣,原因是它們都實現了java.util.List
介面。 -
Vector
類採用synchronized
關鍵字修飾操作方法實現執行緒安全。
掌握這兩點,我們就已經掌握了 Vector
基本功能,實際使用語法與 ArrayList
更是完全通用的。
2.7.2 Map 對映
java.util.Map
是 Java 基礎中集合框架中的一個介面。它用於儲存鍵值對(key-value)資料,每一個鍵(key)都唯一地對映到一個值(value),提供對資料的增、刪、改、查操作。Java SDK 中提供了多個實現了 java.util.Map
介面的功能類,其中最常用的是 java.util.HashMap
。下面繼續介紹它線上程安全平行時空的好兄弟。
ConcurrentHashMap
與 HashMap
功能和呼叫方法均是相同的,這一點與上一節 Vector
和 ArrayList
是一致的。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 自動化
- 理論、感悟、影片