ConcurrentLinkedQueue和LinkedBlockingQueue区别
ConcurrentLinkedQueue
和LinkedBlockingQueue
都是Java中用于实现队列数据结构的类,但它们在一些方面有着不同的特点和适用场景。
-
并发性能:
ConcurrentLinkedQueue
:这是一个非阻塞的无界队列。它使用无锁算法来实现高并发性能,适用于多线程环境。插入和删除操作的性能非常好,但是没有提供阻塞操作,如果队列为空,获取操作会返回null
。LinkedBlockingQueue
:这是一个阻塞的可选有界或无界队列。它使用锁来实现线程安全,支持阻塞式的插入和删除操作,可以设置最大容量。当队列为空时,获取操作会阻塞线程,直到队列非空。
-
容量:
ConcurrentLinkedQueue
:是一个无界队列,因此它可以无限制地增长。LinkedBlockingQueue
:可以选择是有界队列还是无界队列。在创建时可以指定最大容量,如果是有界队列,在达到容量上限时插入操作会阻塞。
-
阻塞行为:
ConcurrentLinkedQueue
:不提供阻塞操作,获取操作会返回null
。LinkedBlockingQueue
:支持阻塞操作,包括在队列为空时等待元素的获取操作,或在队列已满时等待空间的插入操作。
-
迭代器支持:
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循环呢?
这段代码实现了ConcurrentLinkedQueue
的offer
方法,用于将元素插入队列。虽然看起来有一个for
循环,但实际上它是在一个自旋操作中,用于尝试在队列尾部插入新节点。
在这个循环中,代码尝试使用CAS(Compare and Swap)操作插入新节点,以确保在多线程环境中线程安全。这里有一些原因解释为什么要使用循环:
-
无锁算法:
ConcurrentLinkedQueue
使用无锁算法,所以这段代码中没有使用传统的锁机制,而是依赖于CAS操作来保证并发安全。 -
竞态条件: 多个线程可能同时尝试在队列尾部插入节点。如果一个线程正在执行CAS操作,另一个线程也可能在同一时间尝试插入。循环能够处理这种情况,因为只有一个线程能够成功地更新队列的尾部。
-
节点更新: 如果在循环的过程中发现当前节点的下一个节点为空(即
q == null
),那么它尝试通过CAS将新节点插入到队列尾部。如果CAS成功,那么该线性化点保证了插入的成功,然后可能需要更新队列的尾节点。 -
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)并最终耗尽线程池或处理器资源。这会导致系统性能下降甚至崩溃。
一直循环可能导致的问题包括:
-
高CPU利用率: 由于线程在循环中一直消耗CPU时间,会占用大量的CPU资源,导致其他任务无法得到充分的执行时间。这可能会导致系统整体性能下降。
-
线程池耗尽: 如果线程池中的线程都在一直循环中,没有机会释放资源,线程池可能会被耗尽,无法再为其他任务提供服务。
-
系统负载增加: 高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;
}
}
}