FunTester 【连载 12】线程安全的集合类

FunTester · 2025年01月18日 · 73 次阅读

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 开始性能测试

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册