Java 入门指南:Java 并发编程 —— 并发容器 ConcurrentLinkedQueue

文章目录

ConcurrentLinkedQueue

ConcurrentLinkedQueue 是Java中的一个并发队列实现,是一个基于链接节点(链表)的无界的线程安全非阻塞队列,适用于多线程环境下的并发访问,即多个线程访问同一个 collection 的场景。

ConcurrentLinkedQueue 采用了无锁 (lock-free)的算法实现,并且基于链表数据结构。它支持高效的并发插入和删除操作,以及无界的队列长度。

java 复制代码
public class ConcurrentLinkedQueue<E> 
		extends AbstractQueue<E> 
		implements Queue<E>, java.io.Serializable {}

ConcurrentLinkedQueue 继承了抽象类 AbstractQueue,实现了Queue 接口、Serializable 接口,表示其可以被序列化。

实现原理

ConcurrentLinkedQueue 是基于链表实现的,每个链表节点包含数据域 (item)和指针域 (next)。其中,item 用于存储数据,next 指向下一个节点。节点的 itemnext 属性都是用 volatile 修饰的,保证了其可见性。

内部节点

ConcurrentLinkedQueue 内部类 Node 类表示链表结点,用于存放元素,包含 item域 和 next域,item域 表示元素,next域 表示下一个结点,其利用了反射机制CAS机制来更新 item域 和 next域,保证原子性。

ConcurrentLinkedQueue 内部类 Node 的源代码

特点

  1. 非阻塞算法ConcurrentLinkedQueue 使用非阻塞算法实现,它允许多个线程同时进行插入和删除操作,而不会发生死锁或线程阻塞使用了反射机制和CAS机制来更新头节点和尾结点,保证原子性

  2. 无界队列ConcurrentLinkedQueue 没有固定的容量限制,可以根据需要动态扩容 ,由于没有容量限制,所以插入操作永远不会被阻塞

  3. FIFO 排序ConcurrentLinkedQueue 使用先进先出(FIFO)的行为模式,保证先插入的元素会先进行取出操作。

    队列的头部是队列中时间最长的元素。队列的尾部 是队列中时间最短的元素。新的元素插入到队列的尾部。

  4. 线程安全ConcurrentLinkedQueue 是线程安全的,多个线程可以同时进行插入和删除操作而不需要额外的同步措施。

  5. 支持高并发ConcurrentLinkedQueue 在高并发环境下表现优异,可以有效处理多线程同时对队列进行操作的情况,使用 CAS(Compare And Swap)操作来保证线程的安全性。

  6. 不支持阻塞操作ConcurrentLinkedQueue 不支持阻塞操作 ,如果队列为空时调用 poll() 方法将返回 null 而不是阻塞等待。

  7. 迭代器弱一致性 :由于 ConcurrentLinkedQueue 是并发数据结构,其迭代器的遍历顺序可能不一致或发生变化。因此在迭代时需要注意,并尽量避免修改队列。

  8. ConcurrentLinkedQueue 不允许插入 null 元素

常用方法

ConcurrentLinkedQueue 类提供了一系列方法用于操作并发链表队列。以下是常用的方法:

  1. add(E e):将指定元素添加到队列的尾部,如果队列已满,则抛出IllegalStateException 异常。

  2. offer(E e):将指定元素添加到队列的尾部,并返回是否成功。如果队列已满,返回 false。

  3. poll():移除并返回队列头部的元素,如果队列为空,则返回null。

  4. remove():移除并返回队列头部的元素,如果队列为空,则抛出NoSuchElementException 异常。

    remove()基于 Iterator 实现 的,如果在迭代期间使用 remove() 来删除元素,则可能导致元素遗漏或重复删除的问题。

    通常情况下,建议使用 poll()take() 方法来安全地获取队列元素并保证程序的稳定性。

  5. peek():返回队列头部的元素,如果队列为空,则返回null。

  6. element():返回队列头部的元素,如果队列为空,则抛出NoSuchElementException 异常。

  7. size():返回队列中的元素个数。

  8. isEmpty():判断队列是否为空。

  9. contains(Object o):判断队列是否包含指定元素。

  10. toArray():返回包含队列所有元素的数组。

  11. clear():清空队列,将队列置为空。

构造方法

  1. 创建一个空的 ConcurrentLinkedQueue。
java 复制代码
public ConcurrentLinkedQueue() {
    // 初始化头节点与尾结点
    head = tail = new Node<E>(null);
}
  1. 创建一个包含指定集合元素的 ConcurrentLinkedQueue,按照集合的迭代顺序进行插入。
java 复制代码
ConcurrentLinkedQueue(Collection<? extends E> collection)

延迟更新策略

如果让 tail 永远作为队列的队尾节点,大量的入队操作会导致每次都要执行 CAS 进行 tail 的更新,十分损耗性能。为优化性能,使用了延迟更新策略。

在更新操作时,源码中会有注释为:hop two nodes at a time ,每间隔 1 次(tail 和 队尾节点 的距离为 1)进行才利用 CAS 更新 tail。对于 head 同理。虽然这样设计会多出在循环中定位队尾节点,但总体来说读的操作效率要远远高于写的性能。

通过分析 ConcurrentLinkedQueue 中 offerpoll 的源码,发现tail 尾节点 和 head 头节点 是延迟更新的,两者更新触发时机为:

  • tail更新触发时机:当 tail 指向的节点的下一个节点为 null 的时候,只插入节点不更新 tail

    tail 指向的节点的下一个节点不为 null 的时候,会执行定位队列真正的队尾节点的操作,找到队尾节点后完成插入之后才会通过 casTail() 进行 tail 更新。

  • head更新触发时机: 当 head 指向的节点的 item域 不为 null 的时候,只删除节点不更新 head

    head 指向的节点的 item域 为 null 的时候,会执行定位队列真正的队头节点的操作,找到队头节点后完成删除之后才会通过 updateHead() 进行 head 更新。

使用场景

ConcurrentLinkedQueue 特别适用于以下场景:

  1. 高并发场景:由于 ConcurrentLinkedQueue 采用了无锁编程技术,它在高并发环境下的性能表现非常出色。

  2. 需要快速插入和删除的场景:队列的头部和尾部都可以进行快速的插入和删除操作。

  3. 无界队列场景:与有界队列不同,ConcurrentLinkedQueue 可以存储任意数量的元素,适用于不需要限制队列大小的场景。

ConcurrentLinkedQueue 使用示例

以下是一个简单的 ConcurrentLinkedQueue 使用示例,展示了如何创建一个队列,并在其上执行基本的操作:

java 复制代码
import java.util.concurrent.ConcurrentLinkedQueue;

public class ConcurrentLinkedQueueExample {

    public static void main(String[] args) {
        // 创建 ConcurrentLinkedQueue
        ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();

        // 添加元素
        queue.offer("Apple");
        queue.offer("Banana");
        queue.offer("Cherry");

        // 打印队列
        System.out.println("队列初始状态: " + queue);

        // 创建线程向队列中添加元素
        Thread addThread = new Thread(() -> {
            queue.offer("Durian");
            queue.offer("Elderberry");
            System.out.println("添加元素后的队列: " + queue);
        });

        // 创建线程从队列中取出元素
        Thread pollThread = new Thread(() -> {
            while (!queue.isEmpty()) {
                String item = queue.poll();
                if (item != null) {
                    System.out.println("取出元素: " + item);
                }
            }
        });

        // 启动线程
        addThread.start();
        pollThread.start();

        try {
            // 等待线程结束
            addThread.join();
            pollThread.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            System.err.println("主线程被中断");
        }

        // 打印最终队列状态
        System.out.println("最终队列状态: " + queue);
    }
}

注意事项

在使用 ConcurrentLinkedQueue 的过程中,应注意以下问题:

  1. 弱一致性ConcurrentLinkedQueue 提供的是弱一致性保证,这意味着如果存在多个线程并发操作队列,队列的顺序可能会发生变化。
  2. 非阻塞特性ConcurrentLinkedQueuepoll 方法在队列为空时会立即返回 null,而不是阻塞等待。这意味着如果你需要等待队列中有元素,应该考虑使用 BlockingQueue
  3. 迭代器快照ConcurrentLinkedQueueiterator 方法返回的是一个快照迭代器,不会反映迭代期间队列的变化。如果需要实时迭代队列中的元素,可以考虑使用其他机制,如轮询。
  4. 内存回收ConcurrentLinkedQueue 中的节点在没有引用指向时会被垃圾回收。因此,如果队列中的元素不再被引用,相应的节点也会被回收。
相关推荐
m0_571957581 小时前
Java | Leetcode Java题解之第543题二叉树的直径
java·leetcode·题解
一点媛艺2 小时前
Kotlin函数由易到难
开发语言·python·kotlin
姑苏风2 小时前
《Kotlin实战》-附录
android·开发语言·kotlin
奋斗的小花生3 小时前
c++ 多态性
开发语言·c++
魔道不误砍柴功3 小时前
Java 中如何巧妙应用 Function 让方法复用性更强
java·开发语言·python
NiNg_1_2343 小时前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
闲晨3 小时前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
老猿讲编程3 小时前
一个例子来说明Ada语言的实时性支持
开发语言·ada
Chrikk4 小时前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*4 小时前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go