java并发包-ConcurrentLinkedQueue

ConcurrentLinkedQueue和LinkedBlockingQueue区别

ConcurrentLinkedQueueLinkedBlockingQueue都是Java中用于实现队列数据结构的类,但它们在一些方面有着不同的特点和适用场景。

  1. 并发性能:

    • ConcurrentLinkedQueue:这是一个非阻塞的无界队列。它使用无锁算法来实现高并发性能,适用于多线程环境。插入和删除操作的性能非常好,但是没有提供阻塞操作,如果队列为空,获取操作会返回null
    • LinkedBlockingQueue:这是一个阻塞的可选有界或无界队列。它使用锁来实现线程安全,支持阻塞式的插入和删除操作,可以设置最大容量。当队列为空时,获取操作会阻塞线程,直到队列非空。
  2. 容量:

    • ConcurrentLinkedQueue:是一个无界队列,因此它可以无限制地增长。
    • LinkedBlockingQueue:可以选择是有界队列还是无界队列。在创建时可以指定最大容量,如果是有界队列,在达到容量上限时插入操作会阻塞。
  3. 阻塞行为:

    • ConcurrentLinkedQueue:不提供阻塞操作,获取操作会返回null
    • LinkedBlockingQueue:支持阻塞操作,包括在队列为空时等待元素的获取操作,或在队列已满时等待空间的插入操作。
  4. 迭代器支持:

    • ConcurrentLinkedQueue:提供弱一致性的迭代器,可以在并发情况下使用,但不能保证迭代器的结果是实时更新的。
    • LinkedBlockingQueue:提供强一致性的迭代器,保证迭代器的结果是实时更新的。

选择使用哪个队列取决于你的具体需求。如果需要高并发性能且不需要阻塞操作,可以选择ConcurrentLinkedQueue。如果需要支持阻塞操作、有界队列或强一致性的迭代器,可以选择LinkedBlockingQueue


看名字就知道,最大的区别就是非阻塞。都是基于链表实现队列,已经有了LinkedBlockingQueue,那为什么还要ConcurrentLinkedQueue?原因就是因为这个是非阻塞。

源码分析

入队

入口

java 复制代码
/**
 * Inserts the specified element at the tail of this queue.
 * As the queue is unbounded, this method will never throw
 * {@link IllegalStateException} or return {@code false}.
 *
 * @return {@code true} (as specified by {@link Collection#add})
 * @throws NullPointerException if the specified element is null
 */
public boolean add(E e) {
    return offer(e);
}

入队:基于cas

scss 复制代码
/**
 * Inserts the specified element at the tail of this queue.
 * As the queue is unbounded, this method will never return {@code false}.
 *
 * @return {@code true} (as specified by {@link Queue#offer})
 * @throws NullPointerException if the specified element is null
 */
public boolean offer(E e) {
    checkNotNull(e);
    final Node<E> newNode = new Node<E>(e);

    for (Node<E> t = tail, p = t;;) { //循环
        Node<E> q = p.next;
        if (q == null) {
            // p is last node
            //入队
            if (p.casNext(null, newNode)) {
                // Successful CAS is the linearization point
                // for e to become an element of this queue,
                // and for newNode to become "live".
                if (p != t) // hop two nodes at a time
                    casTail(t, newNode);  // Failure is OK.
                return true;
            }
            // Lost CAS race to another thread; re-read next
        }
        else if (p == q)
            // We have fallen off list.  If tail is unchanged, it
            // will also be off-list, in which case we need to
            // jump to head, from which all live nodes are always
            // reachable.  Else the new tail is a better bet.
            p = (t != (t = tail)) ? t : head;
        else
            // Check for tail updates after two hops.
            p = (p != t && t != (t = tail)) ? t : q;
    }
}

不是直接入队吗?为什么要for循环呢?

这段代码实现了ConcurrentLinkedQueueoffer方法,用于将元素插入队列。虽然看起来有一个for循环,但实际上它是在一个自旋操作中,用于尝试在队列尾部插入新节点。

在这个循环中,代码尝试使用CAS(Compare and Swap)操作插入新节点,以确保在多线程环境中线程安全。这里有一些原因解释为什么要使用循环:

  1. 无锁算法: ConcurrentLinkedQueue使用无锁算法,所以这段代码中没有使用传统的锁机制,而是依赖于CAS操作来保证并发安全。

  2. 竞态条件: 多个线程可能同时尝试在队列尾部插入节点。如果一个线程正在执行CAS操作,另一个线程也可能在同一时间尝试插入。循环能够处理这种情况,因为只有一个线程能够成功地更新队列的尾部。

  3. 节点更新: 如果在循环的过程中发现当前节点的下一个节点为空(即q == null),那么它尝试通过CAS将新节点插入到队列尾部。如果CAS成功,那么该线性化点保证了插入的成功,然后可能需要更新队列的尾节点。

  4. CAS失败: 如果CAS操作失败(例如,由于竞争导致的失败),循环将重新尝试,确保在插入操作成功之前不会退出。

总之,这个循环的目的是确保在并发情况下,只有一个线程能够成功地将新节点插入队列尾部,从而保证了队列的一致性和线程安全性。


如果CAS操作失败,一直循环吗?

是的,如果CAS操作失败,循环将一直尝试重新执行。这种循环的设计被称为"自旋"操作。在这种情况下,自旋循环是为了处理并发情况下的竞态条件。

CAS(Compare and Swap)操作是一种无锁的并发操作,它允许线程尝试将一个变量的值更新为新值,但只有在变量的当前值与预期值匹配时才会成功。如果CAS操作失败,意味着其他线程已经修改了该变量的值,可能是在当前线程读取值和尝试更新值之间发生的。为了处理这种情况,通常会采用自旋的方法,即不断尝试CAS操作,直到成功或者达到一定的尝试次数。

在上述提供的代码中,如果CAS操作在插入新节点时失败,循环会继续执行,重新尝试CAS操作。这种自旋的方式可以在短时间内解决竞态条件,避免了使用传统的锁机制所带来的性能开销。

然而,需要注意的是,过多的自旋可能会浪费CPU资源,因此在实际使用中需要权衡。通常情况下,自旋的次数会有限制,如果达到了限制次数仍然没有成功,可能会采取其他策略,如线程让步(yield)或者阻塞一小段时间后再尝试。这样可以避免无限制的自旋。


一直循环和阻塞有啥区别?

一直循环(Spin)和阻塞是两种处理并发竞态条件的不同策略,它们在性能、资源利用以及响应性方面有所不同。

1. 一直循环(Spin): 在一直循环中,线程会不断尝试某个操作,通常是自旋操作,直到操作成功或达到一定的尝试次数。这种方法避免了线程的上下文切换和阻塞,因此响应速度较快,特别适用于短时间内竞态条件可能解决的情况。

然而,一直循环也有一些缺点:

  • 高CPU利用率:由于线程在循环中消耗CPU时间,会占用大量的CPU资源,可能会浪费电力并导致系统其他任务性能下降。
  • 忙等待:一直循环可能会导致忙等待,即线程一直在做相同的操作,但没有实际进展。这可能会降低系统效率。

2. 阻塞: 在阻塞策略中,线程在无法继续执行的情况下会被挂起,不会占用CPU资源,直到某个特定条件满足或者等待时间结束。阻塞可以减少CPU资源的消耗,适用于长时间等待的情况。

然而,阻塞也有一些缺点:

  • 响应时间:线程被阻塞后需要等待解除阻塞才能继续执行,响应时间可能会相对较长。
  • 上下文切换开销:线程从阻塞状态切换回运行状态需要一定的上下文切换开销,这可能影响性能。

选择哪种策略取决于场景和需求:

  • 当竞态条件短时间内可能解决,并且对响应速度要求较高时,一直循环可能更合适。
  • 当需要等待的时间较长,或者为了减少CPU资源占用时,阻塞可能更合适。

实际中,通常会综合考虑,使用一些优化方法,如自适应自旋、退避策略等,来在不同情况下选择合适的处理策略,以获得最佳性能和资源利用。


ConcurrentLinkedQueue本来是非阻塞,但是一直循环,那和阻塞有啥区别?

你提到的问题非常重要。实际上,ConcurrentLinkedQueue 的设计初衷是为了提供一种非阻塞的并发队列实现,以避免阻塞操作可能带来的性能问题。然而,如果在使用 ConcurrentLinkedQueue 时过于频繁地进行一直循环(spin),它的行为可能会类似于阻塞,因为循环会占用 CPU 资源。

这里需要强调的是,ConcurrentLinkedQueue 中的循环是为了实现无锁的并发操作,尽量避免线程的阻塞和上下文切换。这种自旋的方式可以在短时间内解决并发竞态条件。然而,如果循环的自旋时间过长,就会导致类似于阻塞的情况,即一些线程在自旋等待时会占用 CPU 资源,导致其他任务无法得到足够的执行时间,从而影响整体性能。

阻塞和一直循环之间的主要区别在于资源的占用情况:

  • 阻塞: 在阻塞的情况下,线程会释放 CPU 资源,让出时间片给其他任务,不会占用 CPU。这样可以确保其他任务有机会执行,但可能会带来线程上下文切换的开销。

  • 一直循环: 在一直循环中,线程一直占用 CPU 资源,不会让出时间片,因此不会引发线程上下文切换的开销。然而,如果循环过于频繁和耗时,会导致 CPU 资源浪费,其他任务无法得到充分执行。

综上所述,ConcurrentLinkedQueue 被设计为一种无锁的并发队列,它的自旋操作旨在短时间内解决并发问题。然而,过度的一直循环可能会导致类似于阻塞的问题,因此在使用时需要权衡自旋次数以及自旋的适当时间,以避免资源浪费和性能问题。


一直循环不会很多线程卡死吗?

是的,你提到的是一直循环的一个重要问题,即可能导致线程卡死的情况。这种情况被称为"活锁"(Livelock)。

活锁发生在多个线程相互响应彼此的动作,但是它们却无法继续向前推进。在一直循环中,如果多个线程同时在竞态条件中自旋,它们可能会不断尝试相同的操作,但由于竞争,每次操作都会失败。这会导致线程不断重复相同的操作,而没有实际进展。

为了避免活锁,通常会采取一些策略,例如:

  • 自适应自旋: 在自旋过程中,可以引入一些策略,如逐渐增加自旋次数,或者在自旋一段时间后尝试阻塞。这可以避免线程一直在高频率自旋而导致卡死。
  • 退避策略: 当线程在自旋中失败时,可以让线程稍微休眠一段时间,以便让其他线程有机会执行。然后,线程可以逐渐增加休眠时间,以防止一直自旋。
  • 随机性: 引入随机性可以避免多个线程在同一时间尝试相同的操作。例如,在自旋等待时引入随机的小延迟,以使得线程的操作时间不完全一致。

总之,一直循环在并发编程中需要谨慎使用,并结合适当的策略,以避免活锁和高CPU利用率问题。在某些情况下,使用阻塞等待会更加合适,因为它不会导致线程一直占用CPU资源,从而降低了活锁的风险。


一直循环,不会导致很多线程空转,最后耗尽线程池吗?

是的,你提到的情况是可能发生的。在并发编程中,如果使用一直循环且没有适当的控制机制,会导致线程空转(busy-waiting)并最终耗尽线程池或处理器资源。这会导致系统性能下降甚至崩溃。

一直循环可能导致的问题包括:

  1. 高CPU利用率: 由于线程在循环中一直消耗CPU时间,会占用大量的CPU资源,导致其他任务无法得到充分的执行时间。这可能会导致系统整体性能下降。

  2. 线程池耗尽: 如果线程池中的线程都在一直循环中,没有机会释放资源,线程池可能会被耗尽,无法再为其他任务提供服务。

  3. 系统负载增加: 高CPU利用率会导致系统负载增加,从而增加了硬件资源的消耗,可能使系统变得不稳定。

为了避免这些问题,如果使用一直循环,应该采取一些策略来控制循环的执行:

  • 自适应自旋: 在循环中可以引入自适应的策略,逐渐增加自旋次数,或者在自旋一段时间后尝试阻塞。这可以减少线程一直在高频率自旋而导致的问题。

  • 退避策略: 当循环失败时,可以引入退避策略,让线程休眠一段时间,然后逐渐增加休眠时间。这可以减少CPU资源的占用。

  • 随机性: 引入随机性可以避免多个线程在同一时间尝试相同的操作。在循环等待时引入随机的小延迟,使得线程的操作时间不完全一致。

综上所述,一直循环需要谨慎使用,必须结合适当的控制策略,以避免资源耗尽和性能问题。在某些情况下,使用阻塞等待会更加合适,因为它可以避免线程一直占用CPU资源。

出队

kotlin 复制代码
public E poll() {
    restartFromHead:
    for (;;) {
        for (Node<E> h = head, p = h, q;;) {
            E item = p.item;

            //出队
            if (item != null && p.casItem(item, null)) {
                // Successful CAS is the linearization point
                // for item to be removed from this queue.
                if (p != h) // hop two nodes at a time
                    updateHead(h, ((q = p.next) != null) ? q : p);
                return item;
            }
            else if ((q = p.next) == null) {
                updateHead(h, p);
                return null;
            }
            else if (p == q)
                continue restartFromHead;
            else
                p = q;
        }
    }
}
相关推荐
陌殇殇40 分钟前
002 SpringCloudAlibaba整合 - Feign远程调用、Loadbalancer负载均衡
java·spring cloud·微服务
猎人everest2 小时前
SpringBoot应用开发入门
java·spring boot·后端
山猪打不过家猪4 小时前
ASP.NET Core Clean Architecture
java·数据库·asp.net
AllowM4 小时前
【LeetCode Hot100】除自身以外数组的乘积|左右乘积列表,Java实现!图解+代码,小白也能秒懂!
java·算法·leetcode
不会Hello World的小苗4 小时前
Java——列表(List)
java·python·list
二十七剑5 小时前
jvm中各个参数的理解
java·jvm
东阳马生架构6 小时前
JUC并发—9.并发安全集合四
java·juc并发·并发安全的集合
计算机小白一个7 小时前
蓝桥杯 Java B 组之岛屿数量、二叉树路径和(区分DFS与回溯)
java·数据结构·算法·蓝桥杯
菠菠萝宝7 小时前
【Java八股文】10-数据结构与算法面试篇
java·开发语言·面试·红黑树·跳表·排序·lru
不会Hello World的小苗7 小时前
Java——链表(LinkedList)
java·开发语言·链表