ReentrantLock source code for multithreaded programming

I finished AQS and Unsafe (LockSupport is actually a simple package) before, and then I can look at various locks, concurrency tools, and thread pools. After all, they all rely on AQS or Unsafe implementations.

Lock & Condition

The Java SDK concurrent package reimplements a monitor through two interfaces, Lock and Condition , where Lock is used to solve mutual exclusion problems, and Condition is used to solve synchronization problems.

Then why re-implement it again, wouldn't it be good to use the keyword synchronized?

To a large extent, it is to effectively avoid deadlocks.

The reason is that the use of Synchronized is limited, and it is difficult to break the non-preemptive condition.

  • Does not support blocking timeout;
  • Does not support knowing whether the lock has been acquired;
  • Non-blocking fetches are not supported;
  • Interrupts are not supported;
  • The coordination of multiple conditions is not supported; for example, synchronized only supports waiting notifications on locks; and the Lock of concurrent packages can allow multiple conditions, so it supports waiting notifications in multiple dimensions.

The Condition interface can be said to have only one implementation in the JDK ConditionObject, which is an internal class of AQS. The application of Condition in the Lock interface is based on this implementation. For this class, please refer to AQS Condition Source Code Analysis (juejin.cn) , so I won’t go into details here.

Focus on Lock.

There are mainly two standard implementations of the Lock interface Reentrantlockand ReentrantReadWriteLock, this article and next will analyze the source code of these two classes.

In fact, under the lock package, JDK also provides an optimistic read (there are two states: pessimistic read lock, write lock) implementation: , StampedLockin the case of more read and less write, StampedLock performance is better than ReadWriteLock.

However, this class does not implement the Lock interface, and the two key reasons are:

  • StampedLockThere is no concept of ownership. Simply put, the lock acquired by the current thread can be released by another thread;
  • Pessimistic read locks and write locks do not support condition variables.

Association of ReentrantLock with AQS

AQS defines operations such as resource queuing and blocking wakeup; the most critical resource stateacquisition, release, and resource meaning are all implemented by subclasses. For AQS, it is just an int value that can be manipulated.

And ReentrantLock is this subclass. ReentrantLock allows two kinds of semantics: fair lock and unfair lock, but no matter which kind of semantics, it essentially implements AQS internally, gives state specific meaning, and implements resource acquisition and release methods in turn.

isHeldExclusively()//该线程是否正在独占资源。只有用到 condition (AQS.ConditionObject)才需要去实现它。
tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。

For more details, see AbstractQueuedSynchronizer (AQS): the cornerstone of concurrent tools (juejin.cn)

How to ensure visibility?

According to the volatile-related happens-before principle.

Lock and Condition are actually implemented through AQS. There is a state field in AQS that is volatile, and this field will be read and written when locking and unlocking.

According to relevant principles:

  • The principle of sequence; the business operation of thread 1 is earlier than the unlocking of thread 1 lock.
  • According to the volatile principle, writing to this field happens before reading to this field. That is to say, thread 1 unlocks earlier than thread 2 locks.
  • Passing principle; thread 1 business operation locks earlier than thread 2.

Then if you understand what AQS is, ReentrantLockthere is actually no content.

Because the key method is actually the method of calling AQS internally. However, there are still some things to look at. The most important thing is the implementation of reentrancy and the difference between fair locks and unfair locks.

Fair locks and unfair locks:

The lock corresponds to a waiting queue. If a thread does not acquire the lock, it will enter the waiting queue. When a thread releases the lock, it needs to wake up a waiting thread from the waiting queue.

If it is a fair lock, the wake-up strategy is to wake up whoever waits for a long time, which is very fair;

If it is an unfair lock, this fairness guarantee is not provided, and it is possible that the thread with a short waiting time will be woken up first.

Implementation of reentrant lock

ReentrantLockThere is an internal class that implements AQS, called Sync, which implements the above-mentioned method of AQS on exclusive mode (because it ReentrantLockdoes not support shared locks).

Based on the fact that state == 0 in AQS indicates that the resource is available, ReentrantLockthe definition of state > 0 means that the lock has been held by the thread, and its value represents the number of reentries. Then it can be concluded that the maximum number of reentrants for a thread is: the maximum value of int, 2147483647.

  • Each reentry acquisition, state + 1;
  • release state -1;
  • When the state is reduced to 0, it means that the resource is available, waiting for the thread to join the scramble or the first queued thread to get it.

Note: ReentrantLockIn , the parameters acquires and release of tryAcquire, tryRelease or similar methods are both 1, which means a reentrancy.

abstract static class Sync extends AbstractQueuedSynchronizer {
    
    
    /**
     * Performs non-fair tryLock.  tryAcquire is implemented in
     * subclasses, but both need nonfair try for trylock method.
     */
    @ReservedStackAccess
        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");
                // 因为是重入,肯定不会有争抢,直接 set 即可
                setState(nextc);
                return true;
            }
            return false;
        }

        @ReservedStackAccess
        protected final boolean tryRelease(int releases) {
    
    
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                // 只允许持有线程释放锁
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
    
    
                // 等于 0 的时候, 说明完全释放了锁
                free = true;
                setExclusiveOwnerThread(null);
            }
            // 同理不会有争抢, 直接set
            setState(c);
            return free;
        }
}

Fair Lock & Unfair Lock

It can be seen Syncthat there is only nonfairTryAcquireone method of acquisition. Based on this method and AQS defaults to unfair acquisition (try to acquire resources first when acquiring resources, and start queuing if they cannot be obtained), it can be concluded that there are Synconly unfair locks.

Because Syncis an abstract parent class, it uniformly implements the default unfair acquisition and release, so the implementation of fair lock is actually a simple call.

static final class NonfairSync extends Sync {
    
    
    protected final boolean tryAcquire(int acquires) {
    
    
        return nonfairTryAcquire(acquires);
    }
}

Therefore, an implementation of an unfair lock is also needed, inherited from Sync.

static final class FairSync extends Sync {
    
    
    @ReservedStackAccess
    protected final boolean tryAcquire(int acquires) {
    
    
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
    
    
            // 相对非公平实现, 公平实现只多了这个 : !hasQueuedPredecessors()
            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;
    }
}

Compared with the unfair implementation, the fair implementation only has this: !hasQueuedPredecessors(), you can see the implementation of AQS for the specific implementation of this method.

To put it simply, it is to traverse the waiting queue to determine whether there are queued threads. If there is a queuing thread, it will directly exit the acquisition and go to the queue of AQS.

image-20210524185829692

ReentrantLockHow to decide what kind of lock it is?

Because there are two internal implementations, simply speaking, it is to hold the instance Syncof , and delegate the relevant lock operation to the instance. Instead, you ReentrantLockonly need to provide parameters or constructors to let the caller decide which implementation it is.

The default is an unfair lock , because it can provide better throughput and reduce the number of thread blocking and wake-ups.

It is mainly the batch of threads that came to compete when the service just released the lock.

public class ReentrantLock implements Lock, java.io.Serializable {
    
    
    private final Sync sync;public ReentrantLock() {
    
    
        sync = new NonfairSync();
    }
    public ReentrantLock(boolean fair) {
    
    
        sync = fair ? new FairSync() : new NonfairSync();
    }
}

The public method of the lock is directly delegated toSync

Obtain
void lock() {
    
    sync.acquire(1);}
void lockInterruptibly() throws InterruptedException {
    
    sync.acquireInterruptibly(1);}
public boolean tryLock() {
    
    return sync.nonfairTryAcquire(1);}
boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
    
    
    return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}

Different from several other methods (directly call the public method of AQS, the implementation of Sync decides whether it is a fair lock or an unfair lock), tryLockit directly calls the implementation of the internal unfair lock nonfairTryAcquire.

Because tryLock can neither respond to interrupts nor time out, it is easy to cause too long waiting time for fair locks.

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

As mentioned above, Condition has only one implementation of AQS internal class, so the method of obtaining Condition can also be directly delegated to Sync.

Statistics public methods directly delegate toSync

Since state is endowed with a specific meaning (number of reentries), relevant judgments and statistical methods will be judged or obtained based on this field first.

// Methods relayed from outer class

final Thread getOwner() {
    
    
    return getState() == 0 ? null : getExclusiveOwnerThread();
}

final int getHoldCount() {
    
    
    return isHeldExclusively() ? getState() : 0;
}

final boolean isLocked() {
    
    
    return getState() != 0;
}

/**
 * Reconstitutes the instance from a stream (that is, deserializes it).
 */
private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {
    
    
    s.defaultReadObject();
    setState(0); // reset to unlocked state
}

readObjectThe implementation determines Reentrantlockthat the deserialization result of must be lock-free, although I don't know what to use.

Summarize

I used to think it Reentrantlockwas quite complicated, because the complicated content is in AQS. If you understand AQS, it Reentrantlockwill be very straightforward.

Guess you like

Origin blog.csdn.net/jiangxiayouyu/article/details/118107961