Java并发编程之aqs解析(一)
Java语言本身实现了锁语义,由关键字synchronized实现,本篇主要对 java.util.concurrent包下的aqs即同步队列器实现同步器,锁等语义来探索。
Aqs介绍
Aqs是juc包下的一个抽象类,提供用于实现阻塞锁,同步器(信号量,闭锁等)依赖FIFO等待队列的一个基础框架。大多数依赖单原子值来表达状态的同步器都可以用Aqs来辅助实现。
因此Aqs类中有两个重要的属性:
一个 int 类型的state,以及一个等待队列。
要想使用Aqs来实现锁或者其他同步语义只需要实现它的几个方法即可。
* <li> {@link #tryAcquire}--独占获取锁
* <li> {@link #tryRelease}--独占释放锁
* <li> {@link #tryAcquireShared} -- 共享获取锁
* <li> {@link #tryReleaseShared}--共享释放
* <li> {@link #isHeldExclusively}
自己设计一个简单的锁
state初始状态为0,
定义: state==0 锁未被占有,可以获取锁,state>0时表示锁已被占有 且获取锁时state++,释放锁时state–
public class MySimpleLock implements Lock {
private final Sync sync;
public MySimpleLock(){
sync= new Sync();
}
private static class Sync extends AbstractQueuedSynchronizer{
@Override
protected boolean tryAcquire(int arg) {
int c = getState();
if (c==0){
int nextc = c+arg;
// aqs提供的cas操作,让state++
if (compareAndSetState(0,nextc)){
return true;
}
}else if (c<0){
throw new Error("");
}
return false;
}
@Override
protected boolean tryRelease(int arg) {
int c = getState()-arg;
boolean free = false;
if (c==0){
free=true;
}
setState(c);
return free;
}
}
@Override
public void lock() {
sync.acquire(1);
}
@Override
public void unlock() {
sync.release(1);
}
}
以下为测试类:
public class MultiThreadTest {
private final Lock lock;
private int i ;
public MultiThreadTest(Lock lock){
this.lock=lock;
}
public void test() {
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
i++;
System.out.println(i);
}
public void testWithLock()
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.lock();
try{
i++;
System.out.println(i);
}finally {
lock.unlock();
}
}
public void testWithSync() {
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (this){
i++;
System.out.println(i);
}
}
public static void main(String[] args) {
MySimpleLock myLock = new MySimpleLock();
MultiThreadTest multiThreadTest = new MultiThreadTest(myLock);
// test1 不加锁
IntStream.range(0,10).forEach(t->{
new Thread(()->{multiThreadTest.test();}).start();
});
//test2 使用 synchronized
// IntStream.range(0,10).forEach(t->{
// new Thread(()->{multiThreadTest.testWithSync();}).start();
// });
//test3 使用自己定义的锁
IntStream.range(0,10).forEach(t->{
new Thread(()->{multiThreadTest.testWithLock();}).start();
});
}
}
在测试中发现,使用了自己基于Aqs实现的简单的锁可以初步实现Java关键字synchronized的功能。
独占获取与释放方法的解析
思考一下,同步锁应该包含怎么样的功能?
1. 多个线程访问时只能有一个线程获取到锁,在我们的例子中就体现为多个线程修改state状态时,只有一个线程成功。(独占)
2. 线程获取锁失败,应该进入等待队列,例子中修改state状态失败时,就进入了Aqs实现的等待队列中
3. 锁释放时,应该可以唤醒等待队列中的线程。(Aqs实现了对等待队列中线程的唤醒)
针对上面的三点,Aqs具体是如何实现的呢?(Aqs有共享与独占两种模式,此处讨论独占模式)
Aqs中三个重要的Field:
/**
* Head of the wait queue, lazily initialized.
* 注意此处的懒加载,何时进行Head与tail的初始化?
*/
private transient volatile Node head;
/**
* Tail of the wait queue, lazily initialized. Modified only via
* method enq to add new wait node.
*/
private transient volatile Node tail;
/**
* The synchronization state.
* 通过state来表达各种同步语义
*/
private volatile int state;
1 如何保证多个线程修改state时,只有一个线程会成功?
Aqs提供了基于CAS操作原子修改state状态的方法(CAS是什么,可以参考网上的其他博客)
/**
* 如果state当前值确实等于期望值,就原子性的修改state为update值
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
- 如何让获取失败的线程进入等待队列?
- 等待队列如何实现?数据结构是什么?
* +------+ prev +-----+ +-----+
* head | | <---- | | <---- | | tail
* +------+ +-----+ +-----+
//Aqs源码中有一个内部类Node,node就是队列中一个基本元素
//它是一个基于双向链表实现的CLH队列的变种
// 通过拼接在尾部入队,重新设置头部出队。
static final class Node {
/** Marker to indicate a node is waiting in shared mode */
// 标记当前节点为独占还是共享模式
static final Node SHARED = new Node();
/** Marker to indicate a node is waiting in exclusive mode */
static final Node EXCLUSIVE = null;
/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED = 1;
/** waitStatus value to indicate successor's thread needs unparking */
static final int SIGNAL = -1;
/** waitStatus value to indicate thread is waiting on condition */
static final int CONDITION = -2;
/**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate
*/
static final int PROPAGATE = -3;
// 当前节点的等待状态,初始为0
volatile int waitStatus;
volatile Node prev;
volatile Node next;
//进入队列的线程
volatile Thread thread;
Node nextWaiter;
final boolean isShared() {
return nextWaiter == SHARED;
}
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // Used to establish initial head or SHARED marker
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
-入队逻辑是什么?
在我们设计的简单锁中,lock方法调用了sync的acquire方法,来看一下Aqs中acquire的源码;
public final void acquire(int arg) {
// 如果tryAcquire为真,方法调用结束,当前线程不进入等待队列,继续执行
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
// 若tryAcquire返回false,生成一个独占模式的节点加入等待
private Node addWaiter(Node mode) {
// 新建一个独占模式的节点
Node node = new Node(Thread.currentThread(), mode);
// 尝试进入队列的尾部
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 再循环中 不断尝试入队
enq(node);
return node;
}
// 最外部的函数
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
// 为什么 要判断这个节点的前节点是否为头节点?
// FIFO 队列,所以从头部出队
// 从setHead()函数可以看出,aqs的头节点不包含具体数据(线程),仅作为头节点方便链表操作。
// 若前节点为head,意味着它是队列中最先进入的节点,同时获取锁状态成功成功,则出队。
// 为什么获取锁状态失败,进入等待队列的方法中会有出队出现呢?
// 线程被唤醒之后,会在此处自旋尝试获取锁出队
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 判断当前节点的前继元的waitstatus,若前继节点为cancel,则向前遍历剔除状态为cancel的节点,同时若前节点不为signal将其置为signal
if (shouldParkAfterFailedAcquire(p, node) &&
// 让当前线程block,若线程被唤醒则会自旋尝试获取锁
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
-释放锁后的逻辑是什么?
public final boolean release(int arg) {
// tryRealse返回true 释放锁
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
// 将头节点的后继节点唤醒
unparkSuccessor(h);
return true;
}
return false;
}
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
// 头节点的后继节点
Node s = node.next;
// 若后继节点为null或后继节点状态为cancel,从尾向前遍历找到waitstatus<0的节点并唤醒
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
// 唤醒
if (s != null)
LockSupport.unpark(s.thread);
}
通过图解来最终描述入队出队的流程:
入队流程
思考与总结
1. 重新审视开头的自己设计的简单锁,它有哪些问题呢?
a.它满足独占
b.它是否满足可重入呢?显然不满足,若同一个线程获取两次锁,会发生死锁现象。
c.它是否能实现公平非公平呢?显然它是个非公平锁
如何使用aqs实现可重入,实现公平锁或非公平锁的语义呢?
接下来的文章,我会带着上面的问题,探索ReentrantLock是如何实现上述语义的。

被折叠的 条评论
为什么被折叠?



