Take a look at ReentrantLock

We analyzed the basic principle of AQS, and then tried to implement a reentrant lock based on AQS. Now let's take a look at the official reentrant lock, which is a reentrant exclusive lock, that is, only one thread can acquire the lock at the same time, and this thread can continue to try to acquire the lock;

 

1, Simple use

We first implement a thread safe List based on ReentrantLock, and then analyze the common methods;

package com.example.demo.study;

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

public class Study0204 {
    //Thread unsafe List
    private ArrayList<String> list = new ArrayList<String>();
    //Exclusive lock,Default unfair lock, passed in true It can be a fair lock
    private volatile ReentrantLock lock = new ReentrantLock();
    
    //Add elements to a collection
    public void add(String str) {
        lock.lock();
        try {
            list.add(str);
        } finally {
            lock.unlock();
        }
    }
    //Delete elements from collection
    public void remove(String str) {
        lock.lock();
        try {
            list.remove(str);
        } finally {
            lock.unlock();
        }
    }
    //Get an element in the collection by index
    public String get(int index) {
        lock.lock();
        try {
            return list.get(index);
        } finally {
            lock.unlock();
        }
    }
}

In fact, there will be a process of locking at every step. The usage is the same as the synchronized keyword, but we should pay attention to releasing the lock in finally, and we don't know if we have found a problem. Do we need to use the lock in get method? Why do multithreading read data also need to be locked without changing the data in the collection? This is also a defect of ReentrantLock, which is used in the case of write more read less;

So we will talk about ReentrantReadWriteLock later. This lock is applied to the situation of more reading and less writing. It can be used separately from the read lock. If it is read data, multiple threads can obtain the read lock, which will be mentioned later

 

2, Look at ReentrantLock lock structure

This Lock is also very simple, as shown in the following figure: to implement the Lock interface, there is an internal tool class Sync inherited from AQS, and then there are two classes, non FairSync and FairSync, which inherit Sync to implement their own methods. From the name, we can see that these two classes are actually the implementation strategies of unfair locks and fair locks;

 

First, let's look at the lock method of ReentrantLock, and find that it is to call the lock method of sync, so next, let's see how the sync object is built;

  

If you look at the constructor, you can know whether it is a fair policy or an unfair policy to build a sync object based on whether the parameters passed in are true and false. By the way, the state in AQS represents the number of reentrant locks, and the default state is 0;

When a thread CAS successfully acquires the lock for the first time, the state will be increased by one. When the thread that has acquired the lock continues to acquire the lock, the state will be increased by one, and the state of releasing the lock will be decreased by one until it becomes 0, which means that the lock is not occupied by a thread. At this time, a thread in the blocking queue in AQS can acquire the lock;

 

We can see what methods are implemented in Sync here. As shown in the figure below, only the lock() method is not implemented, which is left to NonfairSync and FairSync to implement according to their own scenarios;

 

 

The implementation of the fair lock strategy is as follows: the lock method and the tryAcquire() method are implemented. The tryAcquire method was previously said to be implemented by subclasses in AQS according to the actual scenarios

 

The implementation of unfair strategy is the same as that of fair strategy;

 

3, Unfair policy acquisition lock

The basic structure of ReentrantLock has been mentioned before, and then we will analyze how to obtain the lock. Starting from the lcok method in NonfairSync, let's sort out: first, thread A uses CAS to try to set the state in AQS from 0 to 1. If it succeeds, it will directly set the current thread as the lock owner; if it fails, it will get the state value, if it is 0 , or set CAS to 1, and set the current thread as the lock possessor, if not 0, let's see if the current thread holds the lock; if it's the current thread, add one to state; if it's not the current thread, then other threads occupy the lock, which really means that the current thread fails to acquire the lock, and we encapsulate the current thread as A node of type Node.EXCLUSIVE, Throw it into the blocking queue;

Here we need to think about why it's unfair? If in the following non fairtryacquire method, the value of c obtained by thread A is 1, and it is not thread A that owns the lock, then the thread will be encapsulated as A node and thrown into the blocking queue. At this time, thread B will obtain the value of c, which is exactly 0 (because the thread that owns the lock may release the lock at this time), so thread B can successfully change the state from 0 to 1, and it can obtain Get the lock.

Although thread A tries to acquire the lock first, and thread B tries to acquire it later, thread B can successfully acquire the lock first, just like you are queuing for dinner, and A later person actually hits the rice first to eat, which is really A dog in the sun!

final void lock() {
    //Try to pass CAS take state Change from 0 to 1. If successful, the current thread will occupy the lock
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    //CAS Failure indicates that other threads have acquired the lock, so the current thread will be dropped into the blocking queue. Focus on acquire Method
    else
        acquire(1);
}

//Mainly look at tryAcquire Method, in NonfairSync Implementation in
//In the current method, if The second condition in the judgment has been seen, mainly encapsulating the current thread into a Node.EXCLUSIVE Node of type
//And throw it at the end of the blocking queue
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();
    //Obtain state Value
    int c = getState();
    //state If the value of is 0, then CAS Set to 1, and set the current thread to occupy the lock
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //CAS Setting failed. There must be other threads occupying the lock. Since it is a reentrant lock, it is necessary to determine whether the current thread occupies the lock
    else if (current == getExclusiveOwnerThread()) {
        //Yes, then state add one-tenth
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    //If you can get here, explain state The value of is not 0, and the thread holding the lock is not the current thread. It must be another thread. Then return false
    return false;
}

 

4, Fair strategy acquisition lock

In fact, the implementation of fair strategy is basically the same as that of unfair strategy. We only look at the implementation of tryAcquire method:

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        //The point is, it's different here. It won't go straight through CAS Get lock, call hasQueuedPredecessors Method to check whether there is a precursor node in the blocking queue,
        //This method is the core. If there is a node in front of the current thread node in the blocking queue, it will not go in here, but will go directly to the following else if
        //If there is no node in front, the current blocking queue is null Or only the current thread node, you can get CAS modify state,Lock acquired successfully
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

//Judge whether there is a node in front of the current thread node in the blocking queue?
public final boolean hasQueuedPredecessors() {

    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
    //Note, head node head It's the sentinel node
    //If the head node is the same as the tail node h==t,The previous blog drawings all point to the sentinel node. At this time, the blocking queue is empty. If there is no precursor node, it will return false
    //If h != t meanwhile(s = h.next) == null,Indicates that a node is being inserted after the sentinel node in the blocking queue. At this time, it indicates that there is a precursor node. Return to true
    //If h != t meanwhile(s = h.next) != null,And s.thread != Thread.currentThread(),It indicates that there is a node behind the sentinel node, and this node is not the current thread node
    //That is to say, if there are precursor nodes, return true
    return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
}

 

 

5, Other methods

We have looked at the implementation methods of fair strategy and non fair strategy above, but they are nothing. It is relatively easy. Continue to look at some other methods, which are universal;

1. Lockinterruptible() method

//It was added before Interruptibly Indicates that if the current thread calls this method, other threads call the current thread's interrupt()Method, then
//The current thread will throw InterruptedException Exception, we can see how the internal mechanism is realized
public void lockInterruptibly() throws InterruptedException {
    sync.acquireInterruptibly(1);
}

public final void acquireInterruptibly(int arg) throws InterruptedException {
    //If the current thread is interrupted, an exception is thrown
    if (Thread.interrupted())
        throw new InterruptedException();
    //Try to obtain resources, which are divided into fair strategy and unfair strategy. As mentioned before;
    //If the resource acquisition fails, call AQS Interruptible method
    if (!tryAcquire(arg))
        doAcquireInterruptibly(arg);
}

 

 

2.tyLock() method

//Obviously tryLock Method is unfair strategy
public boolean tryLock() {
    return sync.nonfairTryAcquire(1);
}
//When this method says unfair strategy tryAcquire Method calls the same 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;
}

  

3.tryLock(long timeout, TimeUnit unit) method

The difference between this method and the above method is that the timeout can be set. In fact, it is similar to the previous method, that is, there is an extra time to judge, which will also respond to thread interruption;

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

public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    return tryAcquire(arg) ||
        doAcquireNanos(arg, nanosTimeout);
}

 

 

4.unlock() to release the lock

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

public final boolean release(int arg) {
    //We can know from the following methods tryRelease Only when state When it is 0, it will return true,That is, the current lock is not held by the thread
    //At this time, if the head node is not null,And the waitStatus Wake up the head node if it is not in the initial state
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

protected final boolean tryRelease(int releases) {
    //take AQS Medium state Minus one
    int c = getState() - releases;
    //If the current thread is not the owner of the lock, it calls unlock Method, it will throw IllegalMonitorStateException abnormal
    //Here we can judge if the original state 0, so the top c It should be negative one, and you will come here and make a mistake state For one, it's down there if(c==0)inside
    //If state Greater than 1, only the lowest setState(c)After subtracting one state Update to AQS in
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    //If the reentrant number is 0, the thread currently holding the lock is set to null
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    //No matter if the number is 0 or not, we will only update the latest state Update acquisition
    setState(c);
    return free;
}

 

 

Six. Conclusion

We have seen the basic function of ReentrantLock. In fact, it is an exclusive lock, and it is reentrant. Here, reentry refers to a thread that has occupied the lock, and can continue to acquire the lock!

As shown in the figure below, there are three threads competing for ReentrantLock lock lock at the same time. At this time, only Thread1 successfully holds the lock, and then the other two threads are thrown into the AQS blocking queue (there is a sequence here, first Thread2, then Thread3);

 

 

If Thread1 calls the wait method of condition variable 1 at this time, then Thread1 will be dropped into condition queue 1 and the lock will be released. At this time, a Thread in the blocking queue can acquire the lock. If it is a fair lock, then it is the first acquiring lock in the blocking queue. At this time, only Thread3 is in the blocking queue. (if it is an unfair lock, then It's luck. If Thread2 and Thread try to get lock first, they will get lock.)

Tags: Java less

Posted on Wed, 05 Feb 2020 03:13:19 -0500 by kiwis