头图

Exclusive lock ReentrantLock principle

introduce

ReentrantLock is reentrant exclusive lock, while only one thread may acquire the lock, the other thread to acquire the lock is blocked then put into the lock the AQS blocking queue.

It has the synchronized same basic behavior and semantics, but ReentrantLock more flexible, more powerful, increasing the polling timeout interrupt and other advanced features, and also supports fair and unfair lock lock .

类图

As can be seen from the class diagram, ReentrantLock is still implemented using AQS, and it can be selected according to parameters whether it is a fair lock or an unfair lock. defaults to an unfair lock .

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

The Sync class directly inherits the AQS class, and its subclasses NonfairSync and FairSync implement the unfair and fair strategies for acquiring locks, respectively.

static final class NonfairSync extends Sync {}
static final class FairSync extends Sync {}

In ReentrantLock, the state value of AQS indicates the number of times the thread can acquire the lock. By default, the state value of 0 means that no thread currently holds it. When a thread acquires the lock for the first time, it will try to use CAS to set the state to 1. If CAS succeeds, the current thread acquires the lock, and then records that the lock holder is the current thread. After the thread acquires the lock for the second time, it sets state to 2, which is the number of reentrants. When the thread releases the lock, it will try to use CAS to decrement state by 1, and if the decrement is 0, the lock will be released.

acquire lock

void lock()

public void lock() {
    sync.lock();
}

When a thread calls this method, if the current lock is not occupied by other threads and the current thread has not acquired the lock before, the current thread acquires it, then sets the owner of the lock to itself, then sets the state of AQS to 1, and returns.

If the current thread has already acquired the lock, it simply increments the state of AQS by 1 and returns.

If the lock is already held by other threads, the current thread will enter the blocking queue of AQS to block and suspend.

The lock() method in ReentrantLock is delegated to the sync class, and sync chooses to use NonfairSync or FairSync according to the constructor of ReentrantLock.

Let's first look at the implementation of unfair locks.

final void lock() {
    // CAS设置状态值
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        // 调用AQS的acquire方法
        acquire(1);
}

The compareAndSetState method is called first in lock(), because the default state value is 0, so the first thread will set it to 1 through CAS when calling this method for the first time, and then successfully acquire the lock, and then use the setExclusiveOwnerThread method to set the lock The holder is set to the current thread.

When another thread acquires the lock through lock(), it will fail CAS and enter the else to call the acquire(1) method and pass the parameter 1. Let's look at the acquire method again.

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

As mentioned in the previous article, AQS does not provide the implementation of the tryAcquire method. Let's take a look at the tryAcquire method overridden by ReentrantLock. Here we still look at the implementation of the unfair lock.

static final class NonfairSync extends Sync {
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
                //调用该方法
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }
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;
}

This method first checks whether the current state value is 0. If it is 0, it means that the lock is currently in an idle state, and then tries to acquire the lock by CAS. After success, the state is set to 1, and then the lock holder is the current thread.

If the state is not 0, the lock is already held. If the holder happens to be the current thread, the state is incremented by 1, and then returns true. Note that if nextc < 0, the lock may overflow.

If the current thread is not the holder, return false and then join the AQS blocking queue.

Let's take a look at the implementation of fair locks.

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;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

The difference between the tryAcquire methods of fair locks and unfair locks is that there is an additional hasQueuedPredecessors() method, which is the core code for implementing fair locks.

public final boolean hasQueuedPredecessors() {
    //读取头节点
    Node t = tail;
    //读取尾节点
    Node h = head;
    //s是首节点h的后继节点
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

In this method, because the queue is FIFO, it is necessary to determine whether there are nodes with related threads in the queue already queued. If there is, return true to indicate that the thread needs to be queued, and if not, return false to indicate that the thread does not need to be queued.

First we look at the first condition h != t ,

  • The head node and tail node are both null, indicating that the queue is still empty, and the initialization has not even been completed, so it will naturally return to fasle without queuing.
  • The head node and the tail node are not null but equal, indicating that the head node and the tail node both point to an element, indicating that there is only one node in the queue, and there is no need to queue at this time, because the first node in the queue does not participate in the queue, it Holds the synchronization state, then the second incoming node does not need to queue, because its predecessor node is the head node, so the second incoming node is the first node that can normally obtain the synchronization state, and the third node Only need to queue and wait for the second node to release the synchronization state.

Next, let's look at the second condition, (s = h.next) == null , if h != t && s == null it means that there is an element that will be queued as the first node of AQS, then return true.

Next, look at the third condition, s.thread != Thread.currentThread() , to determine whether the successor node is the current thread.

🙋🏻‍♀️ For example, case 1:

h != t returns true , (s = h.next) == null returns false , s.thread != Thread.currentThread() returns false

First, h != t returns true , indicating that there are at least two different nodes in the queue;

(s = h.next) == null returns false , indicating that there is a successor node after the head node;

s.thread != Thread.currentThread() returns false, indicating that the current thread is the same as the successor node;

indicates that it is the current node's turn to try to obtain the synchronization status, no need to queue, return false .

🙋🏻‍♀️ For example, case 2:

h != t returns true , (s = h.next) == null returns true

First, h != t returns true , indicating that there are at least two different nodes in the queue;

(s = h.next) == null returns true , indicating that the head node is the sentinel node and there is no successor node;

returns true, indicating that needs to be queued.

🙋🏻‍♀️ For example, case 3:

h != t returns true , (s = h.next) == null returns false , s.thread != Thread.currentThread() returns true

First, h != t returns true , indicating that there are at least two different nodes in the queue;

(s = h.next) == null returns false , indicating that there is a successor node after the head node;

s.thread != Thread.currentThread() returns true , indicating that the thread of the successor node is not the current thread, indicating that someone is already queuing up, and it is still necessary to queue up honestly.

returns true, indicating that needs to be queued.

void lockInterruptibly()

public void lockInterruptibly() throws InterruptedException {
    sync.acquireInterruptibly(1);
}
public final void acquireInterruptibly(int arg)
            throws InterruptedException {
        // 如果当前线程被中断,则抛出异常
        if (Thread.interrupted())
            throw new InterruptedException();
        //尝试获取资源
        if (!tryAcquire(arg))
            //调用AQS可被中断的方法
            doAcquireInterruptibly(arg);
}

This method is similar to the lock() method, except that it will respond to interruption, that is, 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.

boolean tryLock()

public boolean tryLock() {
    return sync.nonfairTryAcquire(1);
}
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;
}

This method tries to acquire the lock, if the current lock is not held by another thread, the current thread acquires the lock and returns true, otherwise it returns false.

📢 Note: This method will not cause the current thread to block.

boolean tryLock(long timeout,TimeUnit unit)

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

The difference from tryLock is that a timeout period is set, and if the lock has not been acquired within the timeout period, it will return false.

release lock

void unlock()

public void unlock() {
    sync.release(1);
}
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;
    //如果不是锁持有者调用unlock则抛出异常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    // 如果当前可重入次数为0 则情况锁持有线程
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

The method first tries to release the lock. If the tryRelease() method returns true, the lock is released, otherwise it is just decremented by 1. If it is not the lock holder to call unlock, an IllegalMonitorStateException is thrown.

Code Practice

/**
 * @author 神秘杰克
 * 公众号: Java菜鸟程序员
 * @date 2022/1/21
 * @Description 使用ReentrantLock实现简单的线程安全List
 */
public class ReentrantLockList {

    private List<String> array = new ArrayList<>();

    private volatile ReentrantLock lock = new ReentrantLock();

    public void add(String e) {
        lock.lock();
        try {
            array.add(e);
        } finally {
            lock.unlock();
        }
    }

    public void remove(String e) {
        lock.lock();
        try {
            array.remove(e);
        } finally {
            lock.unlock();
        }
    }

    public String get(int index) {
        lock.lock();
        try {
            return array.get(index);
        } finally {
            lock.unlock();
        }
    }

}

Before operating the array, this class ensures that only one thread can operate the array at the same time by locking, but only one thread can access the elements of the array.

Summarize

When three threads try to acquire the exclusive lock ReentrantLock at the same time, if thread 1 acquires it, threads 2 and 3 will be converted to Node nodes and then put into the AQS blocking queue corresponding to ReentrantLock and then suspended.

阻塞队列线程2,线程3

Suppose that thread 1 calls the condition variable 1 created by the lock to enter await after the lock, and thread 1 will release the lock 161f27431248d3. Then thread 1 will be converted to a Node node and inserted into the condition queue of condition variable 1.

Since thread 1 releases the lock, thread 2 and thread 3 in the blocking queue will have the opportunity to acquire the lock. If a fair lock is used, thread 2 will acquire the lock and then remove it from the AQS blocking queue The Node node corresponding to thread 2.

阻塞队列线程3


神秘杰克
765 声望382 粉丝

Be a good developer.