说实话,我在准备探究Java线程同步的实现原理时,并没有听说过AbstractQueuedSynchronizer。所以作为一个昨天才第一次听说的自旋锁实现方式,AQS成功地吸引了我。AQS的实现基于CLH锁(Craig, Landin, and Hagersten Lock),这种数据结构的特点在于把“临界数据”和“进程控制”同时实现。相比于我以前看到的方法,这种实现明显更加高效和简洁。在Java concurrent库中,AQS是最重要的基础之一。
-
AbstractQueuedSynchronizer的原理
一个同步线程工具大致要实现以下三个功能:
- 原子地管理同步状态
- 阻塞和唤醒线程
- 管理线程队列
对于第一个功能,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的大致工作原理:
这个双向链表中的每一个节点代表一个线程,一般说来越靠近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,这里就不一一讨论了。
参考资料:
发表评论