Java多线程同步实现

本文出自:【InTheWorld的博客】

说实话,我在准备探究Java线程同步的实现原理时,并没有听说过AbstractQueuedSynchronizer。所以作为一个昨天才第一次听说的自旋锁实现方式,AQS成功地吸引了我。AQS的实现基于CLH锁(Craig, Landin, and Hagersten Lock),这种数据结构的特点在于把“临界数据”和“进程控制”同时实现。相比于我以前看到的方法,这种实现明显更加高效和简洁。在Java concurrent库中,AQS是最重要的基础之一。

  • AbstractQueuedSynchronizer的原理

一个同步线程工具大致要实现以下三个功能:

  1. 原子地管理同步状态
  2. 阻塞和唤醒线程
  3. 管理线程队列

对于第一个功能,AQS使用了一个int型的字段来管理锁的状态。类似于物理硬件的compareAndSwap原子指令,Java也提供了类似的原子指令,如compareAndSetState。在Java内存模型中,对volatile修饰的int字段的读、写以及CAS操作都是原子性的。

对于线程的阻塞与唤醒,Java使用了系统原生的进程挂起和唤醒,简单说来线程的调度不会在意AQS的存在。

第三个功能相对比较复杂,这也是AQS的核心功能。前文所述的CLH locks就是应用在这里。CLH锁的获取的代码如下:

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

CLH同步队列其实就是一个双向链表,CLH队列使用Head和tail两个“指针”来维护。CLH锁的独特之处在于入队的操作,多个线程同时改写tail指针,但是根据Java内存模型的规定,只有一个线程可以改写成功。下图展示了CLH的大致工作原理:

image

这个双向链表中的每一个节点代表一个线程,一般说来越靠近head的线程等待的越久。如果使用公平的调度方式,一般会唤醒最接近head的那个线程,所以一般情况下CLH锁的释放操作是O(1)的复杂度。 节点的状态由一个int变量维护。

static final int CANCELLED =  1;  //结点会被设置为取消状态  
static final int SIGNAL    = -1;  //这个结点的继任结点被阻塞了                                          
static final int CONDITION = -2;  //节点由于条件不满足而阻塞 
static final int PROPAGATE = -3;  //共享头模式时使用,暂时还不明白
  • 基于AQS的同步类实现

正如前文所述AQS是Java中各种同步类的实现基础,换言之如果真正地理解AQS,其他的线程同步类的实现也就显得非常清晰了。区别大概就在状态的表示方式不同了,这里的状态指的是队列的状态。

1. ReentrantLock类使用同步状态保存锁的个数;

2. Semaphore类使用同步状态保存信号的数目,很可能变成负数;

3. CountDownLatch类使用同步状态保存未完成的线程个数,变成0的时候唤醒所有线程;

此外应该还有很多线程同步类使用了AQS,这里就不一一讨论了。

参考资料:

The java.util.concurrent Synchronizer Framework

发表评论