并发基础_11_并发_容器_ConcurrentLinkedQueue

来源:互联网 发布:python crash course 编辑:程序博客网 时间:2024/06/05 21:05

要实现一个线程安全的队列有两个方式:一种是使用阻塞算法,另一种是使用非阻塞算法。


阻塞算法:

使用阻塞算法的队列可以用一个锁(入队和出队同一把锁)或两把锁(入队和出队用不同的锁)来实现。

非阻塞的实现方式则可以使用循环CAS的方式来实现。



ConcurrentLinkedQueue非阻塞线程安全队列

ConcurrentLinkedQueue是一个基于链接节点的无界 线程安全的队列

它采用FIFO(先进先出)的规则对节点进行排序;

当我们添加一个元素的时候,它会添加到队列的尾部;

当我们获取一个元素时,它会返回队列头部的元素。

队列中不允许null元素。

判断队列成员是否为空,不要使用size()方法,使用isEmpty()方法,因为size()方法会遍历整个队列。


ConcurrentLinkedQueue由head节点和tail节点组成,每个节点(Node)由节点元素(item)和指向下一个节点的引用(next)组成;

节点与节点之间就是通过这个next关联起来,从而形成一个链表结构的队列。

我们看下源码中的参数:

private transient volatile Node<E> head;private transient volatile Node<E> tail;public ConcurrentLinkedQueue() {head = tail = new Node<E>(null);}

要是你看过HahsMap的源码,会发现,这个和Entry链表差不多..

默认情况下head节点存储的元素为空,tail节点等于head节点

我们看下Node的源码:

private static class Node<E> {volatile E item;volatile Node<E> next;/** * Constructs a new node. Uses relaxed write because item can only be * seen after publication via casNext. */Node(E item) {UNSAFE.putObject(this, itemOffset, item);}boolean casItem(E cmp, E val) {return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);}void lazySetNext(Node<E> val) {UNSAFE.putOrderedObject(this, nextOffset, val);}boolean casNext(Node<E> cmp, Node<E> val) {return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);}.....}



ConcurrentLinkedQueue入队操作

入队列就是将新添加的Node节点,添加到队列的尾部。

在没添加元素之前,head和tail都是Node自己(head节点)。

a. 添加元素1:队列更新head节点的next节点为"元素1节点",又因为tail节点默认情况下等于head节点,所以他们的next节点都指向"元素1节点"

b. 添加元素2:队列首先设置"元素节点1"的next节点为"元素2节点",然后更新tail节点指向"元素2节点"

c. 添加元素3:设置tail节点为元素3节点

d. 添加元素4:设置元素3的next节点为元素4节点,然后将tail节点指向元素4节点。


通过观察入队过程以及head节点和tail节点的变化,发现入队主要做两件事:

第一:将入队节点设置成当前队列尾节点的下一个节点。

第二:更新tail节点,如果tail节点的next节点不为空,则将入队节点设置成tail节点,如果tail节点的next节点为空,则将入队节点设置成tail的next节点,所以tail节点不总是尾节点。(理解这点很重要...)


(让tail节点永远作为队列的尾节点,这样实现的代码量非常少,而且逻辑清晰、易懂,但是有个缺点,每次都要使用循环CAS更新tail节点。

如果能减少CAS更新tail节点的次数,就能提高入队的效率;so,并不是每次节点入队后都将tail节点更新成尾节点;而且随着队列长度越来越大,

每次入队时定位尾节点的时间就越长,太消耗性能..)

我们来看下源码:

public boolean offer(E e) {// 检查传入参数是否为空checkNotNull(e);// 构造一个新的Node节点,入队节点final Node<E> newNode = new Node<E>(e);// t为tail节点,p为尾节点,默认相等,采用失败即重试的方式,直到入队成功for (Node<E> t = tail, p = t;;) {// 获取p的下一个节点qNode<E> q = p.next;// 如果q为null(p就是尾节点)if (q == null) {// p is last node,p是尾节点// 将入队节点newNode,设置为当前队列尾节点的next节点if (p.casNext(null, newNode)) {// 判断tail节点是不是尾节点,也可以理解为如果插入节点后,tail节点与p节点距离是否达到两个节点?if (p != t) // hop two nodes at a time// 如果tail不是尾节点,则将入队列节点(newNoed)设置为tail节点。casTail(t, newNode); // Failure is OK.return true;}}// 如果p和它的下一个节点相等。// 则说明p节点和q节点都为空,表示队列刚刚初始化,所以返回head节点else if (p == q)p = (t != (t = tail)) ? t : head;else// p有next节点,表示p的next节点是尾节点,则需要重新更新p后,将它指向next节点p = (p != t && t != (t = tail)) ? t : q;}}
(此源码来自JDK1.7,与原书有差别)

从源码角度来看,整个入队过程主要做两件事:


1. 定位出尾节点;

tail节点并不总是尾节点,所以每次入队都必须通过tail节点来找到尾节点。

尾节点可能是tail节点也可能是tail节点的next节点


2. 使用CAS算法将入队节点设置成尾节点的next节点,如不成功则重试。



ConcurrentLinkedQueue出队列

出队列就是从队列中返回一个节点元素,并清空该节点对元素的引用。

观察上图可以发现,并不是每次出队列时都更新head节点,当head节点有元素时,直接弹出head节点中的元素,而不会更新head节点。

只有当head节点中没有时,出队操作才会更新head节点。

主要是,尽量减少CAS更新head节点的消耗,这种做法可以提高出队的效率。



ConcurrentLinkedQueue使用Demo

public class ConcurrentLinkedQueueTest {private static ConcurrentLinkedQueue<Integer> queue = new ConcurrentLinkedQueue<Integer>();private static int count = 2;private static CountDownLatch latch = new CountDownLatch(count);public static void main(String[] args) throws Exception {long startTime = System.currentTimeMillis();ExecutorService service = Executors.newFixedThreadPool(4);ConcurrentLinkedQueueTest.offer();for (int i = 0; i < count; i++) {service.submit(new Pool());}latch.await();System.out.println("cost time : " + (System.currentTimeMillis() - startTime) + " ms");service.shutdown();}/** * 生产者 */public static void offer() {for (int i = 0; i < 10000; i++) {queue.offer(i);}}/** * 消费 *  * @author CYX * @time 2017年7月31日上午9:34:02 */static class Pool implements Runnable {@Overridepublic void run() {// 此处判断队列是否为空,不要使用size(),size()会将队列先遍历一遍,性能太差while (!queue.isEmpty()) {System.out.println("消费 : " + queue.poll());}latch.countDown();}}}

原创粉丝点击