jdk1.8 AQS源码分析以及以ReentrantLock分析公平不公平控制

AQS

AQS全名叫做AbstractQueuedSynchronizer,从名字可以看出,它是一个抽象的队列型的同步器。被广泛地用于比如需用并发控制的类中。AQS分为两种:独占式的和共享式的。ReentrantLock就是独占式的,CountDownLatch就是共享式的。两者的区别就是独占式的,同一时刻只能有一个线程可以持有资源,其他线程竞争资源会阻塞;而共享式的,同一时刻可以有多个线程持有资源,当资源不够时,其他竞争资源才会阻塞
AQS主要通过下面几个属性以及数据结构来完成并发控制

// 代表资源状态
private volatile int state;
// 等待队列的尾节点
private transient volatile Node tail;
// 等待队列的头结点
private transient volatile Node head;
// 队列中的每个节点分为两种类型,SHARED对应共享式的,EXCLUSIVE对应独占式的
// 并且每个节点具有多个状态:CANCELLED,SIGNAL,CONDITION,PROPAGATE
// 其中需要注意,只有CANCELLED状态的值是小于0的
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;

   static final int PROPAGATE = -3;

   volatile int waitStatus;

   volatile Node prev;

   volatile Node next;

   volatile Thread thread;
   //........
}

其实,AQS就是使用一个int变量state代表资源的状态,一个双向链表组成的FIFO队列来控制并发

自定义AQS

如果我们想要完成自己的并发控制,那么我们只需要实现如下几个简单的方法即可
AQS使用模板设计模式,我们只需要指明根据状态什么情况下能够成功申请资源,什么情况下能够成功释放资源即可,其他的比如申请资源失败之后的处理方法,由AQS操作
如果需要自定义独占式的并发控制,那么我们需要实现下面两个方法:

// 尝试获取资源,返回值代表操作是否成功
boolean tryAcquire(int arg)
// 尝试释放资源,返回值代表操作是否成功
boolean tryRelease(int arg)

如果需要自定义共享式的并发控制,那么我们需要实现下面两个方法:

// 代表获取完资源后,资源的剩余量
// 如果小于零,代表获取失败
int tryAcquireShared(int arg)
// 释放资源,返回值代表操作是否成功
bool tryReleaseShared(int arg)

独占式源码分析

独占式主要调用acquire()方法和release()方法,下面我们分别分析它们的源码

public final void acquire(int arg) {
	// 首选调用tryAcquire()方法,判断能否获取到资源
	// 如果能够获取到资源,那么函数就结束了,接着线程就可以执行自己的任务
	// 如果获取资源失败,那么就会调用addWaiter()方法,创建一个代表当前线程的节点
	// 添加到等待队列末尾,然后执行acquireQueued()方法
     if (!tryAcquire(arg) &&
         acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
         // 能够运行到这里,代表首次竞争任务失败,从而线程阻塞,添加到等待队列中
         // 在阻塞的过程中被中断了,但是因为线程在阻塞状态下无法响应中断,所以需要在
         // 获取资源能够正常运行任务时,再执行中断响应
         selfInterrupt();
 }
// addWaiter的实现十分简单,就是创建一个代表当前线程的节点,然后使用cas添加到等待队列的末尾
private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    // 尾节点不为null,代表当前队列中有等待任务
    // 尝试使用cas将当前节点设置为尾节点
    if (pred != null) {
        node.prev = pred;
        // 在并发情况下,这里可能会失败
        // 如果设置成功就直接返回
        // 如果设置失败,会通过后面的enq方法,保证设置成功
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 当前队列中没有等待任务或者有等待任务但是首次cas失败
    enq(node);
    return node;
}
private Node enq(final Node node) {
	// 这里使用死循环和cas保证设置成功
    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;
            }
        }
    }
}

至此,任务添加到等待队列就分析完了,接下来分析一下任务添加到等待队列之后需要接着做什么,看acquireQueued()

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);
               // 方便gc回收节点
               p.next = null; // help GC
               failed = false;
               // 返回当前节点的任务在阻塞过程中是否被中断过
               return interrupted;
           }
           // 判断当前任务是否能够安全地进入阻塞状态
           // 如果能那么就进入阻塞状态,死循环就不会继续执行了,暂停在这里了
           if (shouldParkAfterFailedAcquire(p, node) &&
               parkAndCheckInterrupt())
               interrupted = true;
       }
   } finally {
       if (failed)
           cancelAcquire(node);
   }
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
	// 获取当前节点前一个节点的状态
   int ws = pred.waitStatus;
   // 如果前一个节点的状态是signal,那么就判断为可以进入阻塞状态
   if (ws == Node.SIGNAL)
       /*
        * This node has already set status asking a release
        * to signal it, so it can safely park.
        */
       return true;
       // 如果前一个节点的状态大于0,也就是CANCENLLED
       // 那么当前节点会沿着链表向头结点方向移动,知道找到一个节点的状态<=0,将当前节点连接在该节点的后面
   if (ws > 0) {
       /*
        * Predecessor was cancelled. Skip over predecessors and
        * indicate retry.
        */
       do {
           node.prev = pred = pred.prev;
       } while (pred.waitStatus > 0);
       // 其实就是将中间那些CANCELED的节点从队列中移除
       pred.next = node;
   } else {
       /*
        * waitStatus must be 0 or PROPAGATE.  Indicate that we
        * need a signal, but don't park yet.  Caller will need to
        * retry to make sure it cannot acquire before parking.
        */
       compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
   }
   return false;
}
// 将当前线程阻塞
// 当一个线程被唤醒之后,就会从这个方法中退出,并且返回在阻塞的时候是否被中断过
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

至此,对acquire的分析就结束了,总结一下流程:

  1. 尝试获取资源,如果资源获取成功,那么就可以继续执行自己的任务;
    获取失败,创建一个代表当前线程的节点,添加到阻塞队列的末尾
  2. 在一个死循环里,判断自己是否是第二个节点,并且可以成功获取资源,如果获取成功,那么就退出死循环,执行自己的任务
    不是第二节点或者获取资源失败,那么就判断自己能够安全地阻塞(即找到一个前面的节点,节点状态是SIGNAL的,连接到该节点的后面),如果能够安全地阻塞,那么就阻塞,等待唤醒

下面分析release()

public final boolean release(int arg) {
	// 尝试释放资源成功
    if (tryRelease(arg)) {
    	// 唤醒头结点的下一个节点
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
private void unparkSuccessor(Node node) {
    /*
     * If status is negative (i.e., possibly needing signal) try
     * to clear in anticipation of signalling.  It is OK if this
     * fails or if status is changed by waiting thread.
     */
    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;
    if (s == null || s.waitStatus > 0) {
        s = null;
        // 从队列的末尾向前遍历,唤醒离head最近的没有cancel的节点
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);
}

共享式源码分析

public final void acquireShared(int arg) {
	// 返回值小于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) &&
                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);
    /*
     * Try to signal next queued node if:
     *   Propagation was indicated by caller,
     *     or was recorded (as h.waitStatus either before
     *     or after setHead) by a previous operation
     *     (note: this uses sign-check of waitStatus because
     *      PROPAGATE status may transition to SIGNAL.)
     * and
     *   The next node is waiting in shared mode,
     *     or we don't know, because it appears null
     *
     * The conservatism in both of these checks may cause
     * unnecessary wake-ups, but only when there are multiple
     * racing acquires/releases, so most need signals now or soon
     * anyway.
     */
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        // 资源还有剩余,唤醒当前节点的下一个节点
        if (s == null || s.isShared())
            doReleaseShared();
    }
    
}
private void doReleaseShared() {
   /*
    * Ensure that a release propagates, even if there are other
    * in-progress acquires/releases.  This proceeds in the usual
    * way of trying to unparkSuccessor of head if it needs
    * signal. But if it does not, status is set to PROPAGATE to
    * ensure that upon release, propagation continues.
    * Additionally, we must loop in case a new node is added
    * while we are doing this. Also, unlike other uses of
    * unparkSuccessor, we need to know if CAS to reset status
    * fails, if so rechecking.
    */
   for (;;) {
       Node h = head;
       if (h != null && h != tail) {
           int ws = h.waitStatus;
           // 获取当前头结点的状态
           if (ws == Node.SIGNAL) {
               if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                   continue;            // loop to recheck cases
               // 唤醒之后的节点
               unparkSuccessor(h);
           }
           else if (ws == 0 &&
                    !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
               continue;                // loop on failed CAS
       }
       if (h == head)                   // loop if head changed
           break;
   }
}

唤醒多个节点的示意图如下
当head节点成功获得资源后,如果资源还有剩余,会唤醒第二个节点,并且将第二节点设置为head节点,并且唤醒第三个节点
就是通过这种方式,唤醒多个节点的
开始状态
状态2
下面看一下释放资源
逻辑也很简单,如果成功释放资源,那么就唤醒后面的节点

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}
private void doReleaseShared() {
    /*
     * Ensure that a release propagates, even if there are other
     * in-progress acquires/releases.  This proceeds in the usual
     * way of trying to unparkSuccessor of head if it needs
     * signal. But if it does not, status is set to PROPAGATE to
     * ensure that upon release, propagation continues.
     * Additionally, we must loop in case a new node is added
     * while we are doing this. Also, unlike other uses of
     * unparkSuccessor, we need to know if CAS to reset status
     * fails, if so rechecking.
     */
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                unparkSuccessor(h);
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}

公平并发控制

下面以ReentrantLock来分别介绍公平和不公平并发控制
在创建ReentrantLock时,可以传入一个参数来指明是公平的还是不公平的
这里的公平和非公平指的是,线程获得资源的顺序和线程申请获得资源的顺序相同
假如A线程当前持有资源,B线程请求资源失败,加入了等待队列
当A线程释放资源时,B被唤醒,同时另外一个线程C也请求资源
如果能够保证B获得资源,那么就是公平的,否则就是不公平的

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

下面分析FairSync

static final class FairSync extends Sync {
  private static final long serialVersionUID = -3000897897090466540L;

  final void lock() {
      acquire(1);
  }

  /**
   * Fair version of tryAcquire.  Don't grant access unless
   * recursive call or no waiters or is first.
   */
  protected final boolean tryAcquire(int acquires) {
  		// 获取当前线程
      final Thread current = Thread.currentThread();
      // 获取当前状态
      // state == 0 代表资源可用,state == 1代表资源不可用
      int c = getState();
     // 当前资源可用
      if (c == 0) {
      		// 判断等待队列中是否还有等待任务,如果有等待任务,那么当前线程直接添加到等待队列中,如果没有等待任务,那么设置state,并且设置当前线程拥有资源
          if (!hasQueuedPredecessors() &&
              compareAndSetState(0, acquires)) {
              setExclusiveOwnerThread(current);
              return true;
          }
      }
      // 当前资源不可用,但是因为当前线程独占式地拥有资源,所以仍然能够使用资源
      // 这里就是可重入的实现方法
      else if (current == getExclusiveOwnerThread()) {
      		// 这里实际上就是记录重入的次数
          int nextc = c + acquires;
          if (nextc < 0)
              throw new Error("Maximum lock count exceeded");
          setState(nextc);
          return true;
      }
      return false;
  }
}
public final boolean hasQueuedPredecessors() {
    // The correctness of this depends on head being initialized
    // before tail and on head.next being accurate if the current
    // thread is first in queue.
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
    // 返回true的情况,等待队列中有阻塞任务
    // 且
    // 只有一个等待任务或者头节点的下一个节点不代表当前线程
    // 也就是说下一个可能获得资源的节点不是当前节点
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}
```
一个新的线程只有在没有等待的任务时才能使用资源,如果队列中有等待任务,那么自己也添加到等待队列中
## 不公平并发控制
```java
static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;

    /**
     * Performs lock.  Try immediate barge, backing up to normal
     * acquire on failure.
     */
    final void lock() {
    	// 如果能够将状态设置为1,那么直接就可以拥有资源,不用理会等待任务
    	// 所以是不公平的
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }
    
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}
// 当申请资源失败时,会执行下面的代码
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    
    int c = getState();
    // c == 0代表资源可用
    if (c == 0) {
    	// 粗暴地尝试将状态设置为1来占用资源
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}
```
每当一个新线程申请资源,都会不理会等待队列中是否有等待任务,直接使用cas尝试占用资源,这就是不公平的
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值