Java - ReentrantLock ReentrantLock Implementation Principle

Java - ReentrantLock ReentrantLock Implementation Principle

At the implementation level, in addition to relying on the CAS (compareAndSet) method, it also depends on some methods in the class LockSupport.


1. LockSupport

The class LockSupport is located in the package java.util.concurrent.locks, and its basic methods are

public static void park()
public static void parkNanos(long nanos)
public static void parkUntil(long deadline)
public static void unpark(Thread thread)

The parkmethod will make the current thread give up the CPU and enter the waiting (WAITING) state, and the operating system will no longer schedule it. Until another thread calls a unparkmethod on it, where the unparkmethod restores the thread specified by the parameter to a runnable state:

public class LockSupportTest {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread (){
            public void run(){
                LockSupport.park(); //放弃CPU
                System.out.println("exit");
            }
        };
        t.start(); //启动线程
        Thread.sleep(1000); //睡眠1秒保证子线程先运行
        System.out.println("after 1 second");
        LockSupport.unpark(t);
    }

}

In the above code, first the main thread starts a sub-thread t, and then thread t calls park to enter the blocking state. The main thread sleeps for 1 second to ensure that the child thread has called the LockSupport.park();method . Finally, the main thread calls the unparkmethod to make the child thread t resume running, and print the output on the console.

The park method is different from the Thread.yield() method. yield just tells the operating system that it can let other threads run first, but it can still be running. The park method is to give up the running qualification of the thread, so that the thread enters the waiting state of WAITING.

At the same time, the park method is responsive to interruptions . When an interruption occurs, the park method returns and resets the interruption status of the thread.

There are two variants of the park method

  1. parkNanos: You can specify the maximum time to wait, the parameter is the number of nanoseconds relative to the current time.
  2. parkUntil: You can specify the longest waiting time, the parameter is absolute time, the number of milliseconds relative to the epoch.

When the wait times out, the method returns. At the same time, there are some other variants. You can specify an object to indicate that the object is waiting for it to facilitate debugging. Generally, the parameter passed is this, for example:

public static void park(Object blocker)

LockSupport has a method that returns a thread's blocker object:

/**
 * Returns the blocker object supplied to the most recent
 * invocation of a park method that has not yet unblocked, or null
 * if not blocked.  The value returned is just a momentary
 * snapshot -- the thread may have since unblocked or blocked on a
 * different blocker object.
 *
 * @param t the thread
 * @return the blocker
 * @throws NullPointerException if argument is null
 * @since 1.6
 */
public static Object getBlocker(Thread t) {
    if (t == null)
        throw new NullPointerException();
    return UNSAFE.getObjectVolatile(t, parkBlockerOffset);
}

二、AQS

In addition to reentrant locks in Java, there are other concurrency tools, such as ReentrantReadWriteLock, Semaphore, CountDownLatch, etc. Their implementations have other similarities. In order to reuse code, Java provides an abstract class AbstractQueuedSynchronizer, referred to as AQS , which simplifies the implementation of concurrent tools.

Only a brief introduction to AQS is given here, a state is encapsulated in AQS, and methods for querying and setting the state are provided to subclasses:

private volatile int state;
protected final int getState()
protected final void setState(int newState)
protected final boolean compareAndSetState(int expect, int update)

When used for lock implementation, AQS can save the current thread holding lock, and provides methods to query and set:

private transient Thread exclusiveOwnerThread;
protected final void setExclusiveOwnerThread(Thread t)
protected final Thread getExclusiveOwnerThread()

AQS maintains a waiting queue internally, and uses the CAS method to implement a non-blocking algorithm for updating.

3. ReentrantLock

When ReentrantLock uses AQS internally, there are mainly the following three inner classes.

abstract static class Sync extends AbstractQueuedSynchronizer
static final class NonfairSync extends Sync
static final class FairSync extends Sync

Sync is an abstract class, NonfairSync is the class used when fair is false, and FairSync is the class that needs to be used when fair is true.

Inside ReentrantLock there is a Sysc member:

private final Sync sync;

The member is initialized in the constructor, for example:

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

Next, look at the implementation of the basic method lock/unlock in ReentrantLock.

3.1 lock

Because sync defaults to NonfairSync, and the non-fair method is more commonly used. The lock code of NonfairSync is:

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

ReentrantLock uses state to indicate whether it is locked and the number of holdings. If it is not currently locked, the lock is obtained immediately, otherwise the acquire(1);method is called to obtain the lock, which acquireis the method in AQS, which is implemented as:

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

This code calls the tryAcquire method to try to acquire the lock. This method needs to be overridden by subclasses. The implementation in NonFairSync is:

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

The nonfairTryAcquire method is implemented by the abstract class Sync:

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

What this code means is, if it is not locked, use CAS to lock; if the current thread is already locked, increase the number of locks.

If the tryArquire method returns false, the acquiremethod will continue to be called acquireQueued(addWaiter(Node.EXCLUSIVE), arg).

Among them, addWaiter will create a new node Node, representing the current thread, and then add it to the internal waiting queue.

This part can refer to the blog post: https://blog.csdn.net/yanyan19880509/article/details/52345422

After waiting for the queue, call acquireQueued to try to acquire the lock, the code is:

/**
 * 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(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

The main body of the code here is an infinite loop. In each loop, first check whether the current node is the first waiting node. If it is and can acquire the lock, remove the current node from the waiting queue and return to it. Otherwise , the CPU isparkAndCheckInterrupt abandoned through the final method call , enters the waiting state, checks whether an interrupt has occurred after being woken up, and records the interrupt flag. LockSupport.parkAnd return the interrupt flag.

If an interrupt occurs, the acquire method will call the selfInterruptmethod to set the interrupt flag bit, and its implementation code is:

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

From the above, it can be concluded that the basic process of the lock method is : if the lock can be obtained, it will be obtained immediately, if not, it will be added to the waiting queue. After being awakened, check whether it is the first waiting thread, if it is and can obtain the lock, return, otherwise continue to wait. If an interrupt occurs during the process, lock will record the interrupt flag, but will not return early or throw an exception

3.2 unlock

The unlock method code of ReentrantLock is:

/**
 * Attempts to release this lock.
 *
 * <p>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);
}

The method called is the releasemethod defined in AQS, and its implementation is:

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

The tryRelease method will modify the thread state and release the lock, and the unparkSuccessor method will call LockSupport.unpark to wake up the first waiting thread. This is implemented as:

/**
 * Wakes up node's successor, if one exists.
 *
 * @param node the node
 */
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);
}

3.3 Fair and unfair comparison

The main differences between FairSync and NonfairSync are: When acquiring a lock, in the tryAcquire method, if the current thread state is not locked, that is c == 0, FairSysc will perform an additional check, which is implemented as follows:

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

The purpose of this check is to acquire the lock when there are no other threads waiting longer.

Why not guarantee fairness by default? The overall performance of ensuring fairness will be relatively low. The reason is not because the check is slow, but because the active thread cannot obtain the lock, thus entering the waiting state, causing frequent context switching and reducing the overall efficiency.

Typically, the order in which the threads run has little effect. When the program runs for a long time, in the statistical churn, the unfair thread processing method is basically fair, which can make the active thread continue to run.

It should be noted that even if the parameter fair of the constructor is true, the tryLock method without parameters in ReentrantLock is not guaranteed to be fair, and it does not detect whether there are other threads waiting longer.

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

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

This is evident from the method call.

4. Comparison of ReentrantLock and synchronized

Compared with synchronized, ReentrantLock can achieve the same semantics as synchronized, and it supports acquiring locks in a non-blocking manner. But synchronized is simpler to use and less code to use.

Synchronized represents a declarative programming thinking, and what the programmer expresses is more of a synchronization statement. The Java system is responsible for the implementation, and the programmer does not know the implementation details; while the explicit lock represents an imperative programming thinking, and the user needs to implement all the details.

In addition to simplicity, the benefits of declarative programming are also reflected in performance. On newer versions of the JVM, the performance of ReentrantLock and synchronized are close, and the Java compiler and virtual machine will continue to optimize the implementation of synchronized, such as automatically analyzing the use of synchronized, and automatically ignoring the acquisition of locks/ A call to release the lock.

Finally, use synchronized if you can use synchronized, and consider using ReentrantLock when it does not meet the usage requirements.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324884613&siteId=291194637