Java并发包--AbstractQueuedSynchronizer

官方文档:AQS提供了一种依赖先进先出队列(FIFO)的用于实现阻塞锁和相关同步器的框架。并且为大多数依赖一个单个原子int变量来表达其状态的同步器提供了一个非常有用的基础支持。AQS的子类必须实现在AQS中定义的保护方法,这些保护方法可以改变同步器的状态,而状态的改变要依赖同步器自身的获取和释放的状态。除此以外,AQS中的其他方法实现了所有的入队和阻塞机制。子类可以定义其他的状态变量,但是getState, setState, compareAndSetState这些方法只能用来操作AQS中定义的用于同步状态的原子变量int的值。

简单来讲:AQS实现了一个用于多线程同步的阻塞队列,该阻塞队列本身不包含任何的业务含义,仅仅只一种基础设施,而线程是否进入阻塞队列是依赖AQS中定义的一个volatile int类型变量state, 而state所表示的含义在AQS中也不做限定,需要子类定义state的不同值所代表的逻辑意义,同样如何改变state的值也是在子类中实现。即AQS实现技术细节,AQS的子类定义具体业务。

官方文档:AQS的子类应该被定义称为一个non-public的内部帮助类,用于实现其所在的外层闭包类的同步属性。子类还应该实现例如acquireInterruptibly这样的方法,这些方法会被具体的锁和相关的同步器来调用以实现他们的共有方法。

简单来讲:AQS的子类不能单独声明为一个共有类,例如在FutureTask,CountDownLatch以及ReentrantLock中的内部类Sync都是AQS的子类,都根据自身的业务同步逻辑实现了AQS中的具体改变state变量的方法,并且利用AQS提供的阻塞队列实现了多线程同步。

AQS中实现了两个队列,即等待队列和条件队列:


条件队列中的节点等待CPU调度而执行尝试获取锁的操作,如果获取失败是因为某些条件暂时未能满足,则会转而进入条件队列的队尾;而条件队列中的节点由于等待某一条件发生而暂时不参与CPU调度,等到条件满足,会从条件队列中转移到等待队列的队尾等待尝试获取锁,无论是等待队列还是条件队列,队列中的节点统一用如下数据结构表示:

static final class Node {
       
        static final int CANCELLED =  1;
	// 当节点状态为-1,表示下一个后继节点需要被从阻塞状态唤醒
        static final int SIGNAL    = -1;
        static final int CONDITION = -2;

	// 表示一个节点处于共享模式
        static final Node SHARED = new Node();
	// 表示一个节点处于排他模式
        static final Node EXCLUSIVE = null;
       
        // 当前节点的状态,初始化为0,表示正常等待队列中的节点,-2表示是条件队列中的节点
        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;
        }
}

为了实现阻塞队列,AQS定义了如下几个成员变量:

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {

    // 等待队列头
    private transient volatile Node head;

    // 等待队列尾
    private transient volatile Node tail;

    // 同步状态
    private volatile int state;
}



另外在AQS的父类AbstractOwnableSynchronizer中还定义了一个表示排他模式下的当前执行线程:

public abstract class AbstractOwnableSynchronizer implements java.io.Serializable {

    private transient Thread exclusiveOwnerThread;

    protected final void setExclusiveOwnerThread(Thread t) {
        exclusiveOwnerThread = t;
    }

    protected final Thread getExclusiveOwnerThread() {
        return exclusiveOwnerThread;
    }
}

接着我们看看AQS阻塞队列的入队操作:

    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;

	    // 首次入队时,tail为空,创建一个空的头节点
            if (t == null) {
                Node h = new Node(); // Dummy header
                h.next = node;
                node.prev = h;
                if (compareAndSetHead(h)) {
                    tail = node;
                    return h;
                }
            }
	    // 将入队节点挂在尾节点后面,并将入队节点设置为新的尾节点
            else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }
其中设置队头和队尾两个操作是通过native方式完成

private final boolean compareAndSetHead(Node update) {
    return unsafe.compareAndSwapObject(this, headOffset, null, update);
}

private final boolean compareAndSetTail(Node expect, Node update) {
    return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}

private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long stateOffset;
private static final long headOffset;
private static final long tailOffset;

static {
	try {
	    stateOffset = unsafe.objectFieldOffset
		(AbstractQueuedSynchronizer.class.getDeclaredField("state"));
	    headOffset = unsafe.objectFieldOffset
		(AbstractQueuedSynchronizer.class.getDeclaredField("head"));
	    tailOffset = unsafe.objectFieldOffset
		(AbstractQueuedSynchronizer.class.getDeclaredField("tail"));

	} catch (Exception ex) { throw new Error(ex); }
}
AQS作为多线程同步器,主要起着在多线程间共享状态变量的作用,AQS按照三个维度来操纵同步状态:

1) 获取还是释放

2) 共享还是排他

3) 是否可以中断

4) 阻塞还是自旋

对应的操作分别是:

1) acquire 

2) acquireInterruptibly

3) acquireShared

4) acquireSharedInterruptibly

5) tryAcquireNanos

6) release

7) releaseShared

上述操作就是围绕着状态变量state与阻塞队列之间周旋。

首先来看一下最典型的非中断的排他模式的锁获取acquire操作:

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

// 需要子类根据业务逻辑实现
protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

private static void selfInterrupt() {
    Thread.currentThread().interrupt();
}
acquire的唯一一个参数int arg代表着与状态变量state相关的一个业务操作,留给了子类来实现具体业务。

首先通过tryAcquire尝试执行业务操作(例如获取锁),如果成功则直接返回;如果失败则将通过方法addWaiter将当前线程加入等待队列中

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;  
 }
成功将当前线程插入等待队列后,通过调用函数acquireQueued,不断循环尝试获取操作直到获取成功,注意该操作有可能阻塞,因此不会无限占用CPU资源:

final boolean acquireQueued(final Node node, int arg) {
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } catch (RuntimeException ex) {
            cancelAcquire(node);
            throw ex;
        }
}
节点能够获得尝试操作共享状态的前提是等待队列中当前节点的前置节点是头节点,也就是说当前节点已经排到了等待队列的第一位,如果此时操作成功则直接返回,流程结束;否则判断失败后是否需要阻塞线程,如果需要,对当前线程进行阻塞,不需要就重复判断前驱节点是否头节点。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int s = pred.waitStatus;
        if (s < 0)
            // 已经通知了前驱节点在操作完共享变量以后唤醒本节点,可以放心去阻塞了
            return true;
        if (s > 0) {
            // 前驱节点已经无效,继续向前寻找一个有效前驱
	    do {
		node.prev = pred = pred.prev;
	    } while (pred.waitStatus > 0);
	    pred.next = node;
	}
        else
            // 通知前驱节点,在完成共享变量操作后唤醒本节点,本次操作不阻塞当前线程,下次再阻塞
            compareAndSetWaitStatus(pred, 0, Node.SIGNAL);
        return false;
}
在打理好阻塞后的唤醒机制后,可以放心的去阻塞了,执行阻塞相对简单:

private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
}
以上就是从一个线程尝试操作共享变量,尝试成功和尝试失败后分别的操作流程。
方法acquireInterruptibly与acquire的区别是可以响应中断,不会一条道走到黑。

public final void acquireInterruptibly(int arg) throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (!tryAcquire(arg))
            doAcquireInterruptibly(arg);
}
函数首先尝试执行操作共享变量逻辑,如果成功立即返回;如果失败,则进入"正式流程":

private void doAcquireInterruptibly(int arg)
        throws InterruptedException {
	// 将线程加入等待队列,进入排他模式
        final Node node = addWaiter(Node.EXCLUSIVE);
        try {
	    // 不断循环尝试进入操作共享状态变量state的逻辑操作
            for (;;) {
                final Node p = node.predecessor();

		// 如果该节点是第一个节点(排除头节点),则尝试进入获取逻辑操作,操作成功直接返回
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    return;
                }
		// 操作不成功,并且判断可以进入阻塞状态,则以响应中断的方式阻塞线程
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    break;
            }
        } catch (RuntimeException ex) {
            cancelAcquire(node);
            throw ex;
        }
        // 执行到这里说明线程响应了中断,从阻塞状态唤醒,并取消该线程的任务,并抛出异常
        cancelAcquire(node);
        throw new InterruptedException();
}
 private void cancelAcquire(Node node) {
	
        if (node == null)
	    return;
        
	// 将等待队列节点所代表的线程置空
	node.thread = null;

	// 向前找到一个有效前驱
	Node pred = node.prev;
	while (pred.waitStatus > 0)
	    node.prev = pred = pred.prev;

	// 将有效前驱的后继节点记录下来,方便后面使用CAS操作来更新该前驱的后继节点
	Node predNext = pred.next;

	// 将节点的状态设置为"已取消"
	node.waitStatus = Node.CANCELLED;

	// 如果该节点是尾节点,则直接删除节点并重新设置尾节点为该节点的第一个有效前驱
	if (node == tail && compareAndSetTail(node, pred)) {
	    compareAndSetNext(pred, predNext, null);
	// 待删除节点不是尾节点,这是操作有点复杂
	} else {
	    // 如果存在一个有效前驱(非头节点,并且已被告知要唤醒后继节点),
	    // 则将该待删除节点的直接后继设置该其前驱的新直接后继
	    if (pred != head
		&& (pred.waitStatus == Node.SIGNAL
		    || compareAndSetWaitStatus(pred, 0, Node.SIGNAL))
		&& pred.thread != null) 
	    {

		
		Node next = node.next;
		if (next != null && next.waitStatus <= 0) {
		    compareAndSetNext(pred, predNext, next);
		}
	    // 否则说明该待删除节点已经没有有效前驱,此时直接唤醒待删除节点的直接后继
	    } else {
		unparkSuccessor(node);
	    }

	    node.next = node; // help GC
	}
}
最后是唤醒待删除节点的直接后继,代码很简单,不多说了

private void unparkSuccessor(Node node) {
        /*
         * Try to clear status in anticipation of signalling.  It is
         * OK if this fails or if status is changed by waiting thread.
         */
        compareAndSetWaitStatus(node, Node.SIGNAL, 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;
        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);
}
acquireShared是共享模式下的共享变量操作逻辑
public final void acquireShared(int arg) {
    // 首先尝试执行共享模式下的共享变量state操作逻辑
    if (tryAcquireShared(arg) < 0)

        // 如果失败,进入"正式流程",即线程进入等待队列,不断的阻塞,唤醒,直到操作成功,同时忽略中断
        doAcquireShared(arg);
}

tryAcquireShared需要由子类来定义以及实现相关业务逻辑,例如在CountDownLatch中的实现为:

public int tryAcquireShared(int acquires) {
    return getState() == 0? 1 : -1;
}
CountDownLatch可以简单的理解为是一个计数器,允许一个或者多个线程阻塞(await),直到其他线程完成了一系列操作(操作个数为共享状态变量state)后,便去唤醒阻塞线程继续执行,而CountDownLatch实现的tryAcquireShared在方法await中被调用,其实现的业务逻辑含义为:如果当前线程正在等待的一系列操作已经完成(state==0),则tryAcquireShared执行成功,从而await可以成功返回。

doAcquireShared类似于acquire和acquireInterruptibly中的acquireQueued和doAcquireInterruptibly,但是有一些区别:

private void doAcquireShared(int arg) {
        // 将线程加入等待队列
        final Node node = addWaiter(Node.SHARED);
        try {
            boolean interrupted = false;
            for (;;) {

	        // 如果前继节点是头节点,即轮到当前节点进入共享变量操作,
		// 则再次进入获取逻辑
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {

		        // 将当前节点从等待队列中移除,并唤醒等待队列中的下一个共享模式节点
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
			    // 进入阻塞,等待前驱节点唤醒
                            selfInterrupt();
                        return;
                    }
                }
		// 与之前相同,失败后进入阻塞
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } catch (RuntimeException ex) {
            cancelAcquire(node);
            throw ex;
        }
}
区别就在方法setHeadAndPropagate的调用,该方法设置了新的头节点,并将当前节点的下一个直接后继(如果后继是共享模式节点)唤醒,而在acquire和acquireInterruptibly中的acquireQueued和doAcquireInterruptibly中仅仅是设置了新的头节点,并未执行唤醒操作。

这就是共享模式和排他模式的区别,共享模式下,可以有多个节点操作共享状态变量(满足条件的前提下),而在排他模式,只有在一个线程操作并且显示执行释放操作,释放了共享变量后,才能唤醒后继节点继续操作共享状态变量。

acquireSharedInterruptibly结合了共享模式和响应中断两个特性,这两个特性的实现与前面一样,不再重复。

doAcquireNanos在排他模式和响应中断两个特性的基础上,又具有另一种特性,即可以将线程阻塞指定的时间后唤醒。

public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
	if (Thread.interrupted())
	    throw new InterruptedException();
	return tryAcquire(arg) ||
	    doAcquireNanos(arg, nanosTimeout);
}
上述流程与之前一致,很容易看懂,奥秘就在方法doAcquireNanos中。

private boolean doAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {

	// 记录首次执行的时间,方便后面计算时间差
        long lastTime = System.nanoTime();

	// 加入当前线程到等待队列
        final Node node = addWaiter(Node.EXCLUSIVE);
        try {

	    // 不断尝试操作共享变量,失败阻塞,超时唤醒,继续尝试
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    return true;
                }

		// 如果指定的超时时间小于等于零,则直接返回,不再做尝试,即非阻塞版本
                if (nanosTimeout <= 0) {
                    cancelAcquire(node);
                    return false;
                }

		// 指定的超时时间有效,并且条件允许进入阻塞,则阻塞线程到指定的时间
                if (nanosTimeout > spinForTimeoutThreshold &&
                    shouldParkAfterFailedAcquire(p, node))
                    LockSupport.parkNanos(this, nanosTimeout);

		// 线程自动从阻塞时间后唤醒,重新计算剩余可阻塞时间
                long now = System.nanoTime();
                nanosTimeout -= now - lastTime;
                lastTime = now;

		// 如果线程被中断,则跳出阻塞,唤醒循环
                if (Thread.interrupted())
                    break;
            }
        } catch (RuntimeException ex) {
            cancelAcquire(node);
            throw ex;
        }
        // 取消节点
        cancelAcquire(node);
        throw new InterruptedException();
}
上述讲的都是acquire获取操作,release释放操作的过程与获取过程相反,就不再累述,有兴趣的可以参考JDK代码。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值