Introduction of Lock and ReentrantLock in JUC and Source Code Analysis

Posted by wshell on Fri, 12 Jul 2019 00:13:45 +0200

Links to the original text

Lock framework is a new addition to jdk1.5. It works as synchronized, so it can be compared with synchronized when learning. In this paper, we first make a simple comparison with synchronized, and then analyze the Lock interface and the source code and description of ReentrantLock. Specific analysis of other Lock implementations will be introduced later.

Lock framework and synchronized

The role and usage of synchronized is not specified, and should be familiar with it. Lock has the same meaning as synchronized, but it has more functions than synchronized. From the definition of Lock interface alone, it has more functions than synchronized.

  • Interruptible acquisition locks are threads that acquire locks that respond to interruptions.
  • You can try to acquire locks, that is, non-blocking acquisition locks. A thread can try to acquire locks. If it succeeds, it holds locks and returns true, otherwise it returns false.
  • An attempt to acquire a lock with a timeout means that when trying to acquire a lock, there will be a timeout. When the lock has not been acquired, it will return false.

In addition to definitions, the Lock framework differs from synchronized in that:

  • Lock needs to display the lock and release the lock, and must release the lock in final. synchronized does not require us to care about the release of locks.
  • Lock interface does not define the method of fairness, but uses AQS to achieve the fairness of locks in specific implementation classes.

Lock interface source code

public interface Lock {

    //Get the lock, get the lock and return
    //Be careful to remember to release the lock
    void lock();

    //Interruptible access lock
    //When a lock is acquired, the thread can respond to interruptions if it is waiting to acquire the lock.
    void lockInterruptibly() throws InterruptedException;

    //Attempt to acquire a lock. When a thread acquires a lock, the success or failure of the acquisition will be returned immediately.
    //Not always waiting to get locks
    boolean tryLock();

    //Attempt to acquire locks with timeout
    //Acquiring a lock within a certain period of time returns true
    //Interrupted during this period, it will return.
    //During this time, if the lock is not acquired, false will be returned.
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    //Release lock
    void unlock();

    //Get a Condition object.
    Condition newCondition();
}

The definition of Lock interface is not complicated, such as acquisition lock release lock and non-blocking acquisition lock. In fact, imagine everyday use, it's probably the same. When I get the lock, release the lock, and get the lock, I don't get the lock. I turn around and try again. After a certain time, I don't want to go. There's not much to say about the definition of the interface, so let's look at the implementation of the Lock interface.

Implementation of Lock Interface

The main implementation of Lock interface is ReentrantLock re-entry lock. Segment in Concurrent HashMap inherits ReentrantLock. WriteLock and ReadLock in ReentrantReadWriteLock also implement Lock interface.

ReentrantLock

ReentrantLock is a re-entrant mutex, equivalent to synchronized, but more flexible than synchronized, reducing the probability of deadlocks. We said above that the Lock framework provides a fair lock mechanism, which is implemented in ReentrantLock to provide a fair lock mechanism, defaulting to unfair locks.

Before continuing to look at the implementation of ReentrantLock's various methods, first of all, we need to understand how to achieve fair and unfair locks internally. In fact, it's easy to think about it. For example, I am a re-entrantLock. You want to get fair or unfair from me, but you can't say what I am. There is a balance (AQS), which is generally recognized as a machine to achieve fairness and inequity. If you ask for it, I will give it a word, and he will operate it, and then I will give you the results. (The more descriptive, the more complicated!). Almost all operations in ReentrantLock are handed over to Sync for implementation.

There is no introduction about AQS here. There is an introduction in the special article of AQS. Please refer to it by yourself. Next, look at the two locks I have, fair and unfair.

Sync

Sync is a fair and unfair base class. If you can't see the code directly, you can first see the following fair and unfair parsing, and then come back:

//Inherited from AQS
abstract static class Sync extends AbstractQueuedSynchronizer {

    //By specific subclasses, that is, fair and unfair are implemented differently
    abstract void lock();

    //Unfair Attempt to Acquire
    final boolean nonfairTryAcquire(int acquires) {
        //Current thread
        final Thread current = Thread.currentThread();
        //Current status of AQS synchronizer
        int c = getState();
        //The state is 0, indicating that no one has acquired the lock.
        if (c == 0) {
            //Attempt to Achieve, Achieve Success Set to Monopoly Mode
            if (compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        //The explanation here is as fair as the one below.
        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;
    }

    //Trying to release
    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;
    }

    //Is it exclusive?
    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();
    }

    final ConditionObject newCondition() {
        return new ConditionObject();
    }

    // Methods relayed from outer class
    //Get the holder thread
    final Thread getOwner() {
        return getState() == 0 ? null : getExclusiveOwnerThread();
    }
    //Get reentrant number
    final int getHoldCount() {
        return isHeldExclusively() ? getState() : 0;
    }
    //Is it locked?
    final boolean isLocked() {
        return getState() != 0;
    }

    //
    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();
        setState(0); // reset to unlocked state
    }
}

FairSync

The realization of fair lock and the realization of fair are dealt with in this way.

//Inherited from Sync
static final class FairSync extends Sync {
    //Acquisition locks
    //The fair lock method is handed over to the AQS acquire method for processing.
    //acquire method adopts exclusive mode and ignores interruption
    //The implementation of AQS acquisition lock is to use tryAcquire method to acquire the lock first, and join the queue if it can not be acquired, and try to acquire it until it returns successfully.
    //The implementation of tryAcquire is also implemented by specific subclasses. The following tryAcquire method is a fair tryAcquire implementation.
    //
    final void lock() {
        //Parametric 1 is the synchronization state of AQS
        //First of all, we understand the definition of synchronization state in AQS.
        //0 denotes that the lock was not acquired, 1 denotes that the lock has been acquired, and greater than 1 denotes the number of reentry.
        //If we want to get the lock, we must want the current synchronization state to be zero, and then we change the state to 1, so the lock is ours.
        acquire(1);
    }

    //Implementation of Fair tryAcquire Method
    protected final boolean tryAcquire(int acquires) {
        //Current thread
        final Thread current = Thread.currentThread();
        //Getting the synchronization status of AQS
        int c = getState();
        //If the state is zero, no one else gets the lock.
        if (c == 0) {
            //hasQueuedPredecessors queries whether there are other threads waiting longer to acquire locks than the current thread
            //CompeAndSetState uses cas e to set the state, expected to be 0, and we want to set the value to 1
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                //If we wait for the longest acquisition time (that's fair, I wait for the longest time, I should be the first to be served)
                //And the cas setup succeeded, indicating that we got the lock.
                //Set the current thread to exclusive access
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        //Next is the case where the state is not zero, that is, 1, or greater than 1.
        //If the current thread and the exclusive thread are the same
        else if (current == getExclusiveOwnerThread()) {
            //The current state plus the parameter 1 we want to get
            //Now it's the reentrant number.
            int nextc = c + acquires;
            //state is a 32-bit integer, less than 0, indicating that the number of reentrants exceeds the maximum number.
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            //Set the current state
            setState(nextc);
            return true;
        }
        return false;
    }
}

You can see that fair tryAcquire calls only once at the beginning of acquiring the lock, gets it as soon as it gets it, or increases the number of reentrants it has acquired, and returns false if it does not. If it returns false, AQS will join it in the queue and try to get it all the time.

NonfairSync

//Also inherited from Sync
static final class NonfairSync extends Sync {
    //Unfair access lock
    final void lock() {
        //First try to get the lock directly
        //If a lock can be acquired, it is set to exclusive mode
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            //If it is not directly accessible, it will be accessed in the same process as fair lock.
            //tryAcquire is below
            acquire(1);
    }
    //This is an unfair tryAcquire, calling the nofairTryAcquire method in Sync directly.
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

The distinction between fairness and unfairness

After reading the code above, it's a bit vague. I feel that the code is almost the same. What's the difference between fairness and unfairness? First look at fair lock acquisition. Fair lock acquisition calls the acquire method directly. The acquire method does not directly acquire the lock, but calls the fair tryAcquire method. Fair tryAcquire method first acquires the current synchronizer state. If no one uses the synchronizer, that is, the state is 0, it will first judge whether there is a synchronizer or not. Someone waits longer than I do, and sometimes I can't get the lock, but let others go first; if I wait the longest, I use CAS to change the status and get the lock.

The unfair realization is that I come up and use CAS to get locks directly without asking whether others have been waiting for a long time. I get them, which is mine. I can't get them. Then I call the acquire method, and then the acquire method calls the unfair tryAcquire method. The unfair tryAcquire method is also very direct, if not, the acquire method calls the unfair tryAcquire method. The current lock is unused, i. e. state0. I don't care if anybody waits longer than I do, I go to get it and set up exclusivity.

Fair lock, get the lock first to try, if not queue up, after my turn, but also to ask if there is a longer waiting time. Unfair locks are not queued, directly up, not get, I also try to get, try to get when I still go directly up, regardless of other people.

Understanding fair and unfair locks and looking at other methods is not so difficult.

ReentrantLock constructor

//As you can see, default is unfair
public ReentrantLock() {
    sync = new NonfairSync();
}

Fairness can also be specified

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

lock method

//lock method for calling a fair or unfair synchronizer directly
public void lock() {
    sync.lock();
}

lock method has three cases:

  • If the lock is not held by other threads, the current thread immediately gets the lock and returns with the synchronizer status set to 1.
  • If the current thread already holds a lock, the state is added to 1 and returned immediately.
  • If the lock is held by another thread, the current thread hangs until the lock is acquired and then returns, with the synchronizer set to 1.

lockInterruptibly Method

//Interruptible access lock
public void lockInterruptibly() throws InterruptedException {
    //Method of calling AQS
    sync.acquireInterruptibly(1);
}

Get the lock, which can be interrupted by Thread.interrupt. There are also three situations:

  • If the lock is not held by other threads, the current thread immediately gets the lock and returns with the synchronizer status set to 1.
  • If the current thread already holds a lock, the state is added to 1 and returned immediately.
  • If the lock is held by other threads, the current thread will hang up to acquire the lock. In this process, there are two cases:

    • The current thread gets the lock, returns, and the synchronizer status is set to 1.
    • When the current thread is interrupted, an InterruptedException exception is thrown and the interrupt state is cleared.

tryLock method

//Attempts to acquire locks will not be blocked, and success or failure will be returned directly.
public boolean tryLock() {
    //Acquisition of unfair locks used
    return sync.nonfairTryAcquire(1);
}

Using unfair lock acquisition, if used fairly, the acquisition time also needs to judge whether other people have been for a long time, but not fair TryAcquire, can be obtained directly, can not get back false, more direct.

If you don't want to undermine fairness, you can use the tryLock method with timeout.

tryLock method with timeout

//In time out, and without interruption, the lock is not held by other threads, and the lock is immediately retrieved and returned.
public boolean tryLock(long timeout, TimeUnit unit)
        throws InterruptedException {
    return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}

If a fair lock is used, and if there are other threads that wait longer, even if no one holds the lock now, the current thread will not get the lock and give the wait longer to get it.

unlock method

//Release lock
//Call AQS directly to release the lock
public void unlock() {
    sync.release(1);
}

New Condition Method

//Return a new Condition instance
public Condition newCondition() {
    return sync.newCondition();
}

The method of returning the Condition instance is actually the same as the wait, notify, notifyAll method of Object.

Other methods

//Number of reentry
 public int getHoldCount() {
    return sync.getHoldCount();
}

//Is the lock held by the current thread?
public boolean isHeldByCurrentThread() {
    return sync.isHeldExclusively();
}

//Query whether the lock is held by any thread
public boolean isLocked() {
    return sync.isLocked();
}

//Is it a fair lock?
public final boolean isFair() {
    return sync instanceof FairSync;
}

//Returns the thread that currently has a lock
protected Thread getOwner() {
    return sync.getOwner();
}

//Query if there are threads queuing for locks
public final boolean hasQueuedThreads() {
    return sync.hasQueuedThreads();
}

//Query whether a given thread is waiting to acquire a lock
public final boolean hasQueuedThread(Thread thread) {
    return sync.isQueued(thread);
}

//Get the length of the queue waiting to acquire the lock
public final int getQueueLength() {
    return sync.getQueueLength();
}

//Gets all threads waiting to acquire locks
protected Collection<Thread> getQueuedThreads() {
    return sync.getQueuedThreads();
}

//Query if there is a thread waiting for a given Condition
public boolean hasWaiters(Condition condition) {
    if (condition == null)
        throw new NullPointerException();
    if (!(condition instanceof AbstractQueuedSynchronizer.ConditionObject))
        throw new IllegalArgumentException("not owner");
    return sync.hasWaiters((AbstractQueuedSynchronizer.ConditionObject)condition);
}

//Get the length of the queue waiting for a Condition
public int getWaitQueueLength(Condition condition) {
    if (condition == null)
        throw new NullPointerException();
    if (!(condition instanceof AbstractQueuedSynchronizer.ConditionObject))
        throw new IllegalArgumentException("not owner");
    return sync.getWaitQueueLength((AbstractQueuedSynchronizer.ConditionObject)condition);
}

//Get all sets of threads waiting for a Condition
protected Collection<Thread> getWaitingThreads(Condition condition) {
    if (condition == null)
        throw new NullPointerException();
    if (!(condition instanceof AbstractQueuedSynchronizer.ConditionObject))
        throw new IllegalArgumentException("not owner");
    return sync.getWaitingThreads((AbstractQueuedSynchronizer.ConditionObject)condition);
}

The difference between ReentrantLock and synchronized

ReentrantLock is similar to synchronized, but it has more functions than synchronized, such as interruptible locks, locks with timeouts, and non-blocking access to locks. ReentrantLock also provides conditional conditions, similar to Object wait/notify, but more appropriate where there are multiple conditional variables and highly competitive locks.

In addition, AQS is the focus, we must learn several times, learn, in order to master the lock (I do not understand!).

Other implementations, such as ReadLock and WriteLock of ReentrantReadWriteLock and Segment s in Concurrent HashMap, will be introduced separately.

Topics: Java less