ReentrantLock

  这一节聊一下ReentrantLock的源码,这个也是面试中必不可少的内容,希望本文能让你有收获。

一、锁的分类

  首先ReentrantLock分为公平锁和非公平锁两种,默认情况下创建的是非公平锁。在ReentrantLock的内部是存在一个Sync的内部类,公平锁和非公平锁都是这个类的子类。

    /**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }

    /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

  根据布尔类型的构造参数来返回两个不同的类对象,我们先看一下Sync类的内容

   abstract static class Sync extends AbstractQueuedSynchronizer {

  这里可以看到Sync类是AQS的一个子类,AQS的就先不单独看了,因为在后面介绍一些方法的时候会涉及到其中的源码,到时候会拿出来详细的说。

二、获取锁

  创建完ReentrantLock对象之后,我们最先要使用的就是lock方法,那就从这个方法开始说起。

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

  这是在ReentrantLock中的方法,可以看到是调用的sync对象,那么上面我们在说锁的分类的时候提到,会根据构造参数返回不同的sync对象

    /**
     * Base of synchronization control for this lock. Subclassed
     * into fair and nonfair versions below. Uses AQS state to
     * represent the number of holds on the lock.
     */
    abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = -5179523762034025860L;

        /**
         * Performs {@link Lock#lock}. The main reason for subclassing
         * is to allow fast path for nonfair version.
         */
        abstract void lock();

  所以追到这里我们可以看到在父类Sync中定义了一个抽象方法lock,所以接下来我们来对比的看一下在公平锁和非公平锁中的lock方法的实现有什么不同

 

非公平锁中:

        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

公平锁中

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

  这里可以很清楚的看到,在非公平锁场景下,直接尝试通过CAS的方式获取锁,在获取失败的时候再调用acquire方法,那么就来看一下acquire方法

    /**
     * Acquires in exclusive mode, ignoring interrupts.  Implemented
     * by invoking at least once {@link #tryAcquire},
     * returning on success.  Otherwise the thread is queued, possibly
     * repeatedly blocking and unblocking, invoking {@link
     * #tryAcquire} until success.  This method can be used
     * to implement method {@link Lock#lock}.
     *
     * @param arg the acquire argument.  This value is conveyed to
     *        {@link #tryAcquire} but is otherwise uninterpreted and
     *        can represent anything you like.
     */
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

  这里要说明一下,这个acquire方法是在AQS中提供的方法,跟锁的类型没有关系。这里的逻辑比较重要,因为在if条件中的逻辑比较多,那么我们先看一下tryAcquire方法

    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }

  同样是在AQS中,可以看到是一个需要子类来实现的方法,那么就还是分别看一下:

 

非公平锁:

        /**
         * Performs non-fair tryLock.  tryAcquire is implemented in
         * subclasses, but both need nonfair try for trylock method.
         */
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                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;
        }

公平锁:

        /**
         * 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();
            int c = getState();
            if (c == 0) {
                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;
        }

  这里通过对比我们可以看到,两个方法的区别不大,都是先判断state的值,如果为0表示当前没有线程持有这个锁,所以会尝试获取,如果不是0,那么检查是不是重入行为,如果都不是返回false,表示尝试获取锁失败。但是因为之前的if条件判断是取反操作所以:

  • 如果获取锁,或者重入锁成功,返回true ,那么根绝逻辑短路操作,后面的操作不再执行,当前线程获取锁成功
  • 如果获取锁,或者重入锁失败,返回false, 那么需要执行后面的 acquireQueued 方法进行逻辑处理,这个方法下面会提到

  说完了相同的部分,我们再来看一下这个两个方法中的不同部分,就是公平锁中的 hasQueuedPredcessors方法。

    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;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

  我们知道公平锁的特点就是只要之前有线程比自己早申请获取锁了,那么就一定会等待之前的线程执行完,或者取消等待。所以基于以上铺垫,我们来看一下这个方法。方法的名称就是看是否有排队的前置进程,简单的说就是是否存在已经等待的任务。

  而返回值是boolean,所以会存在两种结果:

  • 返回True:存在前置等待任务,所以需要等待
  • 返回False:当前没有前置任务在等待,所以可以尝试获取锁

所以这里根据整体的返回结果来分别说明这个方法中的几种情况:

  • 返回False 
    • h != t 的结果为false,直接短路,整体结果返回false 
      • 这里h和t代表头和尾节点,如果相等可以存在两种情况,首先是两者皆为null,就是在还未初始化,那么此时肯定没有前置等待任务。
      • 第二种情况就是两者不为null,那么如果两者相等,那就是两者指向同一个对象,也就是在刚完成初始化,这里我们先看一下AQS中的enq方法,会创建一个默认的Node对象,然后head和tail都指向这个对象,但是此对象并不是等待任务,只是作为一个标识对象。这么说第二个进来的任务也不需要排队,因为前置节点是head,第三个进来的线程才需要排队。
 1    /**
 2      * Inserts node into queue, initializing if necessary. See picture above.
 3      * @param node the node to insert
 4      * @return node's predecessor
 5      */
 6     private Node enq(final Node node) {
 7         for (;;) {
 8             Node t = tail;
 9             if (t == null) { // Must initialize
10                 if (compareAndSetHead(new Node()))
11                     tail = head;
12             } else {
    • h != t 的结果为true , 但是  ((s = h.next) == null || s.thread != Thread.currentThread()) 的结果为false,也就是这两个条件都是false
      • (s = h.next) == null 为false ,也就是说头节点的后续节点不为null,存在一个生效的等待任务
      • s.thread != Thread.currentThread() 为false,表明head的后置节点就是当前线程,那么也就是说当前线程已经获取了锁,现在正在执行重入操作,所以允许
  • 返回True:
    • h != t 为true , (s = h.next) == null 为true
      • 这种情况看起来有点疑惑,就是首先h != t 为true,那么就是当前队列中存在多个等待节点,但是为什么head的第一个后置节点又是null呢?还是在enq方法中,这里就不粘贴了,直接看上面的内容,就是在10行和11行直接中间,其中一个线程先执行了if条件里面的内容,将head初始化为一个空的Node节点,但是此时11行的代码还没有执行,也就是说有一个线程比当前线程早一步执行,那么当前线程就一定需要等待,所以这里整体会返回True
    • h != t 为 true , s.thread != Thread.currentThread() 为true ,
      • 这个就是比较常规的情况,h != t说明当前存在多个线程节点在等待,而且head的后置任务节点并不是当前线程,所以当前线程就一定需要等待。

 

  好了,这个方法深入的有点多,咱们回过头来继续看。上面提到的都是tryAcquire() 方法。那么如果获取锁成功,那么就直接返回true,通过短路操作后面的逻辑就不再执行了。但是如果获取锁失败,这个返回的是false呢?那我们继续看一下后面的逻辑: acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

首先看一下addWaiter方法:

  

    /**
     * Creates and enqueues node for current thread and given mode.
     *
     * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
     * @return the new node
     */
    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;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

  这个也是AQS中的一个方法,同样与锁的类型无关。通过方法名称我们可以知道,是在同步等待队列的尾部添加当前线程,表示进入队列等待。那么这里首先获取了tail节点,如果不是null,那么就说明之前已经完成了初始化操作,通过链表指针操作完成连接,最后通过CAS条件tail节点的指针即可,但如果tail为空呢?那就需要直接进入到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;
                }
            }
        }
    }

  这里首先通过自旋的方式进入循环,如果tail为null,那么执行初始化操作,将head和tail都指向默认的Node节点。但是这里并没有返回,所以通过自旋的方式还会再次进去循环体中,这次tail节点就不是空了,将node添加到链表的尾部,返回tail节点。

再来看一下 acquireQueued 方法:  

 1     /**
 2      * Acquires in exclusive uninterruptible mode for thread already in
 3      * queue. Used by condition wait methods as well as acquire.
 4      *
 5      * @param node the node
 6      * @param arg the acquire argument
 7      * @return {@code true} if interrupted while waiting
 8      */
 9     final boolean acquireQueued(final Node node, int arg) {
10         boolean failed = true;
11         try {
12             boolean interrupted = false;
13             for (;;) {
14                 final Node p = node.predecessor();
15                 if (p == head && tryAcquire(arg)) {
16                     setHead(node);
17                     p.next = null; // help GC
18                     failed = false;
19                     return interrupted;
20                 }
21                 if (shouldParkAfterFailedAcquire(p, node) &&
22                     parkAndCheckInterrupt())
23                     interrupted = true;
24             }
25         } finally {
26             if (failed)
27                 cancelAcquire(node);
28         }
29     }

  这个方法的主体是一个自旋处理,因为是一个同步等待队列,所以只有在前面节点执行完之后,当前节点才能尝试获取锁。所以一般情况下第一个if条件是不满足条件的,这里也先跳过,我们直接看第二个if条件:shouleParkAfterFailedAcquire , 这个方法p是前置节点,node是当前线程节点,来判断当前线程是否需要阻塞等待。

 1     private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
 2         int ws = pred.waitStatus;
 3         if (ws == Node.SIGNAL)
 4             /*
 5              * This node has already set status asking a release
 6              * to signal it, so it can safely park.
 7              */
 8             return true;
 9         if (ws > 0) {
10             /*
11              * Predecessor was cancelled. Skip over predecessors and
12              * indicate retry.
13              */
14             do {
15                 node.prev = pred = pred.prev;
16             } while (pred.waitStatus > 0);
17             pred.next = node;
18         } else {
19             /*
20              * waitStatus must be 0 or PROPAGATE.  Indicate that we
21              * need a signal, but don't park yet.  Caller will need to
22              * retry to make sure it cannot acquire before parking.
23              */
24             compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
25         }
26         return false;
27     }

  这里涉及到了Node里面的waitStatus字段,默认初始化这个字段是没有赋值的,所以默认值就是0。这里首先获取前置节点的ws,如果是SINGAL,表示需要等待前置节点的状态,那么直接返回true,表明当前线程节点需要阻塞等待。

  如果ws大于0 ,这里我们根据枚举值可以知道,只有CANCELLED的值是大于0的,所以在循环体中是通过循环不断的获取起前置节点,就是跳过已经取消状态的线程节点。通过指针的处理,最终这些取消了等待获取锁的线程节点最终是会被GC回收掉。

  最终通过CAS的方式将当前线程节点的前置节点的状态设置为SINGAL,那么当前线程在下次循环的时候就会进入到等待状态。执行到这里最大可能就是当前节点刚被添加到队尾,其前置节点还没有来的及更新其ws。

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

  接下来就是 parkAndCheckInterrupt 方法,因为在执行shouleParkAfterFailedAcquire的时候,如果前置节点的ws是SINGAL,那么方法会返回true,接下来就是执行当前方法,在这里会通过LockSupport.park将当前线程阻塞住,然后会调用 Thread.interrupted 方法检测中断状态,如果期间有其他线程调用了当前线程的中断状态,则这个方法会返回true,并且清除中断状态。也就是说存在其他线程通过唤醒的方式,请求中断当前线程的等待状态。所以清除了当前线程的中断状态之后,继续执行后续逻辑,因为当前方法在自旋体中,所以会再次进入到if判断中:

            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;
            }

  这里p是其前置线程节点,那么p == head一定满足条件,然后尝试调用tryAcquire获取锁,这里因为其前置节点已经释放锁了,所以这里是有可能获取锁成功的。但是如果非公平锁,那么这里是有可能被其他线程抢占锁的。

  那么不考虑这个条件,假设当前线程被中断唤醒之后,已经通过tryAcquire方法获取到了锁,通过setHead将head指针指向当前线程节点,然后因为p的prev已经为null , 所以这里将其next指针也置为null,那么当前已经执行完任务的节点p就会在下一次GC时被回收。然后返回interrupted标志位。

    /**
     * Convenience method to interrupt current thread.
     */
    static void selfInterrupt() {
        Thread.currentThread().interrupt();
    }

  如果上面的逻辑执行完,最终返回的interrupted标志位为true,那么就是执行到selfInterrupt方法中,因为刚才调用interrupted方法清除了中断标志位,那么这里再次设置中断标志,因为刚才在acquireQueued方法中是无法响应中断的。

三、释放锁

  上面介绍到了获取锁时线程会在一定的条件下被阻塞住,然后等待其前置线程节点的状态,等待获取锁的机会。下面看一下unlock的实现,把全流程串起来

    /**
     * Releases in exclusive mode.  Implemented by unblocking one or
     * more threads if {@link #tryRelease} returns true.
     * This method can be used to implement method {@link Lock#unlock}.
     *
     * @param arg the release argument.  This value is conveyed to
     *        {@link #tryRelease} but is otherwise uninterpreted and
     *        can represent anything you like.
     * @return the value returned from {@link #tryRelease}
     */
    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

  这个release方法是AQS中的一个方法,所以是跟锁的类型无关的。这里看一下tryRelease方法:

        protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

  这里看state的值,如果当前线程并不是获取锁的线程,那么直接抛出异常。如果state的值为0,表明当前锁已经释放完成,如果一个线程执行了多次重入锁,那么这个值需要执行多次释放锁的操作才能变为0 。最终通过CAS的方式将state的状态修改为0 ,最终返回true 。

  如果tryRelease返回true,表明当前线程已经释放了锁,那么会执行 if (h != null && h.waitStatus != 0) 条件判断。head节点指针指向h,也就是当前释放了锁的线程,那么这个h != null肯定为true 。此时前置节点的ws一定是SINGAL,所以整体if判断条件是一定满足的。然后会调用 unparkSuccessor 方法

    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);
    }

  这里的node节点就是当前已经使用并且已经释放了锁的线程,首先如果当前线程的ws小于0 ,那么则有可能还是SINGAL状态,那么首先将状态置为0。然后获取下一个线程节点。

  但是这里有一个小问题,就是如果当前线程节点的后续节点为null , 然后接下来通过一个for循环寻找下一个节点,但是在循环体中是从tail节点开始往前找,为什么不直接从前往后找呢?问题产生的原因就在与入队的方法中:

    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方法,我们看到在else中,首先将当前线程的prev指针连接上,然后再通过CAS的方式调整后续指针,但是这里是没有任何锁控制的,如果在并发的情况下,是有可能导致其中某个节点无法通过next指针找到其后续节点的。所以这里会从tail指针开始往前遍历查找。最终通过LockSupport.unpark唤醒后面的等待线程节点。

四、Condition 

  在实际场景中,我们使用ReentrantLock另外一个比较有力的支持就是,可以根据等待条件进行不同的队列等待。之我们使用Synorchized关键字进行线程等待,是所有等待获取锁的线程都在同一个队列中等待,最终在所有等待线程中随机选择一个唤醒,但是如果唤醒的线程需要依赖的资源并没有就绪,那么此次唤醒就没有什么价值,这个线程还是需要进入等待状态。

  在这种场景下我们可以通过Lock的newCondition()方法来创建不同的等待队列

Condition condition = lock.newCondition()

  下面看一下newCondition犯方法:

 public class ConditionObject implements Condition, java.io.Serializable {
        private static final long serialVersionUID = 1173984872572414699L;
        /** First node of condition queue. */
        private transient Node firstWaiter;
        /** Last node of condition queue. */
        private transient Node lastWaiter;

        /**
         * Creates a new {@code ConditionObject} instance.
         */
        public ConditionObject() { }

  这里返回AQS其中的一个内部类COnditionObject对象,其实这个对象和我们的常规的Object对象锁一样,也是通过wait方法来阻塞等待,notify来唤醒等待线程,只是这里的名称不一样。

 1         /**
 2          * Implements interruptible condition wait.
 3          * <ol>
 4          * <li> If current thread is interrupted, throw InterruptedException.
 5          * <li> Save lock state returned by {@link #getState}.
 6          * <li> Invoke {@link #release} with saved state as argument,
 7          *      throwing IllegalMonitorStateException if it fails.
 8          * <li> Block until signalled or interrupted.
 9          * <li> Reacquire by invoking specialized version of
10          *      {@link #acquire} with saved state as argument.
11          * <li> If interrupted while blocked in step 4, throw InterruptedException.
12          * </ol>
13          */
14         public final void await() throws InterruptedException {
15             if (Thread.interrupted())
16                 throw new InterruptedException();
17             Node node = addConditionWaiter();
18             int savedState = fullyRelease(node);
19             int interruptMode = 0;
20             while (!isOnSyncQueue(node)) {
21                 LockSupport.park(this);
22                 if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
23                     break;
24             }
25             if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
26                 interruptMode = REINTERRUPT;
27             if (node.nextWaiter != null) // clean up if cancelled
28                 unlinkCancelledWaiters();
29             if (interruptMode != 0)
30                 reportInterruptAfterWait(interruptMode);
31         }

  这里的阻塞等待方法叫做await() , 整体的逻辑与ReentrantLock其中加锁的差不多,但是在ConditionObject中存在两个属性 firstWaiter和lastWaiter , 其实对应head 和 tail节点。也是通过这两个属性来维护自身的等待队列的。

        private Node addConditionWaiter() {
            Node t = lastWaiter;
            // If lastWaiter is cancelled, clean out.
            if (t != null && t.waitStatus != Node.CONDITION) {
                unlinkCancelledWaiters();
                t = lastWaiter;
            }
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
            if (t == null)
                firstWaiter = node;
            else
                t.nextWaiter = node;
            lastWaiter = node;
            return node;
        }

   addConditionWaiter 方法逻辑比较简单,通过nextWaiter将当前线程添加到队列尾部。下面重点说一下singl方法

        /**
         * Removes and transfers all nodes.
         * @param first (non-null) the first node on condition queue
         */
        private void doSignalAll(Node first) {
            lastWaiter = firstWaiter = null;
            do {
                Node next = first.nextWaiter;
                first.nextWaiter = null;
                transferForSignal(first);
                first = next;
            } while (first != null);
        }

  其中通过firstWaiter方法获取到第一个等待的线程节点,然后调用起transferForSingal方法

    /**
     * Transfers a node from a condition queue onto sync queue.
     * Returns true if successful.
     * @param node the node
     * @return true if successfully transferred (else the node was
     * cancelled before signal)
     */
    final boolean transferForSignal(Node node) {
        /*
         * If cannot change waitStatus, the node has been cancelled.
         */
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;

        /*
         * Splice onto queue and try to set waitStatus of predecessor to
         * indicate that thread is (probably) waiting. If cancelled or
         * attempt to set waitStatus fails, wake up to resync (in which
         * case the waitStatus can be transiently and harmlessly wrong).
         */
        Node p = enq(node);
        int ws = p.waitStatus;
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }

  这里可以看到,调用了singal之后不是直接唤醒对应的等待线程节点,而是将其添加到同步等待队列中,这是一个很大的不同

posted @ 2022-01-03 15:54  SyrupzZ  阅读(450)  评论(0)    收藏  举报