What is a read-write lock
In the last article, we talked about reentrant locks ReentrantLcok However, it is also an exclusive lock (also known as an exclusive lock), that is, only one thread is allowed to hold it at the same time, but in most scenarios, there are more reads and less writes, and there is no data competition problem in reading, so there is no thread safety problem. Therefore, if you use it at this time ReentrantLcok , efficiency is bound to be low, so there is a readwritelock ReentrantReadWriteLock.
internal structure
The read-write lock is also implemented based on AQS. It internally maintains a pair of locks, a read lock and a write lock. By separating read lock and write lock, concurrency is significantly improved compared with general exclusive lock. At the same time, it also has the properties of fair lock and unfair lock. Therefore, the read-write lock is mainly analyzed based on unfair lock.
/** Inner class providing readlock */ private final ReentrantReadWriteLock.ReadLock readerLock; /** Inner class providing writelock */ private final ReentrantReadWriteLock.WriteLock writerLock;
Class diagram is as follows:
Simple use
Read / write locks are generally divided into the following three situations:
- Read concurrency, that is, multiple read threads can be accessed and shared at the same time.
- Read and write are mutually exclusive, that is, when the write thread accesses, all read threads and other write threads are blocked.
- Write and write are mutually exclusive. When the write thread accesses, other write threads are blocked.
Use the following code to demonstrate these three situations:
1. Thread 1 gets the write lock. Because the write lock is exclusive, other threads cannot get the lock.
2. Thread 2 goes to get the read lock.
3. Thread 3 goes to get the read lock.
4. Thread 4 goes to get the write lock.
5. Thread 5 goes to get the read lock.
Q: if t5 is read lock, will it be executed together with t2 and t3?
@Slf4j(topic = "ayue") public class RWTest { public static void main(String[] args) throws InterruptedException { ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); //Read lock Lock rl = rwl.readLock(); //Write lock Lock wl = rwl.writeLock(); /** * t1 It is a write lock. Let it sleep for 5s. At this time, other threads cannot get the lock */ new Thread(() -> { wl.lock(); try { log.info("t1 Get write lock"); TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } finally { wl.unlock(); log.info("t1 Release write lock"); } }, "t1").start(); //Sleep for 1s and let it execute first by pressing t1 TimeUnit.SECONDS.sleep(1); /** * At this time, t2 cannot get the lock within 5s */ new Thread(() -> { rl.lock(); try { log.info("t2 Acquire read lock"); TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } finally { rl.unlock(); log.info("t2 Release read lock"); } }, "t2").start(); /** * At this time, t3 cannot get the lock within 5s * * But after t1 is released, t2 and t3 can get the lock at the same time */ new Thread(() -> { rl.lock(); try { log.info("t3 Acquire read lock"); TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } finally { rl.unlock(); log.info("t3 Release read lock"); } }, "t3").start(); /** * t4 Obtain the read lock, sleep for 10 seconds, and ask if t5 you can obtain the read lock after release */ new Thread(() -> { wl.lock(); try { log.info("t4 Get write lock"); TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } finally { wl.unlock(); log.info("t4 Release write lock"); } }, "t4").start(); /** * t5 Is the read lock executed together with T2 and T3? * */ new Thread(() -> { rl.lock(); try { log.info("t5 Acquire read lock"); } finally { rl.unlock(); log.info("t5 Release read lock"); } }, "t5").start(); } }
Output:
11:43:36.390 [t1] INFO ayue - t1 Get write lock 11:43:41.398 [t1] INFO ayue - t1 Release write lock 11:43:41.398 [t2] INFO ayue - t2 Acquire read lock 11:43:41.398 [t3] INFO ayue - t3 Acquire read lock 11:43:42.411 [t3] INFO ayue - t3 Release read lock 11:43:42.411 [t2] INFO ayue - t2 Release read lock 11:43:42.411 [t4] INFO ayue - t4 Get write lock 11:43:52.423 [t4] INFO ayue - t4 Release write lock 11:43:52.423 [t5] INFO ayue - t5 Acquire read lock 11:43:52.423 [t5] INFO ayue - t5 Release read lock
According to the results, we can know:
If a t1 write lock gets the lock first, followed by some other locks (possibly read locks or write locks), when t1 releases the lock, wake up the waiting thread according to the FIFO principle. If the first awakened thread is a write lock (if t2 is a write lock), it will not wake up t3 and subsequent locks. It will wake up t3 only after t2 execution is completed. Assuming that the awakened t2 is a read lock, t2 will judge whether his next t3 is a read lock. If so, t3 will be awakened. After t3 is awakened, t4 will judge whether t4 is a read lock. If t4 is also a read lock, t5 will be awakened, and so on. However, if t4 is awakened as a write lock, t5 will not be awakened, even if the following t5 is a read lock.
Therefore, the above situation will occur, t2 and t3 will be executed together, and t5 will not be executed together.
Lock degradation
Lock degradation refers to the process of obtaining a write lock, obtaining a read lock, and then releasing a write lock. Lock degradation is to ensure data visibility. Lock degradation is one of the important features of ReentrantReadWriteLock.
public class RWDowngrade { public static void main(String[] args) { ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); //Read lock Lock rl = rwl.readLock(); //Write lock Lock wl = rwl.writeLock(); /** * Lock degradation */ new Thread(() -> { wl.lock(); try { System.out.println("Get write lock first"); rl.lock(); System.out.println("Acquire read lock after"); } finally { rl.unlock(); wl.unlock(); } }, "t").start(); } }
Output:
Get write lock first Acquire read lock after
Note: ReentrantReadWriteLock does not upgrade locks. (the reason will be known in the source code)
As follows, if the read lock is obtained first and the write lock is obtained later, the deadlock will occur:
new Thread(() -> { rl.lock(); try { System.out.println("Acquire read lock first"); wl.lock(); System.out.println("Acquire write lock after"); } finally { wl.unlock(); rl.unlock(); } }, "t").start();
Output, directly stuck:
Source code analysis
ReentrantReadWriteLock and ReentrantLock are actually the same. The Lock core is Sync, and the read Lock and write Lock are implemented based on Sync. From this analysis, ReentrantReadWriteLock is actually a Lock, but two different implementation methods are designed according to different scenarios. Its read-write Lock is divided into two internal classes: ReadLock and WriteLock, both of which implement the Lock interface.
The read-write lock is also implemented based on AQS, but we know that the synchronization status is determined according to the state and int integer variables in AQS, and the read-write lock is two locks. How can we represent two locks through an attribute?
How to maintain multiple states on an integer? We know that the int type is 32 bits, so we need to cut the variable by bit. The read-write lock cuts the variable into two parts. The high 16 bits represent read and the low 16 bits represent write.
How to quickly determine the status of read and write locks after segmentation? Through bit operation (those who forget bit operation can see my previous article: Bit operation in Java ), if the current synchronization state is c, the write state is equal to c & 0x0000ffff (all the high 16 bits are erased), and the read state is equal to c > > > 16 (unsigned complement 0 moves 16 bits right).
The code is as follows:
static final int SHARED_SHIFT = 16; static final int SHARED_UNIT = (1 << SHARED_SHIFT); static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1; static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; /** Return to read status */ static int sharedCount(int c) { return c >>> SHARED_SHIFT; } /** Return to write status */ static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
According to the division of States, a corollary can be drawn: when c is not equal to 0, when the write state is equal to 0, the read state is greater than 0, that is, the read lock has been obtained. Such comments can also be seen in the source code.
(Note: if c != 0 and w == 0 then shared count != 0)
Acquisition and release of write lock
A write lock is an exclusive lock that supports reentry. If the current thread has acquired a write lock, the write state is increased. If the read lock has been acquired when the current thread acquires the write lock (the read state is not 0) or the thread is not a thread that has acquired the write lock, the current thread enters the waiting state.
Acquisition of write lock
The acquisition of write lock mainly depends on the tryAcquire() method. The source code is as follows:
protected final boolean tryAcquire(int acquires) { //Current thread Thread current = Thread.currentThread(); //Gets the status of the lock int c = getState(); //Write lock status int w = exclusiveCount(c); if (c != 0) {//If locked /** * 1,w == 0,It is used to judge what the current lock is. If w is 0, it means that the lock has only read lock but not write lock * In other words, the current thread is to obtain the read lock, but this method was originally used to obtain the write lock, so the current thread * It is equivalent to lock upgrade, so obtaining the lock failed. * 2,If you enter 𞓜, then w= 0, indicating that the write lock has been applied. Judge whether the current thread is reentrant, if not failed. */ if (w == 0 || current != getExclusiveOwnerThread()) return false; /** * If the number of write locks obtained exceeds the maximum value of 65535, a direct exception will occur. Generally, it will not be exceeded. */ if (w + exclusiveCount(acquires) > MAX_COUNT) throw new Error("Maximum lock count exceeded"); // Reentrant acquire setState(c + acquires); return true; } /** * writerShouldBlock(),Simply put, it is to judge whether it is necessary to queue. * If it is a non fair lock, it directly returns false, that is, it directly grabs the lock regardless of whether there is a thread queuing or not. * If it is a fair lock, its bottom layer calls the hasQueuedPredecessors() method, which will judge whether there are still queued threads in the queue. If there are, it will * Queue up, if not, try locking. */ if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) return false; setExclusiveOwnerThread(current); return true; }
By analyzing the source code:
If there is a read lock, the write lock cannot be obtained. The reason is that the read-write lock should ensure that the operation of the write lock is 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 other read threads release the read lock. Once the write lock is obtained, subsequent access of other read and write threads will be blocked.
Release of write lock
Release and of write lock ReentrantLock The release process of is basically similar to that in the previous article, which can be viewed by yourself. It is not repeated here.
Acquisition and release of read lock
Read lock is a shared lock that supports re-entry. It can be acquired by multiple threads at the same time. When no other write thread accesses (or the write status is 0), the read lock will always be acquired successfully. It should be noted that if the current thread has acquired the read lock, the read state will be increased. If the current thread obtains the read lock and the write lock has been obtained by other threads, it enters the waiting state.
Acquisition of read lock
The acquisition of write lock mainly depends on the tryAcquireShared() method. The source code is as follows:
//Try to obtain the sharing status. If the sharing status is greater than or equal to 0, it means that the lock is obtained successfully. Otherwise, join the synchronization queue. protected final int tryAcquireShared(int unused) { Thread current = Thread.currentThread(); int c = getState(); /* * 1,exclusiveCount(c) != 0,Determine whether it is write locked. * 2,If the write lock is on, enter & &, why continue to judge when the write lock is on? This is the degradation of the lock. * If you re-enter, you must be degraded. If you do not re-enter, you will fail, because reading and writing need to be mutually exclusive. */ if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current) return -1; /** * If the above code does not return to execution, there are two cases * 1,No one wrote on the lock * 2,Reentrant demotion */ int r = sharedCount(c); /** * readerShouldBlock(): Determine whether the lock needs to wait * r < MAX_COUNT: Determine whether the number of locks exceeds the maximum value of 65535 * compareAndSetState(c, c + SHARED_UNIT): Set sharing status (read lock status) */ if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) { // r==0, indicating that no thread is currently acquiring a read lock if (r == 0) { // Set the current thread as the first thread to acquire the read lock firstReader = current; // The count is set to 1 firstReaderHoldCount = 1; } else if (firstReader == current) { // Indicates a reentry lock on which + 1 is counted firstReaderHoldCount++; } else { /** * HoldCounter It is mainly a class to record the number of locks obtained by threads * cachedHoldCounter What is cached is the HoldCounter object of the last thread to acquire the lock */ 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; } //Try again in spin mode return fullTryAcquireShared(current); }
In the tryAcquireShared(int unused) method, if other threads have obtained the write lock, the current thread fails to obtain the read lock and enters the waiting state. If the current thread obtains the write lock or the write lock is not obtained, the current thread (thread safety, guaranteed by CAS) increases the read state and successfully obtains the read lock.
Release of read lock
protected final boolean tryReleaseShared(int unused) { Thread current = Thread.currentThread(); //Judge that the current thread is the first thread to obtain the read lock if (firstReader == current) { // assert firstReaderHoldCount > 0; // Judge that the number of times to obtain the lock is 1. If it is 1, it indicates that there is no reentry, and directly release firstReader = null; Otherwise, the number of locks held by the thread will be - 1 if (firstReaderHoldCount == 1) firstReader = null; else firstReaderHoldCount--; } else { // If the current thread is not the first thread to acquire a read lock. // Gets the HoldCounter in the cache HoldCounter rh = cachedHoldCounter; // If the HoldCounter in the cache does not belong to the current thread, get the HoldCounter of the current thread. if (rh == null || rh.tid != getThreadId(current)) rh = readHolds.get(); int count = rh.count; if (count <= 1) { // If the number of locks held by the thread is less than 1, delete HoldCounter directly readHolds.remove(); if (count <= 0) throw unmatchedUnlockException(); } // If the number of locks held is greater than 1, perform the - 1 operation --rh.count; } // Spin release synchronization state for (; ; ) { int c = getState(); int nextc = c - SHARED_UNIT; if (compareAndSetState(c, nextc)) // Releasing the read lock has no effect on readers, // but it may allow waiting writers to proceed if // both read and write locks are now free. return nextc == 0; } }
Lock release is relatively simple. First, check whether the current thread is the first thread to obtain the read lock. If yes and no reentry occurs, set the read lock variable obtained for the first time to null. If reentry occurs, the read lock counter - 1 will be obtained for the first time. Second, check whether the counter in the cache is empty or whether it is the current thread, If it is empty or not, the counter of the current thread is obtained. If the number of counters is less than 1, the counter is deleted from ThreadLocl and the counter value is - 1. If it is less than or equal to 0, an exception occurs. Finally, modify the synchronization state.
High performance read / write lock StampedLock
The performance of ReentrantReadWriteLock is already very good, but a series of CAS operations are still required to lock at the bottom. Therefore, JUC also provides another read-write lock, StampedLock.
If StampedLock is a read lock, there is no CAS operation. Therefore, its performance is better than ReentrantReadWriteLock. It is also called optimistic read lock, that is, when reading and obtaining the lock, it does not lock and directly returns a value. Then, when executing the critical area, it verifies whether the value has been modified (write operation locking). If it has not been modified, it directly executes the code in the critical area, If it is modified, it needs to be upgraded to read-write lock ReadLock.
Basic grammar
//The acquisition stamp does not have a lock long stamp = lock.tryOptimisticRead(); //Verification stamp if(lock.validate(stamp)){ //If yes, execute the code of the critical area //return } //If it does not return, it indicates that it has been modified and needs to be upgraded to readLock lock.readLock();
Code example
import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.StampedLock; @Slf4j(topic = "ayue") public class DataContainer { int i; long stampw = 0l; public void setI(int i) { this.i = i; } private final StampedLock lock = new StampedLock(); //First add StampedLock @SneakyThrows public int read() { //Try an optimistic reading long stamp = lock.tryOptimisticRead(); log.debug("StampedLock Read the stamp from the lock{}", stamp); //Stamp verification after 1s TimeUnit.SECONDS.sleep(1); //Check stamp if (lock.validate(stamp)) { log.debug("StampedLock Verification complete stamp{}, data.i: {}", stamp, i); return i; } //The verification must have failed log.debug("Validation failed and was changed by the write thread{}", stampw); try { //The upgrade of the lock will also change the stamp stamp = lock.readLock(); log.debug("Locking after upgrade succeeded {}", stamp); TimeUnit.SECONDS.sleep(1); log.debug("Upgrade read lock completed{}, data.i: {}", stamp, i); return i; } finally { log.debug("Upgrade lock unlock {}", stamp); lock.unlockRead(stamp); } } @SneakyThrows public void write(int i) { //cas locking stampw = lock.writeLock(); log.debug("Write lock succeeded {}", stampw); try { TimeUnit.SECONDS.sleep(5); this.i = i; } finally { log.debug("Write lock unlock {},data.i: {}", stampw, i); lock.unlockWrite(stampw); } } public static void main(String[] args) throws InterruptedException { //Instantiate data container DataContainer dataContainer = new DataContainer(); //An initial value is assigned to the construction method without writing dataContainer.setI(1); //read new Thread(() -> { dataContainer.read(); }, "t1").start(); new Thread(() -> { dataContainer.read(); }, "t2").start(); // new Thread(() -> { // dataContainer.write(9); // }, "t3").start(); } }
Both are read lock outputs (t1, t2):
11:58:48.839 [t1] DEBUG ayue - StampedLock Read the stamp 256 from the lock 11:58:48.839 [t2] DEBUG ayue - StampedLock Read the stamp 256 from the lock 11:58:49.854 [t2] DEBUG ayue - StampedLock Verification complete stamp256, data.i: 1 11:58:49.854 [t1] DEBUG ayue - StampedLock Verification complete stamp256, data.i: 1
If modified (t1, t3):
12:07:36.234 [t1] DEBUG ayue - StampedLock Read the stamp 256 from the lock 12:07:37.238 [t3] DEBUG ayue - Write lock successful 384 12:07:41.213 [t1] DEBUG ayue - Verification failure was changed 384 by the write thread 12:07:42.240 [t3] DEBUG ayue - Write lock unlock 384,data.i: 9 12:07:42.240 [t1] DEBUG ayue - Locking after upgrade succeeded 513 12:07:43.252 [t1] DEBUG ayue - Upgrade read lock completed 513, data.i: 9 12:07:43.252 [t1] DEBUG ayue - Upgrade lock unlock 513
Can StampedLock replace ReentrantReadWriteLock
Although the performance of StampedLock is higher than that of ReentrantReadWriteLock, it also needs to be used by scene. StampedLock has the following characteristics:
- Reentry is not supported
- Conditional queues are not supported
- There are some concurrency problems
summary
When a thread holds a read lock, the thread cannot obtain a write lock (to ensure that the write operation remains visible to all subsequent read operations).
When a thread holds a write lock, the thread can continue to acquire the read lock (if it is found that the write lock is occupied when acquiring the read lock, the acquisition will fail only if the write lock is not occupied by the current thread).
Think about it carefully. This design is reasonable: when a thread acquires a read lock, other threads may also hold a read lock, so the thread that acquires a read lock cannot be "upgraded" to a write lock; For the thread that obtains the write lock, it must monopolize the read-write lock, so it can continue to obtain the read lock. When it obtains the write lock and read lock at the same time, it can also release the write lock and continue to hold the read lock. In this way, a write lock will be "degraded". In order to read, if a thread wants to hold the write lock and read lock at the same time, it must obtain the write lock first and then obtain the read lock.
Write locks can be "downgraded" to read locks, and read locks cannot be "upgraded" to write locks.