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 原创精华