由于链表的特性,因此可以知道其进行添加元素或者删除元素只需要进行节点的改变即可,同时不需要考虑扩容和缩容的问题,比较方便。那实现队列,需要的是生产者和消费者的模型。其本质是执行进队和出队操作。 下面的代码源于https://github.com/takumiCX/beautiful-concurrent。
那么怎样才能实现一个并发无锁的队列呢?首先需要考虑队列是基于链表的,因此我们能操作它的前驱节点和后继节点,同时对数据需要存储到队列中,然后进行消费。为了解决并发问题,我们采用乐观锁+volatile的方式实现。为了测试效果,代码中使用了同步并发容器CountDownLatch测试。
入队考虑:
当前队列为空时,直接将当前节点设置成尾节点
当前队列不为空时,将原来的尾节点设置未当前节点的前驱节点
出队考虑:
如果当前队列为空,则将返回空
如果当前队列不为空,有一个元素,则直接移除,将当前头节点设置为空
如果当前队列有多个元素,此时考虑将头节点进行置为空,此时首先拿到头节点的下一个节点,同时将其的前驱节点(也即头节点)设置为空
因此它的数据结构:
前驱节点、后继节点、当前队列存储的数据
/**
* 链表节点的定义
*
* @param <E>
*/
private static class Node<E> {
// 指向前一个节点的指针
public volatile Node pre;
// 指向后一个结点的指针
public volatile Node next;
// 真正要存储在队列中的值
public E item;
public Node(E item) {
this.item = item;
}
@Override
public String toString() {
return "Node{" + "item=" + item + '}';
}
}
由于我们的数据存储时,需要有先后位置,此时需要考虑到队列有队头和队尾,因此需要将其定义出来:
// 指向队列头结点的原子引用
private AtomicReference<Node<E>> head = new AtomicReference<>(null);
// 指向队列尾结点的原子引用
private AtomicReference<Node<E>> tail = new AtomicReference<>(null);
实现队列的入队操作,如果当前的队列为空,则首先获取队尾的值,执行cas比较,null与当前队尾的值比较,然后执行队列入队操作,如果不为空,则首先将原来的队尾的数据赋给当前节点的前驱节点,同时比较队尾节点节点和当前节点,再进行更新:
public boolean inQueue(E e){
//创建队列
Node<E> newNode = new Node<>();
//进行自旋
for(;;) {
//获取当前的头尾节点
Node<E> tailed = tail.get();
Node<E> headed = head.get();
if(tailed == null) {
//考虑当前是第一个入队的数据
if(head.compareAndSet(null,newNode)){
tail.set(newNode);
return true;
}
}else{
//考虑队列不为空情况
newNode.pre = tailed;
if(taild.compareAndSet(taild,newNode)){
tail.next = newNode;
return true;
}
}
}
}
进行出队操作:首先如果当前队列为空,则直接返回空,如果当前的队列不为空,则查看当前的头尾节点是否相等,如果相等,则数据只有一个数据,直接将头设置为空,否则说明有多个数据,此时直接进行前移操作
public E dequeue(){
for(;;){
//当前头节点
Node<E> tailed = tail.get();
Node<E> headed = head.get();
if(tailed == null){
//无元素情况
return null;
}else if(headed == tailed){
//当前有一个元素情况
if(tailed.compareAndSet(tailed,null)){
head.set(null);
return head.item;
}
}else{
//当前有多个元素情况
Node headNext = head.next;
if(headNext != null && head.compareAndSet(head,headNext)){
headNext.pre = null;
return head.item;
}
}
}
}
使用并发容器CountDownLatch进行测试:
private long singleLinkedQueue() throws InterruptedException {
final LockFreeQueue<String> queue = new LockFreeSingleLinkedQueue<>();
final CountDownLatch latch = new CountDownLatch(threadNum);
long start = System.currentTimeMillis();
for (int i = 0; i < threadNum; i++) {
final int finalI = i;
new Thread(new Runnable() {
public void run() {
for (int j = 0; j < queueOpNum; j++) {
if (j % 2 == 0) {
//入队操作
queue.enqueue(finalI + "-" + j);
} else {
//出队操作
queue.dequeue();
}
}
latch.countDown();
}
}).start();
}
latch.await();
long end = System.currentTimeMillis();
return (end - start);
}
基于链表实现无锁队列或者基于栈实现无锁并发栈的本质是类似的。
但是这样的无锁队列适合大容量场景吗?
对于无锁队列的链表模式:
无锁队列的链表模式采用Head/Tail头尾指针指向一个包含Next指针的结构体。
为了解决Head/Tail的互斥问题,Head/Tail必须指向一个Dummy结构(空队列时Head=Tail 指向Dummy)