J.U.C ReentrantLock use and source code analysis

Posted by iceangel89 on Tue, 07 Dec 2021 19:04:21 +0100

Essence: locks are used to solve thread safety problems
Other implementations of Lock in Java, such as WiteLock write Lock and ReadLock read Lock, are mainly expanded with ReentrantLock reentry Lock in this paper

ReentrantLock reentrant lock

Reentry lock and mutex lock are used to solve the deadlock problem

1. Use of reentrantlock

	static Lock  lock = new ReentrantLock();
    static int sum = 0;
    public static void incr(){
        lock.lock();	//Preemptive lock. If there is no preemption, it will block
        try {
            Thread.sleep(1);
            sum ++ ;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();	//Release lock
        }
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            new Thread(()->{
                LockExample.incr();
            }).start();
        }
        Thread.sleep(3000);
        System.out.println(sum); //Output 1000
    }

J. The difference between U.C Lock and Synchronized is that the locking and releasing of Lock require manual operation

2. Principle and implementation of reentrantlock

Satisfying the mutual exclusion of threads means that only one thread is allowed to enter the locked code at the same time

Basic conditions for a lock:

  • Identification with lock and without lock
  • Thread processing without preemptive lock
    • Wait (directly block first and release CPU resources)
      • wait/notify exists. The specified thread cannot be awakened
      • LockSupport.park/unpark (block the specified thread and wake up the specified thread)
      • Condition
    • Queuing (N threads running are blocked and the thread is waiting)
      • N queued threads are stored through a data structure
  • Lock release process
    • Locksupport.unpark (thread) - > wake up the specified thread in the queue
  • Fairness of lock (whether queue jumping is allowed)

If the above conditions are not very clear, continue to look at the lock flow analysis chart below

3. Source code analysis

1. Lock.lock()

public void lock() {
    //Here you can see that there are two methods: FairSync and NonfairSync
    sync.lock();
}
//Unfair lock code
final void lock() {
    //Unfair lock and fair lock are different here
    //For unfair locks, first try to update the state state. If successful, get the lock directly
    //Fair lock is to get the preemptive lock directly
    if (compareAndSetState(0, 1))
        //If the state is successfully modified here, set the lock owner to the current thread directly
        setExclusiveOwnerThread(Thread.currentThread());
    else
        //Otherwise, grab the lock. Next, look here###
        acquire(1);
}

//AbstractQueuedSynchronizer
public final void acquire(int arg) {
    //If the attempt to obtain the lock fails, it will be added to the AQS queue. First look at tryAcquire
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
//Code for fair lock preemption nonfairTryAcquire for non-public lock preemption
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        //The difference from unfair locks is described under hasqueued predecessors 
        if (!hasQueuedPredecessors() &&
            //cas operation state
            compareAndSetState(0, acquires)) {
            //After getting the lock, set the thread variable to preempt the lock, and the process is over
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //Reentry lock
    else if (current == getExclusiveOwnerThread()) {
        //Record the number of re-entry locks
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        //Status is 0 or > 0
        setState(nextc);
        return true;
    }
    return false;
}
  • Next, analyze the situation of fair lock hasQueuedPredecessors. Returning true indicates that the current thread is going to queue. There is no logic for non fair locks
public final boolean hasQueuedPredecessors() {
    //Tail node
    Node t = tail;
    //Head node
    Node h = head;
    //h later nodes
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}
//h!=t returns true, indicating that at least two different nodes exist in the queue.
//(s = h.next) == null returns false, indicating that there is a successor node.
//s.thread != Thread.currentThread() returns true, which means that the thread of the subsequent node is not the current thread, so the current thread naturally has to queue honestly.
  • As can be seen above, if the above conditions are met and the CAS operation state is successful, the thread will get the lock. Next, continue to analyze how to deal with the thread that does not get the lock?
//Back to AbstractQueuedSynchronizer#acquire 
//Acquirequeueueueued (addwaiter (node. Exclusive, Arg)) / / node.exclusive exclusive exclusive state
  
//Look at addWaiter first
private Node addWaiter(Node mode) {
	//Encapsulate the current thread as a Node
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    //If the current AQS queue is not empty, the cas operation is used to add the node to the tail node 
    if (pred != null) {
    	//Tail interpolation
        node.prev = pred;
        //Set the new node as the tail node
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    //cas failed to add to AQS queue through enq
    enq(node);
    return node;
}
//Add to AQS queue through continuous spin and CAS operation
private Node enq(final Node node) {
    for (;;) {
        //When the thread is unsafe from the tail node to the head node, the next node of the head node keeps changing
        Node t = tail;
        //The current tail node is empty. Create a head and tail node
        if (t == null) { // Must initialize
            //Because the lock is not obtained at this time, it is not safe under multithreading, so CAS operation must be used
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            //The current thread node is added after the tail node
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}
  • Next, let's see how to handle the threads added to the AQS queue
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            //spin
            for (;;) {
                //Take out the pre node of the current node (the previous node)
                final Node p = node.predecessor();
                //If the front node is head, it means that the current node is in the first place in the waiting queue and directly obtains the lock
                if (p == head && tryAcquire(arg)) {
                    //Lock acquired successfully
                    //This indicates that the head node has completed execution and is the current node that releases the lock and wakes up
                    //Set as the head node and return, and then execute the locked code
                    setHead(node);
                    //Remove the front node from the queue so that there is no point to it, which helps GC recycle quickly
                    p.next = null; // help GC
                    failed = false;
                    return interrupted; //Returns the interrupt status. false indicates that there is no interrupt
                }
                //shouldParkAfterFailedAcquire has three situations
                //1. If the status is already signal (waiting to be woken up), return true
                //2 WS > 0 closed state (thread interrupted) remove closed thread and return false
                //3. When the update status is SIGNAL, false is returned
                //If false is returned, the next time you continue to spin to implement the operation, you will eventually return true and execute the blocking code
                if (shouldParkAfterFailedAcquire(p, node) &&
                    // LockSupport.park(this);  Block the execution from here after the current thread wakes up
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

The lock code is analyzed here. Next, continue to analyze the lock release code.

2.Lock.unlock() release lock

public void unlock() {
	//Or take the secure lock as an example
    sync.release(1);
}
//arg represents the number of times the lock is re entered. The lock needs to be released to 0 before it can be obtained by other threads
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            //1. Modify state to initial state 0
            //2.Node s = node.next;  LockSupport.unpark(s.thread);  Wake up the next thread
            unparkSuccessor(h);
        return true;
    }
    return false;
}

protected final boolean tryRelease(int releases) {
    //The reentry lock needs to be released to 0 before it can be obtained by other threads
    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;
}

//Look again at releasing the lock and waking up the next waiting thread
private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        //Modify state to initial state 0
        compareAndSetWaitStatus(node, ws, 0);
    //Gets the next thread waiting to wake up    
    Node s = node.next;
    //The next node is invalid or the node status is off
    if (s == null || s.waitStatus > 0) {
        s = null;
        //Start scanning from the tail node to find the one closest to the head
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        //Wake up the next valid thread. The next thread spins in acquirequeueueueued
        LockSupport.unpark(s.thread);
}

Only I think spin works well? That is the whole content of this chapter.
Be sure to open the source code yourself and read it again. Don't talk on paper.

Previous: Ordering of thread safety and memory barrier
Next: Use of wait/notify and J.U.C Condition in synchronized thread communication and source code analysis

There is a road to the mountain of books. Diligence is the path. There is no end to learning. It is hard to make a boat

Topics: Java source code lock