面试集中营—AQS哪些事儿之ReentrantLock

AQS简介

        AQS—AbstractQueuedSynchronizer是JDK工具包中的一个抽象类。在这个抽象类中,有几个属性和一个双向队列(FIFO)。是JUC并发包下的一个基类,那么我们熟知的ReentrantLock、CountDownLatch、信号量等等都是基于这个基类来实现的。

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    
    static final class Node {
        // 排他锁的标识
        static final Node EXCLUSIVE = null;

        /** 如果带有一个标识,证明失效了 */
        static final int CANCELLED =  1;

        /** 具有这个标识,说明后继节点需要被唤醒 */
        static final int SIGNAL    = -1;

        // Node对象存储标识的地方  
        volatile int waitStatus;
 
        // 上一个节点是谁
        volatile Node prev;

        // 下一个节点是谁
        volatile Node next;
        
        // 当前Node绑定的线程
        volatile Thread thread;

        /**
         * Returns previous node, or throws NullPointerException if null.
         * Use when predecessor cannot be null.  The null check could
         * be elided, but is present to help the VM.
         *
         * @return the predecessor of this node
         */
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }
    }
    /**
     * Head of the wait queue, lazily initialized.  Except for
     * initialization, it is modified only via method setHead.  Note:
     * If head exists, its waitStatus is guaranteed not to be
     * CANCELLED.
     */
    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.
     */
    private volatile int state;

        从上述代码中我们可以看到,AbstractQueuedSynchronizer中有一个head字段和tail字段,head和tail都指向一个Node对象,这个Node对象又维护一个prev和next字段,分别又指向了一个Node,这样就形成了一个双向链表的结构。

ReentrantLock

        首先我们先来看下ReentrantLock的源码。

public class ReentrantLock implements Lock, java.io.Serializable {
    private static final long serialVersionUID = 7373984872572414699L;
    /** Synchronizer providing all implementation mechanics */
    private final Sync sync;

    abstract static class Sync extends AbstractQueuedSynchronizer {

        ...
    }
    ...
    public void lock() {
        sync.lock();
    }

    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

    public boolean tryLock() {
        return sync.nonfairTryAcquire(1);
    }

    public void unlock() {
        sync.release(1);
    }
    ...
}

        在ReentrantLock中,持有一个Sync对象,这个Sync是一个内部抽象类,大家可以看到这个内部类继承了AbstractQueuedSynchronizer类。一些核心的方法,都是由这个内部类来完成的。下面我们针对加锁释放锁等核心方法来解析。

加锁

       Sync有两个实现,一个是公平锁,一个是非公平锁。我们先看非公平锁。我们跟踪代码到Sync的NonfairSync实现类

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() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

        根据上面的代码,可以看到,lock方法是先进行了一次CAS的操作,尝试把state属性从0改成1。如果成功就把当前线程赋值给父类的一个属性。如果没有成功就进入了acquire方法

acquire方法

        这是加锁的核心

    public final void acquire(int arg) {
        // 先再次尝试获取锁资源 
        if (!tryAcquire(arg) &&
            // 尝试失败,就将当前线程封装成一个Node,追加到AQS的队列中
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            // 线程中断
            selfInterrupt();
    }

tryAcquire方法

            通过追踪代码,我们最终还是追踪到了ReentrantLock中的一个方法(非公平),如下所示

final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            // 获取现在AQS的值
            int c = getState();
            // 如果值为0,表示锁资源已经被释放了,尝试再次获取锁资源
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    // 尝试成功,并设置当前线程
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // 不是0 检查当前占有锁资源的线程是否是当前线程,这里为了检查重入
            else if (current == getExclusiveOwnerThread()) {
                // 当前的state值+1
                int nextc = c + acquires;
                // 超过了int能表示的正数最大值
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                // 重新对state赋值
                setState(nextc);
                // 锁重入成功
                return true;
            }
            return false;
        }

        注释上已经写的比较明确了,首先获取当前的state值,如果为0表示锁已经被释放掉了,那么就先尝试去获取锁。如果不为0,就要判定是否为重入锁,判断标准就是占有锁的线程是否是当前线程,如果是当前线程state就+1,如果不是就返回false表示加锁失败。 

addWaiter方法

         我们从acquire方法中还有两个方法没有追踪,一个是addWaiter,一个是acquireQueued。我们先看addWaiter方法(AQS提供的方法),这个方法主要是为了封装一个Node对象出来。

private Node addWaiter(Node mode) {
        // 创建一个node对象
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        // 获取尾部节点
        Node pred = tail;
        // 如果不是空队列
        if (pred != null) {
            // 新node的前置设置成尾部节点
            node.prev = pred;
            // 尝试把尾结点修改成当前node节点
            if (compareAndSetTail(pred, node)) {
                // 老的尾节点的后置节点指向新node节点
                pred.next = node;
                return node;
            }
        }
        
        enq(node);
        return node;
    }

        所以这里我们看到,新创建的Node往尾部去插入。可以看成是链表的插入方法。如果当前队列是空的,或者修改尾结点失败,就会进入enq方法

        再看enq方法

    /**
     * Inserts node into queue, initializing if necessary. See picture above.
     * @param node the node to insert
     * @return node's predecessor
     */
    private Node enq(final Node node) {
        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;
                }
            }
        }
    }

        enq方法,我们可以简单的理解成,就是进入一个死循环,循环内部还是继续尝试替换尾结点。直到替换成功

acquireQueued方法

         上一步已经把Node放入了双向队列中。那么还差一步,就是调用。首先定义了两个boolean类型的标志,一个是为了判断执行是否成功,另一个判断是否有中断。

    /**
     * Acquires in exclusive uninterruptible mode for thread already in
     * queue. Used by condition wait methods as well as acquire.
     *
     * @param node the node
     * @param arg the acquire argument
     * @return {@code true} if interrupted while waiting
     */
    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主要就是把当前线程和上一个节点            
                    // 都清空
                    setHead(node);
                    // 原来的头结点去掉next指针,方便gc
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

        我们又看到了一个死循环,那么这个循环内部就是反复的尝试获取锁资源。

        1、先获取当前节点的prev节点;

        2、如果prev节点是头结点,就尝试获取锁;

        3、如果获取锁成功了,把自己设置成头节点,把老的头结点的指针清空,并成功返回;

        4、如果prev节点不是头结点,或者尝试获取锁失败了,就进入下一个判定shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt(),我们再看跟进看下这两个方法。

        shouldParkAfterFailedAcquire

        从方法的注释可以了解这段代码的意思。“检查并更新获取锁失败的node,如果线程应该阻塞,就返回true”。

    /**
     * Checks and updates status for a node that failed to acquire.
     * Returns true if thread should block. This is the main signal
     * control in all acquire loops.  Requires that pred == node.prev.
     *
     * @param pred node's predecessor holding status
     * @param node the node
     * @return {@code true} if thread should block
     */
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        // 获取上一个节点的状态 1失效了 -1正常
        int ws = pred.waitStatus;
        // 如果上一个节点状态为-1正常那么我就可以安全的等待了
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        // 如果上一个节点状态为1 失效了,就继续往上找,找到状态小于0,然后连上
        if (ws > 0) {
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        }
        // 上一个节点不是-1,也不大于0,那么要么是0或者是-3
        // 那么将上一个有效节点状态修改为-1,因为只有-1才能正常唤醒下一个节点
        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;
    }

        上述代码主要分成几个步骤。

        1、获取当前节点的上一个节点的状态 => ws

        2、检查ws,如果是-1,跳到3,如果是1跳到4,如果是其他跳到5;

        3、如果前一个节点的状态是-1表示正常,那么直接返回true;

        4、如果前一个节点的状态是1,表示是无效节点,那么就依次继续向上找,直到找到节点的状态是-1的,并重新建立连接,丢弃中间状态不是-1的节点;

        5、尝试把前一个节点的状态修改为-1;

        parkAndCheckInterrupt方法

        线程挂起。基于UNSAFE.park(false, 0L)的方法来挂起线程

    /**
     * Convenience method to park and then check if interrupted
     *
     * @return {@code true} if interrupted
     */
    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

        相信大家都晕了吧,当然也有可能根本都还没有看到这里,能够坚持看到这里的,给你一个666,现在,我们尝试画一个流程图来总结一下上述的代码。

释放锁

          加锁讲完了,我们再来看看释放锁。如果看明白了加锁的操作,释放锁的操作相比就简单多了。我们先看释放锁的代码。

 public void unlock() {
        sync.release(1);
    }

        调用了AQS的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;
    }

        tryRelease(arg)方法是个抽象方法,我们再看ReentrantLock中的实现

protected final boolean tryRelease(int releases) {
      
      int c = getState() - releases;
      // 如果所在线程不是AQS的中持有的锁的线程 抛出异常
      if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
      
      boolean free = false;
      // 如果state已经是0了,表示锁完全退出,释放线程
      if (c == 0) {
          free = true;
          setExclusiveOwnerThread(null);
      }
      // 更新state值
      setState(c);
      return free;
}

       上面这段代码,如果看完了加锁的过程的,就比较容易理解了。

       1、获取当前的state值(getState()),减去releases的值,这里是1,也是当前state值减一得到一个值c;

      2、如果所在线程不是AQS的中持有的锁的线程,就抛出异常;

      3、如果state已经是0了,表示锁完全退出,释放线程;

      4、不管state是不是0,都更新state值。

       释放了锁持有之后就进入了如下方法中:

if (tryRelease(arg)) {
    Node h = head;
    if (h != null && h.waitStatus != 0)
         unparkSuccessor(h);
    return true;
}
return false;
    

        第一步:获取头节点

        第二步:unparkSuccessor(Node head),其他的节点在没有获取锁的情况下就park了嘛对不对,现在要唤醒就是unpark;

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;
            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、获取head节点的waitStatus的值ws;

        2、如果ws小于0,就先把头结点的waitStatus通过CAS改为0;

        3、先找头节点的next节点,如果next为空,或者waitStatus大于0都表示不可用,此时要注意,是从tail节点开始循环,从后往前找waitStatus <0 的node,这里看到代码中并不是找到就break,而是一直找到头结点,然后找距离头结点最近的那个waitStatus <0 的node。

        4、把这个节点的线程从挂起状态中释放

        注:这里为什么是从尾节点开始找呢?笔者认为主要是加锁的时候acquireQueued的这段代码

} finally {
    if (failed)
       cancelAcquire(node);
}

      cancelAcquire方法就不贴出来了,但是里面有一个关键的代码就是

// 状态改为取消
node.waitStatus = Node.CANCELLED;

if (node == tail && compareAndSetTail(node, pred)) {
      compareAndSetNext(pred, predNext, null);
}

       此时会把next节点设置成null。所以如果从头往后找,会发现后置节点为null的情况。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值