java multithreaded programming -- various locks -- exclusive lock VS shared lock

Keywords: Java Back-end Multithreading

reference resources: https://tech.meituan.com/2018/11/15/java-lock.html
https://www.cnblogs.com/jyroy/p/11365935.html

Exclusive lock and shared lock are the same concept. Let's first introduce the specific concepts, and then introduce the exclusive lock and shared lock through the source code of ReentrantLock and ReentrantReadWriteLock.

Exclusive Lock is also called exclusive Lock, which means that the Lock can only be held by one thread at A time. If thread T adds an exclusive Lock to data A, other threads cannot add any type of Lock to A. The thread that obtains the exclusive Lock can read and modify the data. The implementation classes of synchronized in JDK and Lock in JUC are mutually exclusive locks.

A shared lock means that the lock can be held by multiple threads. If thread T adds a shared lock to data a, other threads can only add a shared lock to a, not an exclusive lock. The thread that obtains the shared lock can only read data and cannot modify data.

Exclusive locks and shared locks are also realized through AQS. Exclusive locks or shared locks can be realized through different methods.

The following figure shows part of the source code of ReentrantReadWriteLock:

We can see that ReentrantReadWriteLock has two locks: ReadLock and WriteLock, which are known by words. One read lock and one write lock are collectively referred to as "read-write lock". Further observation shows that ReadLock and WriteLock are locks implemented by the internal class sync. Sync is a subclass of AQS. This structure also exists in CountDownLatch, ReentrantLock and Semaphore.

In ReentrantReadWriteLock, the lock bodies of read lock and write lock are Sync, but the locking methods of read lock and write lock are different. A read lock is a shared lock and a write lock is an exclusive lock. The shared lock of read lock can ensure that concurrent reads are very efficient, and the processes of read-write, write read and write are mutually exclusive, because read lock and write lock are separated. Therefore, the concurrency of ReentrantReadWriteLock is much higher than that of general mutex locks.

What is the difference between the specific locking methods of read lock and write lock? Before we know the source code, we need to review other knowledge. When we first mentioned AQS, we also mentioned the state field (int type, 32-bit), which is used to describe how many threads hold locks.

In exclusive locks, this value is usually 0 or 1 (if it is a reentry lock, the state value is the number of reentries). In shared locks, state is the number of locks held. However, there are read and write locks in ReentrantReadWriteLock, so you need to describe the number of read locks and write locks (or state) on an integer variable state. Therefore, the state variable "cut by bit" is divided into two parts. The high 16 bits represent the read lock state (number of read locks) and the low 16 bits represent the write lock state (number of write locks). As shown in the figure below:

After understanding the concept, let's look at the code. First, let's look at the lock source code for writing locks:

protected final boolean tryAcquire(int acquires) {
	Thread current = Thread.currentThread();
	int c = getState(); // Get the number of current locks
	int w = exclusiveCount(c); // Number of write locks w
	if (c != 0) { // If a thread already holds a lock (c!=0)
    // (Note: if c != 0 and w == 0 then shared count != 0)
		if (w == 0 || current != getExclusiveOwnerThread()) // If the number of write threads (w) is 0 (in other words, there is a read lock) or the thread holding the lock is not the current thread, a failure is returned
			return false;
		if (w + exclusiveCount(acquires) > MAX_COUNT)    // If the number of write locks is greater than the maximum number (65535, the 16th power of 2 - 1), an Error is thrown.
      throw new Error("Maximum lock count exceeded");
		// Reentrant acquire
    setState(c + acquires);
    return true;
  }
  if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) // If the number of write threads is 0 and the current thread needs to be blocked, a failure is returned; Or if it fails to increase the number of write threads through CAS, it also returns a failure.
		return false;
	setExclusiveOwnerThread(current); // If c=0, w=0 or C > 0, w > 0 (reentry), set the owner of the current thread or lock
	return true;
}
  • This code first obtains the number of current locks c, and then obtains the number of write locks w through c. Because the write lock is the lower 16 bits, take the maximum value of the lower 16 bits and the current c to do the sum operation (int w)
    = exclusiveCount ©; ), The upper 16 bits and 0 are 0 after the operation. The rest is the value of the lower operation and the number of threads holding write locks.
  • After obtaining the number of write lock threads, first judge whether a thread already holds a lock. If a thread already holds a lock (c!=0), check the number of current write lock threads. If the number of write threads is 0 (that is, there is a read lock at this time) or the thread holding the lock is not the current thread, it will return a failure (involving the implementation of fair lock and unfair lock).
  • If the number of write locks is greater than the maximum number (65535, the 16th power of 2 - 1), an Error is thrown.
  • If the number of write threads is 0 (the number of read threads should also be 0, because c!=0 has been handled above), and the current thread needs to be blocked, a failure is returned; if increasing the number of write threads through CAS fails, a failure is also returned.
  • If c=0,w=0 or C > 0, w > 0 (reentry), set the owner of the current thread or lock and return success!

tryAcquire() except for the reentry condition (the current thread is the thread that has obtained the write lock) In addition, a judgment is added to determine whether a read lock exists. If a read lock exists, the write lock cannot be obtained because: the operation of the write lock must be visible to the read lock. If the read lock is allowed to obtain the write lock when it has been obtained, other running read threads cannot perceive the operation of the current write thread.

Therefore, the write lock can only be obtained by the current thread after waiting for other read threads to release the read lock. Once the write lock is obtained, the subsequent access of other read and write threads will be blocked. The release process of write lock is basically similar to that of ReentrantLock. Each release reduces the write state. When the write state is 0, it indicates that the write lock has been released, and then the waiting read and write threads can be released Continue to access the read-write lock, and the modification of the previous write thread is visible to subsequent read-write threads.

Next is the code for reading the lock:

protected final int tryAcquireShared(int unused) {
    Thread current = Thread.currentThread();
    int c = getState();
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;                                   // If other threads have acquired the write lock, the current thread fails to acquire the read lock and enters the waiting state
    int r = sharedCount(c);
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {
        if (r == 0) {
            firstReader = current;
            firstReaderHoldCount = 1;
        } else if (firstReader == current) {
            firstReaderHoldCount++;
        } else {
            HoldCounter rh = cachedHoldCounter;
            if (rh == null || rh.tid != getThreadId(current))
                cachedHoldCounter = rh = readHolds.get();
            else if (rh.count == 0)
                readHolds.set(rh);
            rh.count++;
        }
        return 1;
    }
    return fullTryAcquireShared(current);
}

You can see that in the tryAcquireShared(int unused) method, if other threads have acquired the write lock, the current thread fails to acquire the read lock and enters the waiting state. If the current thread has acquired the write lock or the write lock has not been acquired, the current thread (thread safety, guaranteed by CAS) increases the read state and successfully acquires the read lock. Each release of the read lock (thread safe, multiple read threads may release the read lock at the same time) reduce the read state, and the reduced value is "1 < < 16". Therefore, the read-write lock can realize the sharing of the read-write process, and the read-write, write-read and write processes are mutually exclusive.

At this point, let's look back at the locking source code of fair and unfair locks in the mutex ReentrantLock:

We found that although there are two kinds of ReentrantLock, fair lock and unfair lock, they add exclusive locks. According to the source code, when a thread calls the lock method to obtain a lock, if the synchronization resource is not locked by other threads, the current thread will successfully seize the resource after successfully updating the state with CAS. If the public resource is occupied and not occupied by the current thread, locking will fail. Therefore, it can be determined that the added locks of ReentrantLock are exclusive locks regardless of read or write operations.

Posted by Mega on Mon, 06 Dec 2021 20:43:48 -0800