Java Review - Concurrent Programming_ Principle of exclusive lock ReentrantLock & source code analysis

Posted by gasper000 on Sun, 05 Dec 2021 13:19:50 +0100

Article catalog

Synchronized vs ReentrantLock

ReentrantLock overview

ReentrantLock is a reentrant exclusive lock. At the same time, only one thread can acquire the lock. Other threads acquiring the lock will be blocked and put into the AQS blocking queue of the lock.

The class diagram structure is as follows

  • The bottom layer is implemented based on AQS, and the lock method of ReentrantLock is delegated to the lock method that depends on sync
  • AQS is a typical template method design pattern. The parent class (AQS) defines the skeleton and internal operation details, and the specific rules are implemented by the child classes

As can be seen from the class diagram, ReentrantLock is finally implemented using AQS, and its internal is a fair lock or an unfair lock according to the parameters. The default is an unfair lock.

    /**
     * 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();
    }

The Sync class directly inherits from AQS, and its subclasses NonfairSync and FairSync implement the unfair and fair policies of obtaining locks respectively.

Here, the state value of AQS indicates the number of reentrant times that the thread obtains the lock.

  • By default, a value of 0 for state indicates that the current lock is not held by any thread.
  • When a thread acquires the lock for the first time, it will try to use CAS to set the value of state to 1. If CAS succeeds, the current thread acquires the lock, and then records that the holder of the lock is the current thread.
  • When the thread does not release the lock, after acquiring the lock for the second time, the status value is set to 2, which is the number of reentrants.
  • When the thread releases the lock, it will try to use CAS to reduce the status value by 1. If the status value is 0 after reducing 1, the current thread releases the lock.

Acquire lock

void lock()

When a thread calls the method, it indicates that the thread wants to obtain the lock.

  • If the lock is not currently occupied by other threads and the current thread has not acquired the lock before, the current thread will acquire the lock, set the owner of the current lock to the current thread, set the AQS status value to 1, and then return directly.
  • If the current thread has acquired the lock before, it will simply add 1 to the AQS status value and return.
  • If the lock is already held by another thread, the thread calling the method will be blocked and suspended after being put into the AQS queue.

The locking process is as follows

  /**
     * Acquires the lock.
     *
     * Acquires the lock if it is not held by another thread and returns
     * immediately, setting the lock hold count to one.
     *
     * If the current thread already holds the lock then the hold
     * count is incremented by one and the method returns immediately.
     *
     * If the lock is held by another thread then the
     * current thread becomes disabled for thread scheduling
     * purposes and lies dormant until the lock has been acquired,
     * at which time the lock hold count is set to one.
     */
    public void lock() {
        sync.lock();
    }

In the above code, the lock() of ReentrantLock is delegated to the sync class. Select whether the implementation of sync is NonfairSync or FairSync according to the creation of the ReentrantLock constructor. This lock is a non fair lock or a fair lock.

Implementation code of unfair lock

Let's first look at the situation of NonfairSync, a subclass of sync, that is, in case of unfair lock.

   /**
     * Sync object for non-fair locks
     */
    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 cas set status value
            if (compareAndSetState(0, 1))
               // Set thread to exclusive lock thread
                setExclusiveOwnerThread(Thread.currentThread());
            else
               // 2. Call the acquire method of AQS
                acquire(1);
        }

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

In code (1), because the default AQS status value is 0, the first thread calling Lock will set the status value to 1 through CAS. If CAS succeeds, it means that the current thread has obtained the Lock, and then setExclusiveOwnerThread sets the Lock holder to be the current thread

If another thread calls the lock method to obtain the lock at this time, CAS will fail and then call the acquire method of AQS. Note that the transfer parameter is 1, and the core code of AQS acquire is pasted here.

    /**
     * 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) {
    	// 3 call the tryAcquire method overridden by ReentrantLock
        if (!tryAcquire(arg) &&
        	// If tryAcquire returns false, the current thread will be put into the AQS blocking queue
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

AQS does not provide an available tryAcquire method. The tryAcquire method needs to be customized by its subclasses, so code (3) here will call the tryAcquire method overridden by ReentrantLock. Let's look at the code of unfair lock first.

     protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
  /**
         * 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();
            // 4. The current AQS status value state is 0
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {// 5 the current thread is the holder of the lock
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
  • Code (4) will check whether the status value of the current lock is 0. If it is 0, it means that the lock is currently idle. Then try CAS to obtain the lock, set the AQS status value from 0 to 1, and set the holder of the current lock as the current thread, and then return true.
  • If the current status value is not 0, it indicates that the lock has been held by a thread. Therefore, code (5) checks whether the current thread is the holder of the lock. If the current thread is the holder of the lock, the status value increases by 1 and returns true. It should be noted that nextc < 0 indicates that the number of reentrant times has overflowed.
  • If the current thread is not the lock holder, false is returned, and then it will be put into the AQS blocking queue.

How does unfair lock reflect?

After reading the implementation code of unfair lock, let's go back and see how unfairness is reflected here.

First, unfairness means that the thread trying to acquire the lock first does not necessarily acquire the lock first than the thread trying to acquire the lock later.

Here, suppose that when thread A calls the lock () method, it executes the code (4) of nonfairTryAcquire and finds that the current state value is not 0, so execute the code (5). If it finds that the current thread is not the thread holder, the execution code (6) returns false, and then the current thread is put into the AQS blocking queue.

At this time, thread B also calls the lock() method to execute the code (4) of nonfairTryAcquire. It is found that the current state value is 0 (assuming that other threads occupying the lock release the lock), so it obtains the lock through CAS settings. Obviously, thread A requests to obtain the lock first, which is the embodiment of unfairness.

Here, before acquiring the lock, thread B does not check whether there are threads in the current AQS queue that request the lock earlier than itself, but uses the preemption strategy.

How does fairness lock achieve fairness

So let's take a look at how fair locks achieve fairness. For a fair lock, you only need to see the tryAcquire method rewritten by FairSync.

    /**
     * Sync object for fair locks
     */
    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();
            int c = getState();
            // 7. The current state is 0 
            if (c == 0) {
            	// 8 equity strategy
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // 9 the current thread is the lock holder 
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }

As shown in the above code, the fair tryAcquire policy is similar to the unfair one. The difference is that code (8) adds the hasQueuedPredecessors method before setting CAS, which is the core code to realize fairness,

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

In the above code, if the current thread node has a precursor node, it returns true; otherwise, if the current AQS queue is empty or the current thread node is the first node of AQS, it returns false.

  • If h==t, it indicates that the current queue is empty and returns false directly;
  • If h= T and s==null indicates that an element is to be queued as the first node of AQS (recall that the first element of enq function is a two-step operation: first create a sentinel head node, and then insert the first element behind the sentinel node), then return true
  • If h= T and S= Null and s.thread= Thread. Currentthread() indicates that the first element in the queue is not the current thread, and returns true.

void lockInterruptibly() method

This method is similar to the lock() method. The difference is that it responds to interrupts. When the current thread calls this method, if other threads call the interrupt () method of the current thread, the current thread will throw an InterruptedException exception and return.

 /**
     * Acquires the lock unless the current thread is
     * {@linkplain Thread#interrupt interrupted}.
     *
     * Acquires the lock if it is not held by another thread and returns
     * immediately, setting the lock hold count to one.
     *
     * If the current thread already holds this lock then the hold count
     * is incremented by one and the method returns immediately.
     *
     * If the lock is held by another thread then the
     * current thread becomes disabled for thread scheduling
     * purposes and lies dormant until one of two things happens:
     *
     * 
     *
     * The lock is acquired by the current thread; or
     *
     * Some other thread {@linkplain Thread#interrupt interrupts} the
     * current thread.
     *
     * 
     *
     * If the lock is acquired by the current thread then the lock hold
     * count is set to one.
     *
     * If the current thread:
     *
     * 
     *
     * has its interrupted status set on entry to this method; or
     *
     * is {@linkplain Thread#interrupt interrupted} while acquiring
     * the lock,
     *
     * 
     *
     * then {@link InterruptedException} is thrown and the current thread's
     * interrupted status is cleared.
     *
     * In this implementation, as this method is an explicit
     * interruption point, preference is given to responding to the
     * interrupt over normal or reentrant acquisition of the lock.
     *
     * @throws InterruptedException if the current thread is interrupted
     */
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }
 /**
     * Acquires in exclusive mode, aborting if interrupted.
     * Implemented by first checking interrupt status, then invoking
     * at least once {@link #tryAcquire}, returning on
     * success.  Otherwise the thread is queued, possibly repeatedly
     * blocking and unblocking, invoking {@link #tryAcquire}
     * until success or the thread is interrupted.  This method can be
     * used to implement method {@link Lock#lockInterruptibly}.
     *
     * @param arg the acquire argument.  This value is conveyed to
     *        {@link #tryAcquire} but is otherwise uninterpreted and
     *        can represent anything you like.
     * @throws InterruptedException if the current thread is interrupted
     */
    public final void acquireInterruptibly(int arg)
            throws InterruptedException {
		// If the current thread is interrupted, an exception is thrown directly
        if (Thread.interrupted())
            throw new InterruptedException();
		// Trying to get resources
        if (!tryAcquire(arg))
        	// Call the method that AQS can be interrupted
            doAcquireInterruptibly(arg);
    }

boolean tryLock() method

Try to acquire a lock. If the lock is not currently held by another thread, the current thread acquires the lock and returns true. Otherwise, it returns false. Note that this method does not cause the current thread to block.

  /**
     * Acquires the lock only if it is not held by another thread at the time
     * of invocation.
     *
     * Acquires the lock if it is not held by another thread and
     * returns immediately with the value {@code true}, setting the
     * lock hold count to one. Even when this lock has been set to use a
     * fair ordering policy, a call to {@code tryLock()} will
     * immediately acquire the lock if it is available, whether or not
     * other threads are currently waiting for the lock.
     * This "barging" behavior can be useful in certain
     * circumstances, even though it breaks fairness. If you want to honor
     * the fairness setting for this lock, then use
     * {@link #tryLock(long, TimeUnit) tryLock(0, TimeUnit.SECONDS) }
     * which is almost equivalent (it also detects interruption).
     *
     * If the current thread already holds this lock then the hold
     * count is incremented by one and the method returns {@code true}.
     *
     * If the lock is held by another thread then this method will return
     * immediately with the value {@code false}.
     *
     * @return {@code true} if the lock was free and was acquired by the
     *         current thread, or the lock was already held by the current
     *         thread; and {@code false} otherwise
     */
    public boolean tryLock() {
        return sync.nonfairTryAcquire(1);
    }
    /**
         * 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;
        }

The above code is similar to the tryAcquire() method code of unfair lock, so tryLock() uses unfair policy.

boolean tryLock(long timeout, TimeUnit unit)

The difference between trying to acquire a lock and tryLock () is that it sets the timeout. If the timeout is too long to acquire the lock, false is returned.

   public boolean tryLock(long timeout, TimeUnit unit)
            throws InterruptedException {
            // Call tryAcquireNanos of AQS
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }

Release lock

void unlock() method

  • Try to release the lock. If the current thread holds the lock, calling this method will reduce the AQS status value held by the thread by 1. If the current status value after subtracting 1 is 0, the current thread will release the lock, otherwise it will only be reduced by 1.
  • If the current thread does not hold the lock and calls the method, an IllegalMonitorStateException will be thrown
  /**
     * Attempts to release this lock.
     *
     * If the current thread is the holder of this lock then the hold
     * count is decremented.  If the hold count is now zero then the lock
     * is released.  If the current thread is not the holder of this
     * lock then {@link IllegalMonitorStateException} is thrown.
     *
     * @throws IllegalMonitorStateException if the current thread does not
     *         hold this lock
     */
    public void unlock() {
        sync.release(1);
    }
    /**
     * 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;
    }
 protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            // 11 if it is not the lock holder, the call throws the IllegalMonitorStateException
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            // 12 If the number of reentrant times is 0, the lock holding thread is cleared
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            // 13 set the number of reentrants to the original value minus 1
            setState(c);
            return free;
        }
  • As shown in code (11), if the current thread is not the lock holder, an exception is thrown directly
  • Otherwise, check whether the status value is 0. If it is 0, it indicates that the current thread wants to give up the ownership of the lock, and execute code (12) to set the current lock holder to null.
  • If the status value is not 0, only the number of reentrant times of the current thread to the lock is reduced by 1.

Demo: use ReentrantLock to implement a simple thread safe list

import java.util.ArrayList;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author Small craftsman
 * @version 1.0
 * @description: TODO
 * @date 2021/12/4 22:05
 * @mark: show me the code , change the world
 */
public class ReentrantLockList {

    //Thread unsafe List
    private ArrayList<String> list = new ArrayList<String>();
    
    //Exclusive lock. It is a non fair lock by default. Passing in true can be a fair lock
    private volatile ReentrantLock lock = new ReentrantLock();

    //Add elements to the collection
    public void add(String str) {
        lock.lock();
        try {
            list.add(str);
        } finally {
            lock.unlock();
        }
    }

    //Delete elements in the collection
    public void remove(String str) {
        lock.lock();
        try {
            list.remove(str);
        } finally {
            lock.unlock();
        }
    }

    //Gets an element in the collection according to the index
    public String get(int index) {
        lock.lock();
        try {
            return list.get(index);
        } finally {
            lock.unlock();
        }
    }
}

The above code ensures that only one thread can modify the array at the same time by locking before operating the array element, but only one thread can access the array element.

Also, use diagrams to deepen understanding.

If Thread1, Thread2 and Thread3 attempt to obtain the exclusive lock ReentrantLock at the same time, assuming that Thread1 obtains it, Thread2 and Thread3 will be converted into Node nodes and put into the AQS blocking queue corresponding to ReentrantLock, and then blocked and suspended. As shown above.

Suppose that after Thread1 gets the lock, it calls the conditional variable 1 created by the corresponding lock, then Thread1 releases the lock that is obtained. Then the current thread is converted to the conditional queue with the Node node inserting conditional variable 1.

Since Thread1 releases the lock, Thread2 and Thread3 blocked in the AQS queue have the opportunity to obtain the lock. If a fair policy is used, Thread2 will obtain the lock and remove the Node corresponding to Thread2 from the AQS queue. As shown below

Summary

We combed the implementation principle of ReentrantLock. The bottom layer of ReentrantLock is a reentrant exclusive lock implemented by AQS.

Here, the AQS State value of 0 indicates that the current lock is idle, and a value greater than or equal to 1 indicates that the lock has been occupied. There are fair and unfair implementations inside the lock. By default, it is an unfair implementation.

In addition, because the lock is exclusive, only one thread can acquire the lock at a time.