CAS与ConcurrentLinkedQueue的实现

CAS

CAS,compare and swap的缩写,意思为比较并交换。

从JDK 5 开始,Doug lea 给我们提供了Coucurrent包,让我们解决并发问题,CAS就是实现这个包的基础。

CAS包含三个操作数--内存位置,预期位置和新值,当且仅当内存位置的值与预期位置的值相同的时候,处理器才会把该位置的值修改为新值,否则,处理器不做任何操作。无论如何,处理器都会在CAS指令之前返回该位置的值。

CAS指令允许算法执行读-修改-写操作,不用担心其他线程同时修改变量,因为该变量只能同时只有一个线程可以修改,其他线程失败后可以重新读取在操作。

利用CAS指令,可以完成java的非组阻塞算法,synchronized是阻塞算法,整个并发包都是在CAS基础之上完成的,相比synchronized,J.U.C在性能上有了很大的提高,可以很高效的完成原子操作。


ConcurrentLinkedQueue

ConcurrentLinkedQueue是基于CAS实现的非阻塞的线程安全的队列,它遵循先入先出的原则。当添加一个元素的时候,它被添加到队列的尾部,当获取一个元素的时候,它会返回队列头部的元素。

ConcurrentLinkedQueue是由head和tail节点组成,每个节点都有item和指向下一个节点的指针,组成了链表结构的队列,默认的构造函数是head和tail为null。

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

下面看一下它的主要常用方法:

add方法

添加元素的过程主要做了两件事,第一是把要添加的节点作为当前队列尾节点的next节点,第二是把更新尾节点,如果尾节点的next节点不为空,就把新添的节点作为尾节点,如果尾节点的next为空,就把新添节点作为尾节点的next节点。

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)) {
                    if (p != t) // hop two nodes at a time
                        casTail(t, newNode);  // Failure is OK.
                    return true;
                }
            }
            else if (p == q)
                p = (t != (t = tail)) ? t : head;
            else
                // Check for tail updates after two hops.
                p = (p != t && t != (t = tail)) ? t : q;
        }
    }

1. 找到当前队列的尾节点,利用CAS把新增节点作为尾节点的下一个节点,保证最后一个节点是新添加的节点,其他失败的线程会更新p指向这个新节点.

2. 如果tail节点与p相距2个节点,修改tail指向.

3. 如果p的next节点指向自己的话,说明节点p已经被删除.

假设多个线程同时都要添加元素,第一次添加元素的时候,队列为空,全部线程都能执行到q=p.next=null,此时,只有一个线程才能利用CAS把p的next节点设为newNode,这时判断p==t,所以并不会利用CAS把newNode更新为tail节点,casNext方法执行成功,直接返回true.其它失败的线程走到最后一个else分支,把p指向newNode.这时,p的next肯定为空,第二个竞争成功执行的线程利用CAS把自己的节点放到newNode的next,由于t指向一个空节点,p指向newNode节点,所有p!=t,利用CAS把新添加的节点设为tail节点,依次类推,每隔两个节点更新一次tail节点,这样通过增加对volatile的读操作减少对volatile的写操作,写操作的开销要高于读操作,所以入队效率总体是提升的.

看一下p==q的情况,这是由于当前节点被删除导致的.next指向自己,这种节点没有任何价值,当遇到这种节点的时候,一般是返回头结点,从头节点开始遍历,找到tail节点.如果发生在执行过程中,tail被其他线程修改的情况,直接把tail节点作为尾节点,避免重新查找的开销.

p = (p != t && t != (t = tail)) ? t : q

这句代码中 != 并不是原子操作,程序执行的时候,先取得t的值,再执行t=tail,然后把tail赋值的t与原来的t进行比较值是否相等.     例如:

public static void main(String[] args) {
        int a = 1;
        int b = 2;
        System.out.println(a != (a = b));
    }

在多线程中,t != t才能成立,取得左边的t,右边的tail可能被其他线程修改,所以新的节点作为tail节点,把p指向最新的tail节点.

当队列为空的时候


当添加第一个元素之后,


当添加第二个元素之后,


头结点永远是一个空节点.,每当添加两个节点之后就要更新tail节点,依次这样循环添加.

poll方法

poll和add方法相似,并不是每次都更新头结点,当队列为空时,直接返回空,每经过读取两个节点更新头结点.

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;
            }
        }
    }

注意,head节点是一个空节点.

当队列为空时,代码执行到q=p.next=null这一条件判断,如果这时候线程没有添加元素的话,进入updateHead方法进行判定,

final void updateHead(Node<E> h, Node<E> p) {
        if (h != p && casHead(h, p))
            h.lazySetNext(h);
    }

此时h和p都是null,所以h != p 为false,直接返回null;如果这时在执行q=p.next=null之前,线程在队列中添加了一个元素,代码会进入p=q,把p指向p的next节点,此时p不为空,利用CAS把item设为null,这时p != h,根据条件把p作为当前链表的头节点,然后h节点的next指向自己,也就变成了哨兵节点,其它cas失败的线程进入(q=p.next)==null,直接返回null。

什么时候p=q那??

初始状态:


poll出e后:

        利用CAS把e置为null,p != h,所以更新头结点,把p.next设为头结点,这时候原来的head指向自己.


poll出f后:

   head节点不为空,直接把head的item设为空,p=head,直接返回item.          

poll出g后:

tail滞后于head,如果这时候添加一个元素h,在offer方法中代码走到p=q中,根据条件判断直接p指向head节点.


队列判空

public int size() {
        int count = 0;
        for (Node<E> p = first(); p != null; p = succ(p))
            if (p.item != null)
                // Collection.size() spec says to max out
                if (++count == Integer.MAX_VALUE)
                    break;
        return count;
    }

在判断队列为空的时候,并不建议使用size方法,主要是为了队列在高并发情况下的数据访问的正确性,由于遍历的时候有可能其他线程会对队列状态进行修改,所有数据有可能有错误,如果队列中节点较多的情况下,遍历所有的节点,性能会有较大影响,可以考虑isEmpty方法.

public boolean isEmpty() {
        return first() == null;
    }

总结

ConcurrentLinkedQueue是基于链表线程安全的非阻塞队列,采用先入先出的顺序对链表排序,当添加一个元素的时候直接加到队尾,当获取一个元素的时候,直接返回头部元素。通过CAS操作,在线程安全的前提下,提高了队列操作效率。很是佩服Doug lea 把这个类设计的如此精妙,ConcurrentLinkedQueue是研究CAS最好的类,通过无锁操作来实现了高并发.Doug lea的代码写的非常简洁,但是并不意味着你能理解.但是一旦掌握了它的核心思想,对于AQS的理解有很大帮助,所以知识的积累还是不断学习总结的过程.


猜你喜欢

转载自blog.csdn.net/qq_30572275/article/details/80260770
今日推荐