并行化资源池队列 2 —— 无锁化的无界队列

摘要

Java™ 5.0 第一次让使用 Java 语言开发非阻塞算法成为可能,java.util.concurrent 包充分地利用了这个功能。

Java™ 5.0 第一次让使用 Java 语言开发非阻塞算法成为可能,java.util.concurrent 包充分地利用了这个功能。非阻塞算法属于并发算法,它们可以安全地派生它们的线程,不通过锁定派生,而是通过低级的原子性的硬件原生形式 —— 例如比较和交换。非阻塞算法的设计与实现极为困难,但是它们能够提供更好的吞吐率,对生存问题(例如死锁和优先级反转)也能提供更好的防御。

在不只一个线程访问一个互斥的变量时,所有线程都必须使用同步,否则就可能会发生一些非常糟糕的事情。Java 语言中主要的同步手段就是 synchronized 关键字(也称为内置锁),它强制实行互斥,确保执行synchronized 块的线程的动作,能够被后来执行受相同锁保护的 synchronized 块的其他线程看到。在使用得当的时候,内置锁可以让程序做到线程安全,但是在使用锁定保护短的代码路径,而且线程频繁地争用锁的时候,锁定可能成为相当繁重的操作。

原子变量提供了原子性的读-写-修改操作,可以在不使用锁的情况下安全地更新共享变量。原子变量的内存语义与volatile 变量类似,但是因为它们也可以被原子性地修改,所以可以把它们用作不使用锁的并发算法的基础。基于这些原子化操作构建起来的并发控制算法称为无锁化非阻塞算法,所谓无锁化其实并不是不加锁,只是所加的锁粒度极小(指令级别或微指令级别),因此从程序本身的宏观角度来看,就好象不使用锁进行并发控制一样,正因为如此无锁化操作,需要在CPU指令级别提供基础支持,当前较新的CPU芯片都提供了原子化的CMPXHG指令,来实现原子化操作的支持。在Java平台上所提供的原子化操作API,如果宿主系统提供原子化指令,那么Java的原子化操作就会使用原子化指令实现原子化操作,反之Java的原子化操作会采用细粒度自旋锁来实现,当然所有这些对于使用者都是透明化的。如果深入 JVM 和操作系统,会发现非阻塞算法无处不在。如垃圾收集器使用非阻塞算法加快并发和平行的垃圾搜集;调度器使用非阻塞算法有效地调度线程和进程等。非阻塞算法要比基于锁的算法复杂得多。开发非阻塞算法是相当专业的训练,而且要证明算法的正确也极为困难。但是在 Java 版本之间并发性能上的众多改进来自对非阻塞算法的采用,而且随着并发性能变得越来越重要,可以预见在 Java 平台的未来发行版中,会使用更多的非阻塞算法。

采用非阻塞思想来实现无锁化并行队列,其最难理解的核心思想部分是“通过让较快的线程帮助较慢的线程来防止方法调用陷入饥饿”,其次是要一直意识到非阻塞算法并不是不使用锁,而是使用粒度极小的原子锁。因此与前面的算法实现一样,首先要构建队列节点元素,但节点的next域采用原子化引用AtomicReference来直向下一个元素节点,因此队列本身是一个包含哨兵头结点和尾节点,以及由AtomicReference串接起来的队列,同时每个元素节点还包含各自的节点值,这里每个节点值采用Java泛型化类型表示。队列节点定义代码如下:

import java.util.concurrent.atomic.AtomicReference;

publicclassNoLockNode<E> {

   finalEvalue;
   finalAtomicReference<NoLockNode<E>>next;
   publicNoLockNode(E item,NoLockNode<E> next){
       this.value=item;
       this.next=newAtomicReference<NoLockNode<E>>(next);
   }

}

然后定义队列的主体实现,其中主要包括:入队和出对操作。

import java.util.concurrent.atomic.AtomicReference;
publicclassNoLockQueue<E> {

  //头尾哨兵节点
  privateAtomicReference<NoLockNode<E>>head,tail=null;
  //构造函数初始化哨兵节点,让头尾指针相等,进而构造空队列
  publicNoLockQueue(){

      head=newAtomicReference<NoLockNode<E>>(new NoLockNode<E>(null,null));
      tail=head;
  }

//入队实现方法
  publicvoidenq(E value){
//创建将要入队的新节点
      NoLockNode<E> node=newNoLockNode<E>(value,null);
      while(true){
//通过尾节点获取链表最后一个元素节点,以及该元素节点的后继结点。因为入//队操作要从队尾进行
         NoLockNode<E> last=tail.get();
         NoLockNode<E> next=last.next.get();
//判断最后一个节点是否为尾节点,此判断通过只说明最后一个节点为“疑似尾//节点”,并不能最后定论,因为线程在运行过程中并没有加锁控制,是无锁化//运行的,因此在任何时刻都可能改变上一时刻的状态
         if(last==tail.get()){
//所以还要增加判断疑似尾节点的后继节点,以便验明正身其确为尾节点。如果//疑似尾节点的后继节点不为空,那说明有后继结点,说明上一个判断的状态被//打破(不管是如何被打破的,因为在并发情况下原因会很多),所以停止本次//尝试开始新一次尝试;如果疑似尾节点的后继节点为空,那说明没有后继结点,//此时尝试通过原子化的CAS操作添加新节点
            if(next==null){
//调用CAS操作在最后一个节点的后继上添加新节点,如果添加失败,说明被其//他线程抢先添加了新节点,所以停止本次尝试并开始一次新的尝试
               if(last.next.compareAndSet(next,node)){
//如果成功添加了新的节点,线程要第二次调用CAS操作,以便使tail指向新节//点。这里是“帮助”思想的体现,这个调用可能会失败,不过无所谓,因为即//便失败线程也能成功返回,因为这个调用只有在其他某个线程已经设置了//tail节点来“帮助”了本线程时才可能失败
                   tail.compareAndSet(last,node);
                   return;
               }
            }
         }

//如果最后一个元素节点还有后继节点,那么要方法要在插入新节点之前,调整//tail指向最后一个节点的后继节点,这就是“帮助”思想的一个体现。这里//通过调整tail,来尝试帮助其他线程。因为此时其他线程可能刚刚插入新节//点,同时还没有来得及调整tail,因此当前这个入队线程检测到这种情况发//生后,要主动尝试协助已经成功添加入队元素的线程调整tail。因为采用原//子化操作调整tail因此无需等待,所以该入队方法在这一点上实现了线性化。//(这是原子化操作其实是粒度极小的指令级别锁的体现)
          else{
            tail.compareAndSet(last,next);
         }
      }
  }

//出队方法
  publicE deq()throwsException{

      E result=null;
      while(true){
//通过头节点获取链表第一个元素节点,以及该元素节点的后继结点。因为出//队操作要从队首进行,同时还要通过队尾节点获取最后一个节点,用于进行相//关的判断
         NoLockNode<E> first=head.get();
         NoLockNode<E> last=tail.get();
         NoLockNode<E> next=first.next.get();

//判断首节点是否为头节点
         if(first==head.get()){

//判断首节点是否指向最后一个节点,如果是则队列可能为空
            if(first==last){

//如果head等于tail,同时head后继为空,说明队列为空,那么抛出相关异常,//结束操作
               if(next==null){

                   thrownewException("Empty Queue!!!!");
               }

//如果head等于tail,同时head后继不为空,说明队列为非空,同时说明tail
//的调整滞后了,因为此时可能由于并发的其他线程成功的入队后,但还没有调//整tail,这时出对线程开始了运行,所以同样需要出队线程来“帮助”调整//tail节点
               tail.compareAndSet(last,next);
            }
         }

//如果首节点没有指向头节点,说明头节点说明头节点已经被其他线程改变,那//么要取出首节点的节点值,同时尝试设置头节点指向首节点的后继节点,如果//设置成功,则返回节点值,即出队成功
          else{
//取出首节点值
            result=next.value;
//尝试设置头节点指向首节点的后继节点,如果设置成功,则返回节点值,即出//队成功
            if(head.compareAndSet(first,next))
               return result;
         }
      }
  }
}

很容易看出这种实现的队列是无锁的。每个方法调用首先找出一个未完成的入队操作,然后尝试完成它。最坏的情形下,所有的线程都试图移动队列的tail域,且其中之一必须成功。仅当另一个线程的方法调用在改变引用中获得成功时,一个线程才可能在入队或出队一个节点时失败,因此总会有某个方法调用成功。这种无锁化的实现从本质上改进了队列的性能,无锁算法的性能比最有效的阻塞算法还要高。

IT家园
IT家园

网友最新评论 (0)

发表我的评论
取消评论
表情