ReentrantLock and Condition source code analysis

In addition to using the keyword synchronized, Java can also use ReentrantLock to implement the exclusive lock function. Moreover, ReentrantLock is more versatile than synchronized, more flexible to use, and more suitable for complex concurrency scenarios. This article mainly analyzes ReentrantLock from the perspective of source code.

ReentrantLock

1. Locking

code show as below:

ReentrantLock reentrantLock = new ReentrantLock();
reentrantLock.lock();

First look at creating a ReentrantLock object. ReentrantLock has two methods: fair lock and unfair lock. The default is unfair lock.

    public ReentrantLock() {
        sync = new NonfairSync();
    }

Enter the lock method of the NonfairSync class.

   static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

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

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

Set the state from 0 to 1 through CAS, and only one of the multi-threads can be set successfully at the same time, and the thread is set as the current thread. Those that fail will enter the acquire(1) method.

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

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

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

The tryAcquire method will try to acquire it again and return true if successful. Otherwise, it will judge whether the current thread is equal to the thread in ReentrantLock. If it is equal, it means that the lock has been acquired and needs to be entered again, which is equivalent to reentrant lock, and the value is +1.

If the acquisition fails, it will try to join the waiting queue through the acquireQueued(addWaiter(Node.EXCLUSIVE), arg) method.

   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;
        //如果已存在前置节点,设置pred为新增节点的前置节点
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        //如果不存在前置节点,进入enq方法
        enq(node);
        return node;
    }

   private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            //如果tail等于null,新创建head和tail节点
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                //设置head为新增节点的前置节点,并设置tail为新增节点
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

 Then enter the acquireQueued method

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                //获取当前节点的前置节点
                final Node p = node.predecessor();
                //如果前置节点等于head,尝试再次获取
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //如果获取失败,执行shouldParkAfterFailedAcquire方法
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

First, the predecessor node of the current node will be obtained to determine whether the predecessor node is a head node. For example, there are thread A and thread B. If thread A executes the lock method to obtain the lock, thread B will create a Node object. The front node of the Node object of thread B is head, and then try to acquire the lock. If the acquisition is successful, set yourself as the Head node. If it fails, it will enter the shouldParkAfterFailedAcquire(p, node) method.

   private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        //如果ws等于Node.SIGNAL(-1),则返回true应该等待
        if (ws == Node.SIGNAL)
            return true;
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
           //初始节点ws等于0,将0置为Node.SIGNAL(-1)状态,等待获取
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

The Node node has four states, 1-cancellation, 0-initialization, -1-waiting, -2-Condition state

After entering the shouldParkAfterFailedAcquire method for the first time, the waitStatus status of the node node will be set from 0 to -1. returns false. After that, the outside for loop will enter again, and when the node node status is -1, it will return true. Then enter the parkAndCheckInterrupt method.

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

The parkAndCheckInterrupt method is relatively simple, and the thread is blocked by the LockSupport.park method.

2. Unlock

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

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

After thread A acquires the lock, it calls the unlock method. The tryRelease method will be called.

        protected final boolean tryRelease(int releases) {
            //获取ReentrantLock的state数值,减去传入的1
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            //为0则代表释放锁
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            //否则设置state数值,也就是我们lock几次,就要调用unlock几次
            setState(c);
            return free;
        }

The tryRelease method will get the state value of ReentrantLock, minus the incoming 1. If it is reduced to 0, it means that the lock is released. Otherwise, set the state value, that is, how many times we lock, we need to call unlock several times. Returns true when released. Call the unparkSuccessor method.

   private void unparkSuccessor(Node node) {
        //获取head节点状态,如果为-1(等待),置为初始化
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        //获取head节点的下一节点
        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);
    }

The previously blocked thread B will continue to execute

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

 will continue

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                //获取当前节点的前置节点
                final Node p = node.predecessor();
                //如果前置节点等于head,尝试再次获取
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //如果获取失败,执行shouldParkAfterFailedAcquire方法
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

 Executing tryAcquire this time will succeed, and then set the node node as the head node.

 private void cancelAcquire(Node node) {
        // Ignore if node doesn't exist
        if (node == null)
            return;

        node.thread = null;

        // Skip cancelled predecessors
        Node pred = node.prev;
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;

        // predNext is the apparent node to unsplice. CASes below will
        // fail if not, in which case, we lost race vs another cancel
        // or signal, so no further action is necessary.
        Node predNext = pred.next;

        // Can use unconditional write instead of CAS here.
        // After this atomic step, other Nodes can skip past us.
        // Before, we are free of interference from other threads.
        node.waitStatus = Node.CANCELLED;

        // If we are the tail, remove ourselves.
        if (node == tail && compareAndSetTail(node, pred)) {
            compareAndSetNext(pred, predNext, null);
        } else {
            // If successor needs signal, try to set pred's next-link
            // so it will get one. Otherwise wake it up to propagate.
            int ws;
            if (pred != head &&
                ((ws = pred.waitStatus) == 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 {
                unparkSuccessor(node);
            }

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

Condition

Condition is a condition control object in ReentrantLock, similar to wait and notify in Object, but more convenient than wait/notify. An Object object can only have one control condition, which is wait/notify. And ReentrantLock can create multiple Condition conditions to wait and meet multiple conditions to wait and wake up.

First write an example to familiarize yourself with the use of Condition,

public class MoreThreadExecute {

    private static ReentrantLock lock = new ReentrantLock();
    private static Condition a = lock.newCondition();
    private static Condition b = lock.newCondition();
    private static Condition c = lock.newCondition();
    private static AtomicInteger t = new AtomicInteger(0);
    private static final int THREADS = 3;

    public static void main(String[] args) {

        MoreThreadExecute lockTest = new MoreThreadExecute();

        ExecutorService pool = Executors.newFixedThreadPool(THREADS);
        pool.execute(lockTest.new PrintTask("B thread",b, c, "B", 1));
        pool.execute(lockTest.new PrintTask("C thread",c, a, "C", 2));
        pool.execute(lockTest.new PrintTask("A thread",a, b, "A", 0));

    }

    class PrintTask implements Runnable {
        //本线程的条件
        private Condition me;
        //下个线程的条件
        private Condition next;
        private String message;
        private int index;
        private String name;

        public PrintTask(String name,Condition me, Condition next, String message, int index) {
            this.name = name;
            this.me = me;
            this.next = next;
            this.message = message;
            this.index = index;
        }

        @Override
        public void run() {
            while(true) {
                if (t.get() >6){
                    break;
                }
                try {
                    lock.lock();
                    while(t.get() % THREADS != index) {
                        try {
                            System.out.println(name+"invoked, but not me");
                            //不到本线程的时机,则等待
                            me.await();
                        } catch (InterruptedException e) {
                            if (Thread.interrupted()) {
                                e.printStackTrace();
                            }
                        }
                    }
                    System.out.println(message);
                    t.incrementAndGet();
                    //通知其他线程重新获取锁,该他们执行了
                    next.signalAll();
                }finally {
                    lock.unlock();
                }
            }
        }

    }
}

What is executed is to print A, B, and C in sequence.

Let's start Condition source code parsing:

First, we need to create a ReentrantLock object, and then call the lock.newCondition() method to create a Condition.

    public Condition newCondition() {
        return sync.newCondition();
    }
    
    final ConditionObject newCondition() {
        return new ConditionObject();
    }

The ConditionObject class is an internal class in AQS.

Next we look at the lock.await() method.

        public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            //创建条件队列
            Node node = addConditionWaiter();
            //释放全部资源
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

1. The conditional waiting queue will be created first through the addConditionWaiter method

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

Create a Node node, waitState is -2 (conditional waiting)

2. Call the fullyRelease method to release the occupied resources.

    final int fullyRelease(Node node) {
        boolean failed = true;
        try {
            int savedState = getState();
            if (release(savedState)) {
                failed = false;
                return savedState;
            } else {
                throw new IllegalMonitorStateException();
            }
        } finally {
            if (failed)
                node.waitStatus = Node.CANCELLED;
        }
    }

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

Why do you want to release all resources here?

Because the prerequisite for us to call the Object.wait or notify method must be to obtain the lock through synchronized. Here, if we want to await to wait for other conditions to trigger execution, then we need to release all the currently acquired locks. If it fails, an IllegalMonitorStateException will be reported.

3. Use the isOnSyncQueue method to determine whether it is in the synchronization queue.

   final boolean isOnSyncQueue(Node node) {
        if (node.waitStatus == Node.CONDITION || node.prev == null)
            return false;
        if (node.next != null) // If has successor, it must be on queue
            return true;
        /*
         * node.prev can be non-null, but not yet on queue because
         * the CAS to place it on queue can fail. So we have to
         * traverse from tail to make sure it actually made it.  It
         * will always be near the tail in calls to this method, and
         * unless the CAS failed (which is unlikely), it will be
         * there, so we hardly ever traverse much.
         */
        return findNodeFromTail(node);
    }

Node in AQS is divided into two queues, one is a synchronization queue that needs to acquire a lock, and the other is a conditional queue created by Condition. A Node can only be in one kind of queue, if it is in a conditional queue, it will not be in a synchronous queue. After calling the await method for the first time, it will definitely not be in the synchronization queue, so return false. Then call LockSupport.park(this); The method is blocked and waits for wake-up.

4. Assume that another thread calls the signalAll method to wake up at this time.

        public final void signalAll() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignalAll(first);
        }
        protected final boolean isHeldExclusively() {
            // While we must in general read state before owner,
            // we don't need to do so to check if current thread is owner
            return getExclusiveOwnerThread() == Thread.currentThread();
        }

First, it will determine whether the current thread has acquired the lock, and if not, an IllegalMonitorStateException will be thrown. Then the doSignalAll method is called.

    private void doSignalAll(Node first) {
        lastWaiter = firstWaiter = null;
        do {
         Node next = first.nextWaiter;
         first.nextWaiter = null;
         transferForSignal(first);
         first = next;
       } while (first != null);
    }

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

The method will obtain the newly created Node node of the Condition, execute the transferForSignal method, set the status of the Node node from -2 (waiting condition) to 0, and add the Node node to the synchronization queue. And release the thread of the previous node. Continue back to where you were blocked

        public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            //创建条件队列
            Node node = addConditionWaiter();
            //释放全部资源
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

 It will enter the isOnSyncQueue method again to determine whether the current node enters the synchronization queue.

    final boolean isOnSyncQueue(Node node) {
        if (node.waitStatus == Node.CONDITION || node.prev == null)
            return false;
        if (node.next != null) // If has successor, it must be on queue
            return true;
        return findNodeFromTail(node);
    }

    private boolean findNodeFromTail(Node node) {
        Node t = tail;
        for (;;) {
            if (t == node)
                return true;
            if (t == null)
                return false;
            t = t.prev;
        }
    }

By the previous signalAll method, the waitState of the queue has been changed from -2 to -1, and the node has been added from the waiting queue to the synchronization queue. Here you can find that the synchronization queue contains the current Node from the findNodeFromTail method, so it returns true. The while loop condition is not met.

6. Enter the acquireQueued method to try to acquire the lock.

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

The method of trying to acquire is the same as that of locking and waiting. node node attempt to get. If the lock is not acquired, continue to block and wait for release. If it is obtained, continue to execute downward.

7. Then the if (node.nextWaiter != null) judgment will be executed.

        if (node.nextWaiter != null) // clean up if cancelled
            unlinkCancelledWaiters();
       
        private void unlinkCancelledWaiters() {
            Node t = firstWaiter;
            Node trail = null;
            while (t != null) {
                Node next = t.nextWaiter;
                if (t.waitStatus != Node.CONDITION) {
                    t.nextWaiter = null;
                    if (trail == null)
                        firstWaiter = next;
                    else
                        trail.nextWaiter = next;
                    if (next == null)
                        lastWaiter = trail;
                }
                else
                    trail = t;
                t = next;
            }
        }

The unlinkCancelledWaiters method clears the nodes whose node status is not CONDITION on the condition queue.

The unlinkCancelledWaiters method is actually useless, because if the await times out or the thread is interrupted, it will try to set the waitState of the node to 0 and add it to the synchronization queue.

        while (!isOnSyncQueue(node)) {
            LockSupport.park(this);
            //假设设立被其他线程调用interrupt阻断或者被唤醒,进入checkInterruptWhileWaiting方法
            if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                break;
        }

        private int checkInterruptWhileWaiting(Node node) {
            return Thread.interrupted() ?
                    (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
                    0;
        }
        //被阻断进入transferAfterCancelledWait
        final boolean transferAfterCancelledWait(Node node) {
            //会将该node节点置为0
            if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
                enq(node);
                return true;
            }
            /*
             * If we lost out to a signal(), then we can't proceed
             * until it finishes its enq().  Cancelling during an
             * incomplete transfer is both rare and transient, so just
             * spin.
             */
            while (!isOnSyncQueue(node))
                Thread.yield();
            return false;
        }

Even if the unlinkCancelledWaiters method is not called here, these nodes will not be added to the synchronization queue when the signalAll wakeup method is executed. Just to clean up the nodes whose status is not CONDITION on the waiting queue.

    private void doSignalAll(Node first) {
        lastWaiter = firstWaiter = null;
        do {
         Node next = first.nextWaiter;
         first.nextWaiter = null;
         transferForSignal(first);
         first = next;
       } while (first != null);
    }

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

Summarize:

Condition itself also maintains a queue. The function of this queue is to maintain a queue waiting for the signal signal. The functions of the two queues are different. In fact, each thread will only exist in one of the above two queues at the same time. There will be threads 1 and 2, and the specific process is as follows.

Thread 1:

  • When thread 1 calls reentrantLock.lock, it holds the lock.

  • Thread 1 calls the await method, enters the [conditional waiting queue], and releases the lock at the same time.

  • Thread 1 gets the signal of thread 2 and enters into [synchronous waiting queue] from [conditional waiting queue].

Thread 2:

  • When thread 2 calls reentrantLock.lock, because the lock is held by thread 1, it enters [synchronous waiting queue].

  • Since thread 1 releases the lock, thread 2 is removed from [synchronous waiting queue] and acquires the lock. Thread 2 calls the signal method, causing thread 1 to wake up.

  • Thread 2 calls reentrantLock.unlock. Thread 1 acquires the lock and continues the loop.


Conditional Waiting Queue

The conditional waiting queue refers to a queue maintained by Condition itself, which is different from the [synchronous waiting queue] of AQS. It has the following characteristics:

  • Nodes that want to join the [ Conditional Waiting Queue ] cannot be in the [ Synchronous Waiting Queue ].
  • Nodes removed from the [ Conditional Waiting Queue ] will enter the [ Synchronous Waiting Queue ].
  • A lock object can only have one [ synchronous waiting queue ], but can have multiple [ conditional waiting queues ].

Guess you like

Origin blog.csdn.net/yytree123/article/details/108856237