Real multithreaded business development , The most commonly used logic is data reading and writing ,ReentrantLock Although it has a completely exclusive effect ( That is, only one thread is executing at the same time lock The next task ),
This ensures thread safety of instance variables , But it's very inefficient . So in JDK A read-write lock is provided in ReentrantReadWriteLock class , Use it to speed up operation efficiency .
Read write locks represent two locks , One is read operation related lock , be called Shared lock ; The other is the lock related to write operation , be called Exclusive lock .
Let's verify the mutex between read-write locks through code
ReentrantReadWriteLock
Read read share
First create an object , Define a read lock method and a write lock method respectively ,
public class MyDomain3 { private ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); public void testReadLock() { try { lock.readLock().lock(); System.out.println(System.currentTimeMillis() + " Get read lock "); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.readLock().unlock(); } } public void testWriteLock() { try { lock.writeLock().lock(); System.out.println(System.currentTimeMillis() + " Get write lock "); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.writeLock().unlock(); } } }
Create thread class 1 Call the read lock method
public class Mythread3_1 extends Thread { private MyDomain3 myDomain3; public Mythread3_1(MyDomain3 myDomain3) { this.myDomain3 = myDomain3; } @Override public void run() { myDomain3.testReadLock(); } }
@Test public void test3() throws InterruptedException { MyDomain3 myDomain3 = new MyDomain3(); Mythread3_1 readLock = new Mythread3_1(myDomain3); Mythread3_1 readLock2 = new Mythread3_1(myDomain3); readLock.start(); readLock2.start(); Thread.sleep(3000); }
Execution results :
1639621812838 Get read lock 1639621812839 Get read lock
It can be seen that the two read locks are executed almost simultaneously , Explain that reading and reading are shared , Because the read operation will not have thread safety problems .
Writing mutually exclusive
Create thread class 2, Call the write lock method
public class Mythread3_2 extends Thread { private MyDomain3 myDomain3; public Mythread3_2(MyDomain3 myDomain3) { this.myDomain3 = myDomain3; } @Override public void run() { myDomain3.testWriteLock(); } }
@Test public void test3() throws InterruptedException { MyDomain3 myDomain3 = new MyDomain3(); Mythread3_2 writeLock = new Mythread3_2(myDomain3); Mythread3_2 writeLock2 = new Mythread3_2(myDomain3); writeLock.start(); writeLock2.start(); Thread.sleep(3000); }
Execution results :
1639622063226 Get write lock 1639622064226 Get write lock
In terms of time , The interval is 1000ms namely 1s, Write lock and write lock are mutually exclusive .
Reading and writing are mutually exclusive
Reuse thread 1 And thread 2 Call read lock and write lock respectively
@Test public void test3() throws InterruptedException { MyDomain3 myDomain3 = new MyDomain3(); Mythread3_1 readLock = new Mythread3_1(myDomain3); Mythread3_2 writeLock = new Mythread3_2(myDomain3); readLock.start(); writeLock.start(); Thread.sleep(3000); }
Execution results :
1639622338402 Get read lock 1639622339402 Get write lock
In terms of time , The interval is 1000ms namely 1s, It is consistent with the code , It is proved that reading and writing are mutually exclusive .
Pay attention to the ," Reading and writing are mutually exclusive " and " Write and read are mutually exclusive " It's two different scenarios , But the way of proof is consistent with the conclusion , So it doesn't prove .
Under the final test results :
1、 Read and read are not mutually exclusive , Because the read operation will not have thread safety problems
2、 Write and write are mutually exclusive , Avoid one write operation affecting another , Raises thread safety issues
3、 Read and write are mutually exclusive , Avoid that the write operation modifies the content during the read operation , Raises thread safety issues
In summary , Multiple Thread Can read at the same time , But only one is allowed at the same time Thread Write operation .
Source code analysis
In the read-write lock Sync It is also realized AQS, To recall ReentrantLock Implementation of custom synchronizer in , The synchronization state indicates the number of times the lock is repeatedly acquired by a thread ,
The custom synchronizer of the read-write lock needs to be in the synchronization state ( An integer variable ) Maintain the status of multiple read threads and one write thread , The design of this state is the key to the implementation of read-write lock .
The read-write lock divides the variable into two parts , high 16 Bit means read , low 16 Bit means write
The current synchronization state indicates that a thread has acquired a write lock , And re entered twice , At the same time, it also obtains two consecutive read locks . How does the read-write lock quickly determine the state of each reading and writing ?
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; /** Returns the number of shared holds represented in count */ static int sharedCount(int c) { return c >>> SHARED_SHIFT; } /** Returns the number of exclusive holds represented in count */ static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
In fact, it is through bit operation . Assume the current synchronization state value is c, Write state is equal to c & EXCLUSIVE_MASK (c&0x0000FFFF( Will be high 16 Erase all the bits )),
The state of reading is equal to c>>>16( Make up without symbols 0 Move right 16 position ). When the write state increases 1 when , be equal to c+1, When the reading state increases 1 when , be equal to c+(1<<16), That is to say c+0x00010000.
A corollary can be drawn from the division of states :c It's not equal to 0 when , When writing state (c & 0x0000FFFF) be equal to 0 when , Then read the state (c>>>16) Greater than 0, The read lock has been acquired .
Acquisition and release of write lock
Pass the test above , We know that a write lock is an exclusive lock that supports reentry , Look at how the source code realizes the acquisition of 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; setExclusiveOwnerThread(current); return true; }
The first 3 Go to the first place 11 That's ok , Briefly describe the implementation logic of the whole method , Here's a compliment , This comment makes it easy for people to know the function of the code . Let's analyze ,
The first 13 To the first 15 That's ok , Get the current thread object respectively current,lock Lock state value of c And the value of the write lock w,c!=0 indicate Currently in a locked state ,
Continue to analyze the second 16 Row to 25 That's ok , There's a key Note:(Note: if c != 0 and w == 0 then shared count != 0): In short, it is : If a has a lock state but no write lock , Then it must have a read lock .
The first 18 That's ok if Conditions , Is to judge that the read lock is added , But the current thread is not the thread owned by the lock , Then failed to acquire the lock , Prove that read-write locks are mutually exclusive .
The first 20 Go to the first place 25 That's ok , Take this step , explain w !=0 , The write lock has been acquired , As long as the maximum write lock is not exceeded , Then the write lock can be successfully obtained by adding the write state .
If the code goes to the 26 That's ok , explain c==0, There are currently no locks , Execute first writerShouldBlock() Method , This method is used to determine whether the write lock should be blocked ,
This is a lock on fairness and unfairness. There will be different logic , For unfair locks , Go straight back to false, There is no need to block ,
The following is the judgment of fair lock execution
public final boolean hasQueuedPredecessors() { // The correctness of this depends on head being initialized // before tail and on head.next being accurate if the current // thread is first in queue. Node t = tail; // Read fields in reverse initialization order Node h = head; Node s; return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); }
For the fair lock, you need to judge whether there is..., in the current waiting queue The thread equal to the current thread and queued to acquire the lock .
The release of write lock and ReentrantLock The release process is basically similar to , Each release reduces the write state , When the write status is 0 Indicates that the write lock has been released ,
So the waiting read-write thread can continue to access the read-write lock , At the same time, the modification of the previous write thread is visible to the subsequent read-write thread .
Acquisition and release of read lock
Read lock is a shared lock that supports reentry , It can be acquired by multiple threads at the same time .JDK Source code is as follows :
protected final int tryAcquireShared(int unused) { 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 { 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); }
The first 4 Go to the first place 6 That's ok , If the write lock is held by another thread , Then return directly false, Failed to get read lock , Prove that writing and reading are mutually exclusive between different threads .
The first 8 That's ok ,readerShouldBlock() Whether the acquisition of read lock should be blocked , We should also distinguish between fair locks and unfair locks ,
In the fair lock mode, it is necessary to judge whether there is... In the current waiting queue The thread equal to the current thread and queued to acquire the lock , If it exists, you need to wait to obtain the read lock .
In the unfair lock mode, it is necessary to judge that the first one in the current waiting queue is waiting for write lock , Then the method returns true, Obtaining a read lock requires waiting .
fullTryAcquireShared() It mainly deals with the complete version of read lock acquisition , It deals with tryAcquireShared() Not handled in CAS Error and reentrant read lock processing logic .
reference
1:《Java The art of concurrent programming 》
2:《Java Multithread programming core technology 》