Continue to create, accelerate growth! This is the 4th day of my participation in the "Nuggets Daily New Plan · June Update Challenge", click to view the details of the event
Introduction to Read-Write Locks
There is such a scenario in reality: there are read and write operations on shared resources, and write operations are not as frequent as read operations (more reads and less writes). When there is no write operation, there is no problem with multiple threads reading a resource at the same time, so multiple threads should be allowed to read shared resources at the same time (reading and reading can be concurrent); but if a thread wants to write to these shared resources, it should not Other threads are allowed to read and write to the resource (read-write, write-read, write-write mutex). Read-write locks can provide better concurrency and throughput than exclusive locks in situations where there are more reads than writes.
For this scenario, JAVA's concurrent package provides a read-write lock ReentrantReadWriteLock , which internally maintains a pair of related locks, one for read-only operations, called read locks; one for write operations, called write locks , described as follows: Preconditions for a thread to enter a read lock:
- No write locks for other threads
- There is no write request or there is a write request, but the calling thread and the thread holding the lock are the same.
The prerequisites for the thread to enter the write lock:
- No read locks by other threads
- No write locks for other threads
The read-write lock has the following three important characteristics:
- Fairness selectivity : Supports both unfair (default) and fair lock acquisition methods, and throughput is still unfair over fairness.
- Reentrant: Both read locks and write locks support thread reentrancy. Take the read-write thread as an example: after the read thread acquires the read lock, it can acquire the read lock again. After acquiring the write lock, the writer thread can acquire the write lock again, and can also acquire the read lock.
- Lock downgrade: Following the sequence of acquiring a write lock, then acquiring a read lock, and finally releasing the write lock, a write lock can be downgraded to a read lock.
After reading the above description, you may be a little dizzy. I will give an example of a previous development order to help you understand. Our order has a concept of main order and sub-order: the main order is coded as orderCode
, the sub-order is coded as the subOrderCode
corresponding relationship is 1:N. When I am refunding, I need to support the sub-bills, and the main-bills will be refunded. The dimension of the refund of the sub-order is the refund of the subOrderCode
main order, and the dimension is orderCode
that there may be concurrency, we can orderCode
add a read-write lock to
- If it is the case of the refund of the main order, whether the refund of the sub-order is mutually exclusive
- If it is the case of a refund of a sub-order, it can actually be parallelized, but the sub-order is a
subOrderCode
dimension , and asubOrderCode
mutex of a must be added.
Read-write lock usage
How to store read-write locks at the same time can be stored by the value of state. The upper 16 bits represent the read lock, and the lower 16 bits represent the write lock. For example: 0000 0000 0000 0000 (1<<16) 0000 0000 0000 0000 The upper 16 bits are not 0: there is a read lock c >>>16 The lower 16 bits are not 0: there is a write lock 5
ReadWriteLock interface
We can see that ReentranReadWriteLock has two locks, a read lock and a write lock.
Example of use
Cache operation:
public class ReentrantReadWriteLockCacheTest {
static Map<String, Object> map = new HashMap<String, Object>();
static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
static Lock r = rwl.readLock();
static Lock w = rwl.writeLock();
// 获取一个key对应的value
public static final Object get(String key) {
r.lock();
try {
return map.get(key);
} finally {
r.unlock();
}
}
// 设置key对应的value,并返回旧的value
public static final Object put(String key, Object value) {
w.lock();
try {
return map.put(key, value);
} finally {
w.unlock();
}
}
// 清空所有的内容
public static final void clear() {
w.lock();
try {
map.clear();
} finally {
w.unlock();
}
}
}
复制代码
In the above example, Cache combines a non-thread-safe HashMap as the implementation of the cache, and uses the read lock and write lock of the read-write lock to ensure that the Cache is thread-safe. In the read operation get(String key) method, a read lock needs to be acquired, which prevents concurrent access to the method without blocking. For the write operation put(String key, Object value) method and clear() method, the write lock must be acquired in advance when updating the HashMap. After the write lock is acquired, other threads are blocked for the acquisition of the read lock and the write lock, and only the write lock After the lock is released, other read and write operations can continue. Cache uses read-write locks to improve the concurrency of read operations, ensure the visibility of all read and write operations for each write operation, and simplify programming.
lock downgrade
Lock downgrade refers to the downgrade of a write lock to a read lock. If the current thread holds the write lock, then releases it, and finally acquires the read lock, this segmented process cannot be called lock downgrade. Lock downgrade refers to the process of holding the (currently owned) write lock, acquiring the read lock, and then releasing the (previously owned) write lock. Lock downgrade can help us get the modified result of the current thread without being destroyed by other threads, preventing the loss of updates.
Example use of lock downgrade
Because the data does not change frequently, multiple threads can process data concurrently. When the data changes, if the current thread senses the data change, it prepares the data, and other processing threads are blocked until the current thread completes the data processing. Preparation.
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock readLock = rwl.readLock();
private final Lock writeLock = rwl.writeLock();
private volatile boolean update = false;
public void processData() {
readLock.lock();
if (!update) {
// 必须先释放读锁
readLock.unlock();
// 锁降级从写锁获取到开始
writeLock.lock();
try {
if (!update) {
// TODO 准备数据的流程(略)
update = true;
}
readLock.lock();
} finally {
writeLock.unlock();
}
// 锁降级完成,写锁降级为读锁
}
try {
//TODO 使用数据的流程(略)
} finally {
readLock.unlock();
}
}
复制代码
Precautions:
- Read locks do not support condition variables
- No upgrade during reentrancy. Not supported: In the case of holding a read lock, acquiring a write lock will result in a permanent wait
- Support downgrade during reentrancy: if you hold a write lock, you can acquire a read lock
ReentranReadWriteLock structure
method structure design
Read and write state design
Source code analysis
write lock
The method tryAcquire
is the lock core logic of the write lock
protected final boolean tryAcquire(int acquires) {
/*
* Walkthrough:
* 1. If read count nonzero or write count nonzero
* and owner is a different thread, fail.
* 2. If count would saturate, fail. (This can only
* happen if count is already nonzero.)
* 3. Otherwise, this thread is eligible for lock if
* it is either a reentrant acquire or
* queue policy allows it. If so, update state
* and set owner.
*/
Thread current = Thread.currentThread();
int c = getState();
// 获取写锁状态
int w = exclusiveCount(c);
if (c != 0) {
// (Note: if c != 0 and w == 0 then shared count != 0)
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
// 重入
setState(c + acquires);
return true;
}
// 获取写锁
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
// 设置写锁 owner
setExclusiveOwnerThread(current);
return true;
}
复制代码
write lock release
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
复制代码
Read lock acquisition
protected final int tryAcquireShared(int unused) {
/*
* Walkthrough:
* 1. If write lock held by another thread, fail.
* 2. Otherwise, this thread is eligible for
* lock wrt state, so ask if it should block
* because of queue policy. If not, try
* to grant by CASing state and updating count.
* Note that step does not check for reentrant
* acquires, which is postponed to full version
* to avoid having to check hold count in
* the more typical non-reentrant case.
* 3. If step 2 fails either because thread
* apparently not eligible or CAS fails or count
* saturated, chain to version with full retry loop.
*/
Thread current = Thread.currentThread();
int c = getState();
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
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 {
// 后续线程,通过 ThreadLocal 获取重入次数
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);
}
复制代码
fullTryAcquireShared
Methods as below:
final int fullTryAcquireShared(Thread current) {
/*
* This code is in part redundant with that in
* tryAcquireShared but is simpler overall by not
* complicating tryAcquireShared with interactions between
* retries and lazily reading hold counts.
*/
HoldCounter rh = null;
for (;;) {
int c = getState();
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
// else we hold the exclusive lock; blocking here
// would cause deadlock.
} else if (readerShouldBlock()) {
// Make sure we're not acquiring read lock reentrantly
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
} else {
if (rh == null) {
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) {
rh = readHolds.get();
if (rh.count == 0)
readHolds.remove();
}
}
if (rh.count == 0)
return -1;
}
}
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
if (compareAndSetState(c, c + SHARED_UNIT)) {
if (sharedCount(c) == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
if (rh == null)
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
cachedHoldCounter = rh; // cache for release
}
return 1;
}
}
}
复制代码
release of read lock
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
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;
}
}
复制代码