JAVA并发编程-AQS底层实现原理及应用(二)

JAVA并发编程-AQS底层实现原理及应用(一)

CANCELLED状态节点生成

acquireQueued方法中的Finally代码:

final boolean acquireQueued(final Node node, int arg) {
	boolean failed = true;
	try {
    ...
		for (;;) {
			final Node p = node.predecessor();
			if (p == head && tryAcquire(arg)) {
				...
				failed = false;
        ...
			}
			...
	} finally {
		if (failed)
			//等待期间出现异常或其他问题
			cancelAcquire(node);
		}
}
通过cancelAcquire方法,将Node的状态标记为CANCELLED,节点被取消
private void cancelAcquire(Node node) {
    // 如果当前节点为null,直接忽略。
    if (node == null)
        return;
    //1. 设置该节点不关联任何线程,也就是虚节点
    node.thread = null;

    //2. 往前跳过被取消的节点,找到一个有效节点
    Node pred = node.prev;
    while (pred.waitStatus > 0)
        node.prev = pred = pred.prev;

    //3. 拿到了上一个节点之前的next
    Node predNext = pred.next;

    //4. 当前节点状态设置为1,代表节点取消
    node.waitStatus = Node.CANCELLED;

    // 脱离AQS队列的操作
    // 当前Node是尾结点,将tail从当前节点替换为上一个节点
    if (node == tail && compareAndSetTail(node, pred)) {
        compareAndSetNext(pred, predNext, null);
    } else {
        // 到这,上面的操作CAS操作失败
        int ws = pred.waitStatus;
        // 不是head的后继节点
        if (pred != head &&
            // 如果当前节点不是head的后继节点,1:判断当前节点前驱节点的是否为SIGNAL,2:如果不是,则把前驱节点设置为SINGAL看是否成功
		    // 如果1和2中有一个为true,再判断当前节点的线程是否为null
		    // 如果上述条件都满足,把当前节点的前驱节点的后继指针指向当前节点的后继节点
            (ws == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) 
            && pred.thread != null) {
            Node next = node.next;
            if (next != null && next.waitStatus <= 0)
                compareAndSetNext(pred, predNext, next);
        } else {
            // 如果当前节点是head的后继节点,或者上述条件不满足,那就唤醒当前节点的后继节点
            unparkSuccessor(node);
        }

        node.next = node; // help GC
    }
}

获取当前节点的前驱节点,如果前驱节点是cancel,就一直往前找,直到waitStatus<=0,将找到的Pred节点和当前Node关联,将当前Node设置为CANCELLED。
根据节点位置分三种情况处理。

  • 当前节点是尾结点
    JAVA并发编程-AQS底层实现原理及应用(二)_第1张图片

  • 当前节点既不是head的后继结点,也不是尾结点
    JAVA并发编程-AQS底层实现原理及应用(二)_第2张图片

  • 当前节点是head的后继节点
    唤醒当前节点的后继结点

如何释放锁

对解锁的基本流程进行分析。由于ReentrantLock在解锁的时候,并不区分公平锁和非公平锁,所以我们直接看解锁的源码:

// java.util.concurrent.locks.ReentrantLock
public void unlock() {
	sync.release(1);
}
//java.util.concurrent.locks.AbstractQueuedSynchronizer
public final boolean release(int arg) {
	//返回true代表锁资源释放完
	if (tryRelease(arg)) {
		Node h = head;
		//头节点不是null,并且头节点状态不是0,可以唤醒后继阻塞节点
		//1.h == null Head还没初始化。
		//2.h != null && waitStatus == 0 表明后继节点对应的线程仍在运行中,不需要唤醒。
		//3.h != null && waitStatus < 0 表明后继节点可能被阻塞了,需要唤醒。
		if (h != null && h.waitStatus != 0)
			unparkSuccessor(h);
		return true;
	}
	return false;
}

在ReentrantLock里面的公平锁和非公平锁的父类Sync定义了可重入锁的释放锁机制。

//java.util.concurrent.locks.ReentrantLock.Sync
protected final boolean tryRelease(int releases) {
	// 减少可重入次数
	int c = getState() - releases;
	// 当前线程不是持有锁的线程,抛出异常
	if (Thread.currentThread() != getExclusiveOwnerThread())
		throw new IllegalMonitorStateException();
	boolean free = false;
	// 如果持有线程全部释放,将当前ExclusiveOwnerThread属性设置为null,并更新state
	if (c == 0) {
		free = true;
		setExclusiveOwnerThread(null);
	}
	setState(c);
	return free;
}

再看一下unparkSuccessor方法

    private void unparkSuccessor(Node node) {
		
        int ws = node.waitStatus;
		//我们都是根据前一节点的waitStatus判断是否唤醒当前节点,所以设置当前节点状态位0
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
		//获取后继节点
        Node s = node.next;
        //如果下个节点是null或者下个节点被cancelled,就找到队列最开始的非cancelled的节点
        if (s == null || s.waitStatus > 0) {
            s = null;
            //从后往前找到离node最近的且不是null并且waitStatus<=0的节点
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        //如果当前节点的s节点不为空,而且状态<=0,就把s节点unpark
        if (s != null)
            LockSupport.unpark(s.thread);
    }

q1:为什么从后往前找第一个非cancel的的节点,为不是从前往后,
a1:
原因:1>我们可以看到// java.util.concurrent.locks.AbstractQueuedSynchronizer#addWaiter方法,节点入队并不是原子操作,也就是说,node.prev = pred; compareAndSetTail(pred, node) 这两个地方可以看作Tail入队的原子操作,但是此时pred.next = node;还没执行,如果这个时候执行了unparkSuccessor方法,就没办法从前往后找了,所以需要从后往前找。
原因:2>还有一点原因,在产生CANCELLED状态节点的时候,先断开的是Next指针,Prev指针并未断开,因此也是必须要从后往前遍历才能够遍历完全部的Node

中断恢复后的执行流程

唤醒后,会执行return Thread.interrupted();,这个函数返回的是当前执行线程的中断状态,并清除状态。

// java.util.concurrent.locks.AbstractQueuedSynchronizer

private final boolean parkAndCheckInterrupt() {
	LockSupport.park(this);
	return Thread.interrupted();
}

再回到acquireQueued代码,当parkAndCheckInterrupt返回True或者False的时候,interrupted的值不同,但都会执行下次循环。如果这个时候获取锁成功,就会把当前interrupted返回。

final boolean acquireQueued(final Node node, int arg) {
	boolean failed = true;
	try {
		boolean interrupted = false;
		for (;;) {
			final Node p = node.predecessor();
			if (p == head && tryAcquire(arg)) {
				setHead(node);
				p.next = null; // help GC
				failed = false;
				return interrupted;
			}
			if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
				interrupted = true;
			}
	} finally {
		if (failed)
			cancelAcquire(node);
	}
}

如果返回true,则会执行,线程打上中断标记位

// java.util.concurrent.locks.AbstractQueuedSynchronizer

static void selfInterrupt() {
	Thread.currentThread().interrupt();
}

但为什么获取了锁以后还要中断线程呢?

  1. 当中断线程被唤醒时,并不知道被唤醒的原因,可能是当前线程在等待中被中断,也可能是释放了锁以后被唤醒。因此我们通过Thread.interrupted()方法检查中断标记(该方法返回了当前线程的中断状态,并将当前线程的中断标识设置为False),并记录下来,如果发现该线程被中断过,就再中断一次。
  2. 线程在等待资源的过程中被唤醒,唤醒后还是会不断地去尝试获取锁,直到抢到锁为止。也就是说,在整个流程中,并不响应中断,只是记录中断记录。最后抢到锁返回了,那么如果被中断过的话,就需要补充一次中断。

你可能感兴趣的:(并发编程,java,算法,开发语言,数据结构)