Java concurrency advanced: ReentrantLock reentrant exclusive lock detailed

Basic usage introduction

ReentrantLock is located in the java.util.concurrent (JUC) package and is the implementation class of the Lock interface. The basic usage is similar to synchronized, both have the characteristics of reentrant and mutual exclusion , but have a more powerful and flexible locking mechanism. This article mainly analyzes ReentrantLock from the perspective of source code, some basic concepts and Lock interface can be poke this: Java concurrent reading notes: Lock and ReentrantLock

The recommended usage of ReentrantLock is as follows:

class X {
    //定义锁对象
    private final ReentrantLock lock = new ReentrantLock();
    // ...
    //定义需要保证线程安全的方法
    public void m() {
        //加锁
        lock.lock();  
        try{
        // 保证线程安全的代码
        }
        // 使用finally块保证释放锁
        finally {
            lock.unlock()
        }
    }
}

Inheritance system

Java concurrency advanced: ReentrantLock reentrant exclusive lock detailed

 

  • Implementing the Lock interface provides key lock methods, such as lock, unlock, tryLock, etc., as well as a method for newCondition to associate condition objects with lock.
  • A Sync is maintained internally, which inherits AQS, implements the exclusive method of acquiring and releasing synchronization resources provided by AQS, and provides a specific implementation of reentrancy.
  • Sync has two implementation classes, which are two implementations of fair lock and non-fair lock, FairSync and NonfairSync.

Exclusive lock means: only one thread can acquire the lock at the same time, and other threads that acquire the lock will be blocked and put into the AQS blocking queue of the lock. This part can be viewed: Java Concurrent Package Source Code Learning Series: The difference between AQS shared and exclusive acquisition and release of resources

Construction method

Sync directly inherits from AQS, and NonfairSync and FairSync inherit from Sync, realizing fair and unfair strategies for acquiring locks.

The operations in ReentrantLock are delegated to the Sync object for actual operations.

/** Synchronizer providing all implementation mechanics */
    private final Sync sync;

The default is to use non-fair lock: NonfairSync, you can pass in parameters to specify whether to use fair lock.

// 默认使用的是 非公平的策略
    public ReentrantLock() {
        sync = new NonfairSync();
    }
	// 通过fair参数指定 策略
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

state state

In ReentrantLock, the state value of AQS represents the reentrant number of times that the thread can acquire the lock. By default:

  • When the state value is 0, it means that the current lock is not held by any thread.
  • When the first thread acquires the lock for the first time, it will try to use CAS to set the value of state to 1. If the 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 without releasing the lock, the state value is set to 2, which is the number of reentrants.
  • When the thread releases the lock, it will try to use CAS to decrease the state value by 1. If the state value is 0 after the decrease by 1, the current thread releases the lock .

Acquire lock

void lock() method

The lock() method of ReentrantLock is delegated to the sync class, and the specific logic is determined according to the specific implementation of creating sync:

NonfairSync

/**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {
            // CAS 设置获取state值
            if (compareAndSetState(0, 1))
                // 将当前线程设置为锁的持有者
                setExclusiveOwnerThread(Thread.currentThread());
            else
                // 设置失败, 调用AQS的acquire方法
                acquire(1);
        }

The initial state of the state value is 0, that is, the CAS operation of the first thread will successfully set 0 to 1, indicating that the current thread has acquired the lock, and then set the current thread as the lock holder through the setExclusiveOwnerThread method.

If at this time, other threads also try to acquire the lock, the CAS fails and goes to the acquire logic.

// AQS#acquire
	public final void acquire(int arg) {
        // 调用ReentrantLock重写的tryAcquire方法
        if (!tryAcquire(arg) &&
            // tryAcquire方法返回false,则把当前线程放入AQS阻塞队列中
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

Hey, we should have a feeling at this time. We said when we analyzed the core method of AQS. AQS is designed based on the template pattern. The following tryAcquire method is reserved for subclasses. This is the case in NonfairSync. Realized:

//NonfairSync#tryAcquire
        protected final boolean tryAcquire(int acquires) {
    		// 调用
            return nonfairTryAcquire(acquires);
        }

		final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            // 获取当前状态值
            int c = getState();
            // 如果当前状态值为0,如果为0表示当前锁空闲
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // 看看当前的线程是不是锁的持有者
            else if (current == getExclusiveOwnerThread()) {
                // 如果是的话 将状态设置为  c + acquires
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

It's still easy to understand, let's take a look at the lock state value first?

  • If it is 0, CAS tries to acquire the lock, changes the state from 0 to 1, and sets the lock holder to the current thread, which is the same as the previous logic.
  • If it is not 0, it means it has been held by a thread. See who is holding the lock? If it's yourself, it's easy to handle, re-enter, change state to nextc [original state + incoming acquires], and return true. Note here: nextc<0 indicates that the number of reentrant overflows.
  • If the lock is already occupied by others, return false and wait for the subsequent acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) method to be placed in the AQS blocking queue.

The unfairness here is that when acquiring the lock, it does not check whether there is a thread that requested the lock earlier than itself in the current AQS queue, but adopts a grab strategy .

FairSync

The tryAcquire implementation of fair lock is as follows:

//FairSync#tryAcquire
		protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                // 状态值为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;
        }

Compare the two strategies. Needless to say, the hasQueuedPredecessors method must be the core of achieving fairness. Let's take a look:

// 如果当前线程有前驱节点就返回true。
	public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

The method: If the current thread has a predecessor node, it returns true. Then we think, what are the situations where it is not a predecessor node?

  1. The queue is empty
  2. The queue is not empty, but the current thread node is the first node of AQS.

After knowing this, we will understand the meaning of the last string of expressions: the first element in the queue is not the current thread, return true, indicating that there are still people in the queue before you, don't grab it, first come first Got .

The difference between fair and unfair strategies

Let's summarize a little:

The constructor of the Reentrant class accepts an optional fairness parameter fair . At this time, there are two options:

  • Fair (fair == true): It is guaranteed that the thread with the longest waiting time will obtain the lock first, which is actually a first-come first-served lock , that is, FIFO.
  • Unfair (fair == false): This lock does not guarantee any specific access sequence.

Fair locks often reflect lower overall throughput than unfair locks, that is, slower, because each time you need to see if there are queues in the queue. The fairness of locks does not guarantee the fairness of thread scheduling, but fair locks can reduce the probability of "starvation".

It should be noted that the irregular tryLock() method does not support fairness settings. If the lock is available, it will successfully acquire the lock even if other threads wait longer than it.

void lockInterruptibly()

This method is similar to the lock method, but the difference is that it can respond to interrupts: 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 and then return.

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

boolean tryLock() method

Try to acquire the lock. If the lock is not currently held by other threads, the current thread acquires the lock and returns true, otherwise it returns false.

The general logic is similar to the unfair lock lock method, but this method will directly return the result of acquiring the lock, regardless of true or false, it will not block.

// ReentrantLock# tryLock
	public boolean tryLock() {
        return sync.nonfairTryAcquire(1);
    }
	abstract static class Sync extends AbstractQueuedSynchronizer {
		// Sync#nonfairTryAcquire
        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;
        }
    }

  • The tryLock()  implementation method, in the implementation, hope to quickly obtain whether the lock can be acquired, so even if it is set to fair = true (using fair lock), still call the Sync#nonfairTryAcquire(int acquires) method.
  • If you really want tryLock() to be based on the fairness of the lock, you can call the #tryLock(0, TimeUnit) method to achieve it.

boolean tryLock(long timeout, TimeUnit unit)

// ReentrantLock# tryLock
    public boolean tryLock(long timeout, TimeUnit unit)
            throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }
	// AQS#tryAcquireNanos
    public final boolean tryAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        return tryAcquire(arg) ||
            doAcquireNanos(arg, nanosTimeout);
    }

Try to acquire the lock. If the acquisition fails, the current thread will be suspended for the specified time. After the time is up, the current thread will be activated. If the lock is still not acquired, false will be returned.

In addition, this method will respond to interrupts. If other threads call the interrupt() method of the current thread, respond to the interrupt and throw an exception.

Release lock

void unlock() method

// ReentrantLock#unlock
	public void unlock() {
        sync.release(1);
    }
	//AQS# release
    public final boolean release(int arg) {
        // 子类实现tryRelease
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

	abstract static class Sync extends AbstractQueuedSynchronizer {
		// Sync#tryRelease
        protected final boolean tryRelease(int releases) {
            // 计算解锁后的次数,默认减1
            int c = getState() - releases;
            // 如果想要解锁的人不是当前的锁持有者,直接抛异常
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            // 可重入次数为0,清空锁持有线程
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            // 可重入次数还没到0,只需要改变一下下state就可
            setState(c);
            return free;
        }
    }

Try to release the lock. If the current thread holds the lock, calling this method will reduce the state of AQS by 1.

If the state is 0 after subtracting 1, the current thread will release the lock.

If the current thread is not the lock holder and attempts to call this method, an IllegalMonitorStateException is thrown.

Condition implements producer consumers

Condition is used to replace wait() and notify() in the traditional Object to realize the cooperation between threads. The await() and signal() of Condition are used to handle the cooperation between threads to be safer and more efficient .

The use of Condition must be used between lock() and unlock() , and can only be obtained through lock.newCondition(). The implementation principle will be specifically learned later.

public class BlockingQueue {

    final Object[] items; // 缓冲数组
    final ReentrantLock lock = new ReentrantLock(); // 非公平独占锁
    final Condition notFull = lock.newCondition(); // 未满条件
    final Condition notEmpty = lock.newCondition(); // 未空条件
    private int putIdx; // 添加操作的指针
    private int takeIdx; // 获取操作的指针
    private int count; // 队列中元素个数

    public BlockingQueue(int capacity) {
        if(capacity < 0) throw new IllegalArgumentException();
        items = new Object[capacity];
    }

    // 插入
    public void put(Object item) throws InterruptedException {
        try {
            lock.lock(); // 上锁
            while (items.length == count) { // 满了
                notFull.await(); // 其他插入线程阻塞起来
            }
            enqueue(item); // 没满就可以入队
        } finally {
            lock.unlock(); // 不要忘记解锁
        }
    }
    private void enqueue(Object item) {
        items[putIdx] = item;
        if (++putIdx == items.length) putIdx = 0; 
        count++;
        notEmpty.signal(); // 叫醒获取的线程
    }

    // 获取
    public Object take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0) {
                notEmpty.await();// 阻塞其他获取线程
            }
            return dequeue();
        } finally {
            lock.unlock();
        }
    }
    
    private Object dequeue() {
        Object x = items[takeIdx];
        items[takeIdx] = null;
        if (++takeIdx == items.length) takeIdx = 0;
        count--;
        notFull.signal(); // 叫醒其他的插入线程
        return x;
    }
}

In fact, the above is part of the implementation of the reduced version of ArrayBlockingQueue. Interested friends can take a look at the implementation of the source code. The source code has also done more detailed processing for concurrency.

to sum up

Exclusive lock at API level : ReentrantLock is a reentrant exclusive lock implemented at the bottom using AQS . It is different from the lock semantics implemented at the synchronized native syntax level. ReetrantLock explicitly implements mutual exclusion locks through two methods, lock() and unlock().

State and reentrant : AQS state of 0 indicates that the current lock is idle, and greater than 0 indicates that the lock is already occupied, and only one thread can acquire the lock at a time. Reentrancy is determined by judging whether the thread holding the lock is the current thread, if it is, state+1, when the lock is released, state-1, and 0 means complete release.

Fair and unfair strategies : ReentrantLock has two strategies, fair and unfair. The difference lies in whether it checks whether there is a predecessor node in the current thread when acquiring the lock . The default is an unfair lock strategy.

Abundant lock extensions : provide lockInterruptibly which responds to interruption of lock acquisition, and provides quick response tryLock method, and timeout acquisition methods.

condition : TODO A ReentrantLock object can bind multiple Condition objects at the same time through newCondition(). The waiting and wake-up operations of threads are more detailed and flexible . We will come back to this when we talk about Condition later.

Original link: https://www.cnblogs.com/summerday152/p/14260300.html

If you think this article is helpful to you, you can follow my official account and reply to the keyword [Interview] to get a compilation of Java core knowledge points and an interview gift package! There are more technical dry goods articles and related materials to share, let everyone learn and progress together!

Guess you like

Origin blog.csdn.net/weixin_48182198/article/details/112562798
Recommended