Part of this article is excerpted from The Art of Concurrent Programming in Java
Reentrant lock
A ReentrantLock, as its name implies, is a ReentrantLock that allows a thread to repeatedly lock a resource. In addition, the lock supports fair and unfair choices when acquiring locks
The so-called unsupported entry can be considered in the following scenarios: If a thread calls the lock() method again after it has called the lock() method, the thread will block by itself because tryAcquire(int acquires) returns false, causing the thread to block
The synchronize keyword implicitly supports re-entry, such as a recursive method with synchronize modification, in which the thread of execution obtains the lock multiple times after the method is executed. ReentrantLock does not support implicit re-entry like the Synchronize keyword, but when the lock() method is called, a thread that has acquired the lock can call the lock() method again without blocking
1. Implement re-entry
The implementation of the reentry feature needs to solve the following two problems:
-
The thread acquires the lock again
The lock needs to identify whether the thread acquiring the lock is the thread currently occupying the lock, and if so, it is successfully acquired again
-
The final release of the lock
The thread repeats the lock n times, and after releasing the lock for the NTH time, other threads can acquire the lock. To implement this feature, use counts should be considered
ReentrantLock combines custom synchronizers to obtain and release locks. Take an unfair lock as an example. The code for obtaining the synchronization status is shown as follows, adding the processing logic for obtaining the synchronization status again
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true; }}// Determine whether the current thread is the thread that acquires the lock
else if (current == getExclusiveOwnerThread()) {
// Increments the synchronization value and returns true
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
Copy the code
Considering that the thread that successfully acquired the lock acquires the lock again and only increases the synchronization state value, this requires ReentrantLock to decrease the synchronization state value when releasing the synchronization state. The code for this method is as follows:
protected final boolean tryRelease(int releases) {
// Reduce the status value
int c = getState() - releases;
if(Thread.currentThread() ! = getExclusiveOwnerThread())throw new IllegalMonitorStateException();
boolean free = false;
// When the synchronization status is 0, set the owning thread to null and return true, indicating that the release was successful
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
Copy the code
2. The difference between fair and unfair lock acquisition
If a lock is fair, the order in which the lock is acquired should conform to the absolute chronological order of the request, known as FIFO. Recall from the previous section that an unfair lock is acquired by the current thread as long as the CAS synchronization status is set successfully. A fair lock is different, and the code looks like this:
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
Hasqueuedtoraise () * This method is used to determine whether the current node has a predecessor * If the method returns true, Indicates that a thread has requested the lock before the current thread * and therefore needs to wait for the precursor thread to release the lock before continuing to acquire it */
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;
}
Copy the code
Read-write lock
The locks mentioned above are basically exclusive locks that allow access by only one thread at a time, while read and write locks allow access by multiple threads at a time. However, when the writer thread accesses, all reader threads and other writer threads are blocked. Read-write locks maintain a pair of locks, a read lock and a write lock. By separating the read lock and the write lock, the concurrency is greatly improved compared to the general exclusive lock
1. Interface example
The following uses a cache example to illustrate the use of read/write locks
public class Cache {
static Map<String, Object> map = new HashMap<>();
static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
static Lock r = rwl.readLock();
static Lock w = rwl.writeLock();
/** * get the value of a key */
public static Object get(String key) {
r.lock();
try {
return map.get(key);
} finally{ r.unlock(); }}/** * sets the value of the key and returns the old value */
public static Object put(String key, Object value) {
w.lock();
try {
return map.put(key, value);
} finally{ w.unlock(); }}/** * clear all contents */
public static void clear(a) {
w.lock();
try {
map.clear();
} finally{ w.unlock(); }}}Copy the code
2. Design the read and write state
Read/write locks also rely on custom synchronizers for functionality, and the read/write state is its synchronizer state. The custom synchronizer for the read-write lock needs to maintain the state of multiple readers and one writer thread on the synchronization state (an integer variable). For this purpose, the read-write lock splits the variable into two parts, with 16 bits higher for read and 16 bits lower for write
The figure above shows that a thread has acquired a write lock, re-entered it twice, and also acquired a read lock twice in a row. The read and write states can be quickly determined by bit operation. Assuming that the current synchronization state value is S, then:
- Write state equal to S & 0x0000FFFF (erase all high 16 bits)
- Read state equals S >>> 16 (unsigned 16 bits to the right)
- When the write state increases by 1, it equals S plus 1
- When the read state increases by 1, it is equal to S + (1<<6), that is, S + 0x00010000
According to the division of states, a conclusion can be drawn: when S is not equal to 0, when the write state (S & 0x0000FFFF) is equal to 0, then the read state (S >>> 16) is greater than 0, that is, the read lock has been obtained
3. Obtain and release write locks
A write lock is an exclusive lock that supports reentry. Increases the write state if the current thread has already acquired the write lock. If the current thread has already acquired the write lock when it acquires the write lock, or if the thread is not the thread that acquires the write lock, the current thread enters the wait state and obtains the write lock as follows:
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
// The exclusiveCount method uses c & 0x0000FFFF to get the number of write states
int w = exclusiveCount(c);
if(c ! =0) {
// If c is not equal to 0 and w is equal to 0, then there is a read lock
// The current thread is not the thread that acquired the write lock
if (w == 0|| current ! = getExclusiveOwnerThread())return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
setState(c + acquires);
return true;
}
if(writerShouldBlock() || ! compareAndSetState(c, c + acquires))return false;
setExclusiveOwnerThread(current);
return true;
}
Copy the code
Each release of the write lock reduces the write status. When the write status is 0, the write lock is released. In this way, the waiting read-write thread can continue to access the lock, and the changes made by the previous write thread are visible to the subsequent read-write thread
4. Obtain and release the 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 writer thread accesses it, the read lock can always be acquired successfully.
protected final int tryAcquireShared(int unused) {
for(;;) {
int c = getState();
int nextc = c + (1<<16);
if(nextc < c) {
throw new Error("Maximum lock count exceeded");
}
// If another thread has acquired the write lock, the read acquisition fails
if(exclusiveCount(c) ! =0&& owner ! = Thread.currentThread()) {return -1;
}
if(compareAndSetState(c, nextc)) {
return 1; }}}Copy the code
Each release of the read lock reduces the read state by 1<<16
5. Lock the drop
Lock degradation refers to the degradation of a write lock to a read lock. If the current thread owns the write lock, then releases it, and finally acquires the read lock, this piecewise completion process is not called lock degradation. Lock degradation is the process of holding a write lock, acquiring a read lock, and then releasing the write lock
public void processData(a) {
readLock.lock();
if(! update) {// The read lock must be released first
readLock.unlock();
// Lock degradation starts from write lock acquisition
writeLock.lock();
try {
if(! update) {// Data preparation process (omitted)
update = true;
}
readLock.lock();
} finally{ writeLock.unlock(); }}try {
// The process of using data
} finally{ readLock.unlock(); }}Copy the code
In the above example, when the data changes, update (volatile) is set to false, and all threads accessing the processData method are aware of the change, but only one thread can acquire the write lock, and the remaining threads are blocked on the lock method that wrote the lock. After the current thread obtains the write lock and completes the data preparation, it acquires the read lock again and releases the write lock to complete the lock degradation