java多线程之AQS设计与源码解析

77 阅读8分钟

本文主要分析AQS设计原理及骨架源码,关于Condition及AQS实现的工具类放到以后的文章单独介绍,防止行文过长

3.5.1 简介

​ AQS(AbstractQueuedSynchronizer)提供了一套实现阻塞锁和相关线程同步器(FIFO wait queue)的基础框架,它完成了大部分工作,使得我们可以很容易的实现一个自己的线程同步逻辑。

​ AQS的核心是一个代表共享资源状态的变量state和一个同步等待队列

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
  	
  	/**
  	 * 同步状态,线程通过此变量获得锁(即访问共享资源)
  	 */
  	private volatile int state;
  	/**
  	 * 一个FIFO的双端队列,没有获得锁的线程进入该队列等待
  	 */
  	private transient volatile Node head;
    private transient volatile Node tail;
    
}

AQS同步示意图

aqs.png

一个线程能否访问共享资源取决于是否能通过CAS修改state至期望的值,如果失败则进入等待队列。如上图,一个线程和队列头后继节点的线程都可以竞争锁资源,根据策略不同可以实现公平/非公平锁,后文介绍。

3.5.2 共享状态变量

AQS通过volatile int state表示共享资源的状态。这样自定义的同步器只需要设置state就可以控制线程的同步行为。AQS提供了getState, setState, compareAndSetState方法来访问state变量。

AQS是模版方法模式的设计,对外提供了以下主要方法,用于获取/释放访问权限:

// Exclusive
public final void acquire(int arg);
public final void acquireInterruptibly(int arg) throws InterruptedException;
public final boolean tryAcquireNanos(int arg, long nanosTimeout)throws InterruptedException;
public final boolean release(int arg);
// Shared
public final void acquireShared(int arg);
public final void acquireSharedInterruptibly(int arg) throws InterruptedException;
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout) throws InterruptedException;
public final boolean releaseShared(int arg) 

而在内部,AQS已经实现了同步逻辑,自定义同步器只需实现state语义即可,一般我们重写以下方法:

protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}
protected boolean tryRelease(int arg) {
    throw new UnsupportedOperationException();
}
protected int tryAcquireShared(int arg) {
    throw new UnsupportedOperationException();
}
protected boolean tryReleaseShared(int arg) {
    throw new UnsupportedOperationException();
}
protected boolean isHeldExclusively() {
    throw new UnsupportedOperationException();
}
  1. 一般只需tryAcquire/tryRelease,tryAcquireShared/tryReleaseShared其中一对即可,所以没有将方法定义为abstract.
  2. isHeldExclusively返回当前线程是否正在独占资源,在用到Condition时使用

3.5.3 等待队列

acquire(int arg)为例:

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

tryAcquire返回true,则获取资源成功直接返回,否则进入排队过程。

AQS的等待队列是一个CLH阻塞队列的变种,CLH队列是一个单链表队列,队列中每个节点自旋等待其前驱节点释放锁。如上图,AQS等待队列是一个双端链表,同样依赖前驱节点状态,不过与CLH lock标志不同。

3.5.4 源码解析

  • acquire(int arg)

接着上面acquire方法分析,里边就有一个if语句:

1. tryAcquire: 子类实现,CAS获取资源
2. addWaiter: 失败则创建当前线程对应的节点并入列, 独占模式
3. acquireQueued: 接着进入排队过程
4: 根据acquireQueued返回排队过程是否发生过中断,如果是,则通过selfInterrupt补充中断

第一步就直接获取资源(竞争锁),而不是排队,说明这是一个非公平锁机制,你是否能推断怎样实现公平锁?

/**
 * 创建一个指定模式(Node.EXCLUSIVE|Node.SHARED)的节点并入列
 */
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;
        }
    }
  	// 失败(队列为空或CAS竞争失败)则通过enq调用无限重试入列
    enq(node);
    return node;
}

/** 自旋入列 */
private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // 队列为空,则先初始化头节点,以后获得资源的节点会变成头节点
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
          	// 下面代码是不是和上面方法快速尝试的那一次一模一样!
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

addWaiter返回后,当前线程对应Node节点已经在队列中了,接下来调用acquireQueued进行排队等待。不过在这之前,我们先看一下Node:

static final class Node {
    /** 标示节点在共享模式下等待 */
    static final Node SHARED = new Node();
    /** 标示节点在排他模式下等待 */
    static final Node EXCLUSIVE = null;

    // 以下常量表示该节点所处状态
    static final int CANCELLED =  1;
    static final int SIGNAL    = -1;
    static final int CONDITION = -2;
    static final int PROPAGATE = -3;
		// 节点等待状态
    volatile int waitStatus;
		// 指向同步队列前驱节点
    volatile Node prev;
		// 指向同步队列后继节点
    volatile Node next;
		// 当前节点代表线程
    volatile Thread thread;
		// 链接在condition上等待的线程或表示节点为Node.SHARED节点
    Node nextWaiter;
}

waitStatus状态:

状态含义
CANCELLED1已取消. 当超时或中断发生(若响应中断)时,变为该状态,不再竞争资源state
00新创建的节点或从condition队列中被唤醒的节点状态先设为0
SIGNAL-1指示后继节点等待当前节点唤醒.由后继节点在入队时设置
CONDITION-2指示节点等待在一个condition条件队列上.被唤醒后进入CLH等待队列竞争state
PROPAGATE-3指示当前节点在共享模式下等待

acquireQueued中排队:

/**
 * 等待排他的获取资源,不可中断,被acquire或condition的wait方法调用
 * 返回这期间是否线程被中断过,如过是,该方法返回后通过selfInterrupt()调用补充中断
 */
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
      	// 资源,直到成功
        for (;;) {
            final Node p = node.predecessor();
          	// 如果前驱节点是头节点,则尝试获取一次资源(因为入队过程可能头节点代表正在执行线程已经执行完)
          	// 如果成功,说明此时头节点已经释放了资源
          	// 或者头节点释放资源后唤醒的node节点
            if (p == head && tryAcquire(arg)) {
              	// node拿到资源,变成了头节点
                setHead(node); // head=node,node.thread=null,node.prev=null
                p.next = null; // help GC (原来的头节点从链表中断开,出队列)
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) && // 判断是否应该挂起当前线程
                parkAndCheckInterrupt()) // 如果是,则挂起并在唤醒时返回中断状态
                interrupted = true; // 期间发生了中断
        }
    } finally {
        if (failed) // 因超时或中断(响应中断情况下)没有成功获得资源
            cancelAcquire(node); // waitStatus变为CANCELED
    }
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        // 已经设置其前驱节点状态为SIGNAL,可以安全park了
        return true;
    if (ws > 0) {
        // 如果前驱节点取消了竞争资源,则一直往前查,直到第一个waitStatus<=0点节点为止并且链接在它后面
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        // 形成新的链表,中间已取消的节点从链表解链接,等待GC
        pred.next = node;
     } else {
      	// 此时waitStatus必定为0或PROPAGATE,将前驱节点状态设置为SIGNAL,但暂不park,在acquireQueued下一次循环中重试一次,如果还不行再park
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
     }
     return false; //返回false使acquireQueued进行下一次循环
}
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted(); //返回是否中断过
}

acquire流程如下图:

aqs_acquire.png

  • release(int arg)

    public final boolean release(int arg) {
        if (tryRelease(arg)) {// 调用子类实现尝试释放资源
            Node h = head;
            // 此时线程正在运行,h != null说明当前线程就是head,
            // waitStatus不可能为1(CANCELED)或-2(CONDITION)
            if (h != null && h.waitStatus != 0)
                //到这里waitStatus就只可能为-1(SIGNAL)或-3(PROPAGATE)了
                unparkSuccessor(h);//唤醒后继节点线程
            return true;
        }
        return false;
    }
    
    /** 唤醒一个后继节点线程 */
    private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        if (ws < 0)
            // 将节点状态设为0,就算失败也没关系,因为下面就会唤醒后继节点线程
            // 如果是共享模式,进入该方法前ws已经被设为0了
            compareAndSetWaitStatus(node, ws, 0);
    
    	// s代表应该唤醒(未取消)的后继节点,初始就认为是该node的后继节点
        Node s = node.next;
        // 如果node.next就是未取消的,则省去了这里遍历
        if (s == null || s.waitStatus > 0) {
            // 不管s本身是null还是已取消状态,都将其设置为null,应为下面的if可能都不满足,后面unpark那个if判断会有问题
            s = null;
            // 开始从尾节点向node遍历,找到最后一个(离node最近)未取消的节点
            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(int arg)

    public final void acquireShared(int arg) {
      	// >0: 获取成功且还有剩余资源,=0: 获取成功,无剩余资源,<0: 获取失败
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }
    
    private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED);//共享模式添加新节点并入队
        boolean failed = true;
        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();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) && //同acquireQueued
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
    
    private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        setHead(node);
        /*
         * 尝试唤醒同步队列中一个节点:
         * propagate > 0:有剩余资源可用
         * h == null || h.waitStatus < 0:原head状态为SIGNAL(-1)或PROPAGATE(-3)
         * (h = head) == null || h.waitStatus < 0: 新head状态为SIGNAL(-1)或PROPAGATE(-3)
         * 或者原/新头节点为null,不知道要不要唤醒,那就唤醒一次
         */
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }
    
  • releaseShared(int arg)

    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }
    
    /** 共享锁由于可能多个线程在请求和释放资源,所以会有多个线程同时调用该方法 */
    private void doReleaseShared() {
        // 循环直到所有可以获取到资源的线程被唤醒
        for (;;) {
            Node h = head;
          	// 队列非空且存在后继节点
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                  	//如果状态为SIGNAL,循环直到改为0,别的线程进来就不会重复唤醒
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;
                    unparkSuccessor(h);//唤醒后继节点
                }
              	//否则,将waitStatus设为PROPAGATE,这是一个过渡状态,使得别的线程进来看到该状态后,就知道正在唤醒其他线程节点,只需要等待唤醒过程完成就可以了
                else if (ws == 0 &&	
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;
              	//除以上两种情况,就进入下面h == head判断,循环直到新线程获得资源变成head
            }
            if (h == head) // 如果头节点没有变,说明唤醒过程已经完成(已经没有要唤醒的节点了),退出循环
                break;
        }
    }
    
  • cancelAcquire

    /** 该方法在所有获取资源的方法中可能会调用(在park之前线程外部中断调用) */
    private void cancelAcquire(Node node) {
            // Ignore if node doesn't exist
            if (node == null)
                return;
    
            node.thread = null;
    
            // 跳过所有已取消的前驱节点,最后得到的pred则是一个从node->head第一个未取消节点
            Node pred = node.prev;
            while (pred.waitStatus > 0)
                node.prev = pred = pred.prev;
    
            // predNext用于后面将其CAS替换为其他节点,即将已取消的节点从队列断开
            Node predNext = pred.next;
    
            // 直接将节点状态置为已取消,不用判断之前的状态,
      			// waitStatus是volatile修饰的,其他线程可以立刻看到此修改
            node.waitStatus = Node.CANCELLED;
    
            // 如果当前节点是尾节点,只需将尾节点替换为上面找到的pred即可
            if (node == tail && compareAndSetTail(node, pred)) {
              	// 然后将其后继设为null,此时已取消节点从队列中断开了
                compareAndSetNext(pred, predNext, null);
            } else {
                // 否则,node的后继节点需要链接到pred后面或着唤醒
                int ws;
                if (pred != head &&	// 1.pred不是头节点,则应该继续排队
                    // 括号中是为了确保前驱的状态为SIGNAL,线程可以安全的继续park
                    ((ws = pred.waitStatus) == Node.SIGNAL ||
                     (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                    pred.thread != null) {// 这里判断pred线程正好也调用了cancelAcquire
                    Node next = node.next;
                    if (next != null && next.waitStatus <= 0)
                        // 如果当前线程节点的后继不为null且不是已取消状态,将其链接到pred后面
                        // 此时已取消的节点也从队列断开了
                        compareAndSetNext(pred, predNext, next);
                } else {//2.如果pred是头节点或上面设置状态失败,则直接唤醒
                    unparkSuccessor(node);
                }
    
                node.next = node; // help GC
            }
        }
    

更多文章,见公-众-号