ReadWriteLock read-write lock

1. Optimistic and pessimistic locks

Reference: developer.51cto.com/article/654…

Pessimistic locks (the implementation classes of the synchronized keyword and Lock are pessimistic locks)

  • What is pessimism lock? Think of themselves as there must be other threads when using data to modify the data, so when get the data will be locked, to ensure that data is not modified by other threads, so people want to get data was blocking, until a pessimistic lock is released, the pessimistic locks in the Shared resources to only one thread at a time use, other threads blocked, after use to transfer resources to other threads
  • In terms of efficiency, however, the mechanism for handling locking incurs additional overhead and increases the chance of deadlocks. It also reduces parallelism, because if A thread A has been locked, other threads must wait for that thread A to finish processing
  • Pessimistic lock applies to scenarios where many write operations are performed. The pessimistic lock ensures that data is correct during write operations (such as adding, deleting, or modifying data) and is explicitly locked before resource synchronization
  • synchronizedKey words andLockImplementation classes are pessimistic locks,In the database, row lock, table lock, read lock (shared lock), write lock (exclusive lock), are pessimistic lock, table lock will occur deadlock, read lock and write lock will occur deadlock phenomenon.
  • Pessimistic locks do not support concurrency

Optimistic locking

  • Concept: Optimistic locks do not add locks because they assume that data will not be modified by other threads while they are using it. Instead, they update data to determine whether the data was previously updated by other threads. If the data is not updated, the current thread writes its modified data successfully. If the data has already been updated by another thread, different operations are performed depending on the implementation
  • Optimistic locking is realized by using lock-free programming in Java, and the CAS algorithm is most commonly used. Incremental operations in Java atomic classes are realized by CAS spin, which is suitable for scenarios with many read operations. Without locking, the performance of read operations can be greatly improved

Optimistic locking is generally implemented in two ways

  • The version number mechanism is adopted
  • CAS algorithm implementation
// How pessimistic locks are called
public synchronized void m1(a){
    // Lock the business logic
}

// Ensure that multiple threads are using the same lock object
ReetrantLock lock=new ReentrantLock();
public void m2(a){
    lock.lock();
    try{
        // Synchronize resources
    }finally{ lock.unlock(); }}// How optimistic locks are invoked
// Ensure that multiple threads use the same AtomicInteger
private  AtomicInteger atomicIntege=new AtomicInteger();
atomicIntege.incrementAndGet();
Copy the code

2. ReadWriteLock

2.1 Overview of read/write locks

We should be able to have a situation in development where there are read and write operations on shared resources, and writes are not as frequent as reads. There is no problem with multiple threads reading a resource simultaneously when there is no write operation, so multiple threads should be allowed to read a shared resource simultaneously. But when a writer thread writes to these shared resources, other threads are not allowed to access them.

Read/write lock is actually a special kind of spin lock. It divides the visitors to the shared resource into readers and writers. Readers only read the shared resource, while writers write the shared resource. Locks related to read operations are called read locks because they can be shared. We also call them “shared locks”. Locks related to write operations are called write locks, exclusive locks, and exclusive locks. Multiple threads of readers can read at a time, but only one writer thread can write at a time, that is, the write operation is exclusive.

Read/write locks are suitable for situations where data structures are read much more often than written. A read/write lock is also called a shared-exclusive lock because it can be shared in read mode and exclusive in write mode.

2.2 ReadWriteLock Read/write lock

In view of the above this kind of scenario, Java and provided under the contract, speaking, reading and writing lock ReadWriteLock (interface) | ReentrantReadWriteLock (implementation class).

ReadWriteLock maintains a pair of related locks, one for read-only operations and one for write operations. As long as there is no writer, the read lock can be held by multiple reader threads simultaneously. The write lock is exclusive.

public interface ReadWriteLock {
	/ / read lock
    Lock readLock(a);
	/ / write locks
    Lock writeLock(a);
}
Copy the code

A ReadWriteLock read normally does not change the shared resource, but a write must be exclusive to obtain the lock.

  • Data structures for which read operations dominate. ReadWriteLock provides higher concurrency than exclusive locks.
  • In the case of read-only data structures, the immutability contained in them can be completely removed from consideration for locking operations.
  • Read/write locks need to be closed separately after useLockAnd then you have to turn it off manually and it’s the same thing.
  • ReadWriteLock is better thanlockLocks for more fine-grained control

2.3 ReentrantReadWriteLock implementation class

ReentrantReadWriteLock implements the ReadWriteLock interface

public class ReentrantReadWriteLock implements ReadWriteLock.java.io.Serializable {
    / * * * / read lock
    private final ReentrantReadWriteLock.ReadLock readerLock;
    / * * * / write lock
    private final ReentrantReadWriteLock.WriteLock writerLock;
    final Sync sync;

    /** Create a new ReentrantReadWriteLock */ using the default (unfair) sort attribute
    public ReentrantReadWriteLock(a) {
        this(false);
    }
    /** Create a new ReentrantReadWriteLock with the given fairness policy */
    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }
    /** returns the lock */ used for the write operation
    public ReentrantReadWriteLock.WriteLock writeLock(a) { return
        writerLock; }

    /** returns the lock */ used for the read operation
    public ReentrantReadWriteLock.ReadLock readLock(a) { return
        readerLock; }
    abstract static class Sync extends AbstractQueuedSynchronizer {}
    static final class NonfairSync extends Sync {}
    static final class FairSync extends Sync {}
    public static class ReadLock implements Lock.java.io.Serializable {}
    public static class WriteLock implements Lock.java.io.Serializable {}}Copy the code

2.4. Pay attention to reading and writing locks

When a write/write lock is in the write/lock state, all threads attempting to lock the lock are blocked until the lock is unlocked

When a read-write lock is in read-lock state, all threads attempting to lock it in read mode gain access, but if a thread wishes to lock it in write mode, it must wait until all threads release the lock

A thread that wants to enter a read lock must:

  • There are no write locks for other threads
  • There is no write request, or there is a write request, but the calling thread is the same as the thread holding the lock (reentrant lock)

A thread can enter a write lock if:

  • No reader thread is accessing
  • No other writer thread is accessing

Generally, when the read-write lock is in the read-mode lock state, if another thread tries to lock in write mode, the read-write lock will block the subsequent read-mode lock request. In this way, the read-mode lock will not be occupied for a long time, while the waiting write mode lock request will be blocked for a long time

2.5, characteristics

Fair and selective:

  • Unfair mode (default)
    • When unfairly initialized, the order in which read and write locks are acquired is uncertain. Unfair locks contend for acquisition and may delay one or more read or write threads, but have higher throughput than fair locks.
  • Fair mode
    • When initialized in fair mode, threads will acquire locks in queue order. When the current thread releases the lock, the thread that waits the longest is assigned the write lock. Or if there is a group of reader thread groups that wait longer than the writer thread, the read lock will be assigned to that group.
    • When a writer thread holds a write lock or has a waiting writer thread, a thread trying to acquire a fair (non-reentrant) read lock will block. The thread does not acquire the read lock until the write lock that has waited the longest acquires it and releases it.

reentrant

  • Both read and write locks support thread re-entry. But a write lock can acquire a read lock, and a read lock cannot acquire a write lock. Because read locks are shared, write locks are exclusive.

Lock down

  • A write lock can be degraded to a read lock by following the sequence of acquiring a write lock, acquiring a read lock, and releasing a write lock.

Access to interrupt locks is supported

  • Interrupts are supported during read and write lock acquisition

monitoring

  • Provide some helper methods, such ashasQueuedThreadsMethod to query whether a thread is waiting to acquire a read or write lock,isWriteLockedMethod to query whether a write lock is held by any thread, and so on

2.6 case demonstration

Scenario: We use a small case of caching, without the use of locks, store and read functionality, and through the concurrency of multiple threads. : Uses ReentrantReadWriteLock to read and write a HashMap collection concurrently

Volatile keyword: Data is constantly changing, multiple threads are visible, and instruction reordering is prohibited

There is no lock

/ / resource class
class  ReentrantReadWriteLockDemo{

    // Create a map collection
    private volatile Map<String, Object> map = new HashMap<>();

    / / data
    public void put(String key, Object value) {
        System.out.println(Thread.currentThread().getName() + "Writing data" + key);
        / / data
        map.put(key, value);
        System.out.println(Thread.currentThread().getName() + "Finished" + key);
    }

    / / get data
    public Object get(String key) {
        Object result = null;
        System.out.println(Thread.currentThread().getName() + "Fetching data" + key);
        result = map.get(key);
        System.out.println(Thread.currentThread().getName() + "I'm done with the data." + key);
        return result;
    }

    public static void main(String[] args) {

        ReentrantReadWriteLockDemo demo = new ReentrantReadWriteLockDemo();

        for (int i = 1; i <= 5; i++) {
            final int number = i;
            new Thread(() -> {//5 threads hold data
                demo.put(String.valueOf(number), number);
            }, String.valueOf(i)).start();
        }

        for (int i = 1; i <= 5; i++) {
            final int number = i;
            new Thread(() -> {//5 threads fetch datademo.get(String.valueOf(number)); }, String.valueOf(i)).start(); }}}Copy the code

The results of

1Writing data1
3Writing data3
2Writing data2
3Finished writing3
1Finished writing1
5Writing data5
4Writing data4
2Finished writing2
4Finished writing4
5Finished writing5
1Fetching data1
2Fetching data2
1We're done with the data1
2We're done with the data2
3Fetching data3
4Fetching data4
4We're done with the data4
3We're done with the data3
5Fetching data5
5We're done with the data5
Copy the code

You can see that while one writer thread is writing data, another thread is entering, which is obviously not acceptable.

Use ReadWriteLock read/write locks to resolve cache concurrency issues

/ / resource class
class  ReentrantReadWriteLockDemo{
    
    // Create a map collection
    private volatile Map<String, Object> map = new HashMap<>();

    // Create a read/write lock object
    private ReadWriteLock rwLock = new ReentrantReadWriteLock();

    / / data
    public void put(String key, Object value) {
        // Add write lock
        rwLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "Writing data" + key);
            // Pause for a moment
            TimeUnit.MICROSECONDS.sleep(300);
            / / data
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "Finished" + key);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // Release the write lockrwLock.writeLock().unlock(); }}/ / get data
    public Object get(String key) {
        // Add read lock
        rwLock.readLock().lock();
        Object result = null;
        try {
            System.out.println(Thread.currentThread().getName() + "Fetching data" + key);
            // Pause for a moment
            TimeUnit.MICROSECONDS.sleep(300);
            result = map.get(key);
            System.out.println(Thread.currentThread().getName() + "I'm done with the data." + key);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // Release the read lock
            rwLock.readLock().unlock();
        }
        return result;
    }

    public static void main(String[] args) {
        
        ReentrantReadWriteLockDemo demo = new ReentrantReadWriteLockDemo();

        for (int i = 1; i <= 5; i++) {
            final int number = i;
            new Thread(() -> {//5 threads hold data
                demo.put(String.valueOf(number), number);
            }, String.valueOf(i)).start();
        }

        for (int i = 1; i <= 5; i++) {
            final int number = i;
            new Thread(() -> {//5 threads fetch datademo.get(String.valueOf(number)); }, String.valueOf(i)).start(); }}}Copy the code

The results of

1Writing data1
1Finished writing1
2Writing data2
2Finished writing2
3Writing data3
3Finished writing3
4Writing data4
4Finished writing4
5Writing data5
5Finished writing5
1Fetching data1
2Fetching data2
4Fetching data4
3Fetching data3
5Fetching data5
2We're done with the data2
1We're done with the data1
5We're done with the data5
3We're done with the data3
4We're done with the data4
Copy the code

As can be seen from the results, the write operation is unique, multiple threads cannot write at the same time, must wait for one thread to finish writing another thread can enter, while the read is shared, multiple threads can read data together.

2.7 Summary (Key points)

Different from the traditional lock, the read/write lock can share the read, but only one write. That is, the read/write thread cannot exist at the same time. To sum up, read/write is not mutually exclusive, read/write is mutually exclusive, and write is mutually exclusive. The traditional exclusive lock is read exclusive, read exclusive, write exclusive, and read exclusive. The read and write lock is a mechanism created for this optimization. Note that read is far more efficient than write. In general, the inefficiency of an exclusive lock is due to the thread context switch caused by fierce competition for critical sections under high concurrency. Therefore, when concurrency is not very high, read-write locks may be less efficient than exclusive locks because of the additional maintenance of read-lock state. Therefore, use it as required.

What are the differences between ReentrantReadWriteLock and Synchonized and ReentrantLock? Or what are the advantages?

  • Synchonized and ReentrantLock are traditional exclusive locks. Read and write operations can only be accessed by one person at a time, resulting in low efficiency.
  • The ReentrantReadWriteLock read operation can be shared to improve performance. Multiple read operations can be performed simultaneously, while the write operation can be accessed by one person at a time.
  • ReentrantReadWriteLock does have the advantage of being lock hungry, but it also has some disadvantage. If a reader gets the lock first, and there are many readers, but only one writer thread, it is very likely that the writer thread will not get the lock until all the readers have finished reading. It may create a phenomenon of reading and not writing.

3. Lock degradation

3.1 overview,

Concept:

Lock degradation means that a write lock is degraded to a read lock. Read locks cannot be upgraded to write locks. 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 (currently owned) write locks, acquiring read locks, releasing (previously owned) write locks, and finally releasing read locks.

Programming model:

Obtain write lock – > Obtain read lock – > Release write lock – > Release read lock

Code demo

public class ReadWriteLockDemo2 {

    public static void main(String[] args) {
        ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
        // Get read lock
        ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
        // Get the write lock
        ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
        
        // get write lock
        writeLock.lock();
        System.out.println("Got a write lock");
        
        // get the read lock
        readLock.lock();
        System.out.println("Continue to get read lock");
        // release write lock
        writeLock.unlock();
	   // release the read lockreadLock.unlock(); }}Copy the code

Results:

The write lock was acquired and the read lock was acquiredCopy the code

You may not see much of it, but if you switch the line of code that gets the read lock to the line of code that gets the write lock, the result may be completely different.

public class ReadWriteLockDemo2 {

    public static void main(String[] args) {
        ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
        // Get read lock
        ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
        // Get the write lock
        ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();

        // get the read lock
        readLock.lock();
        System.out.println("Got a read lock");

        writeLock.lock();
        System.out.println("Continue to get write lock"); writeLock.unlock(); readLock.unlock(); }}Copy the code

Result: The read lock is stopped, that is, the read lock cannot be upgraded to a write lock.

A read lock was obtainedCopy the code

The reason:

This is because a thread cannot acquire a write lock if it holds a read lock (the prerequisite for acquiring a write lock is that there is no reader thread and no other writer thread. If the current read lock is found to be occupied, it will immediately fail to acquire it, regardless of whether the read lock is held by the current thread or not).

However, if the thread holds the write lock, the thread can continue to acquire the read lock (if the write lock is found to be occupied, the acquisition will fail only if the write lock is not occupied by the current thread).

When a thread acquires a read lock, other threads may also hold the read lock. Therefore, the thread that acquires the read lock cannot be “upgraded” to a write lock. The thread that acquired the write lock must have exclusive access to the read lock, so it can continue to acquire the read lock. When it has acquired both the write lock and the read lock, it can also release the write lock and continue to hold the read lock, so that a write lock “degrades” to the read lock.

3.2 Application Scenarios

It is sensitive to data. After data modification, obtain the modified value and perform other operations

Let’s look at a practical example:

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class CacheDemo {
    /** * cache, this is assumed to store 1000 cache objects, according to the default load factor of 0.75, then the capacity =750, approximately estimated the length of each node list is 5 *, then the array length is approximately: 150, and there is also a set of map size is generally 2 index, then the latest number is: 128 */
    private Map<String, Object> map = new HashMap<>(128);
    private ReadWriteLock rwl = new ReentrantReadWriteLock();
    private Lock writeLock=rwl.writeLock();
    private Lock readLock=rwl.readLock();

    public Object get(String id) {
        Object value = null;
        readLock.lock();// Enable read lock first, fetch from cache
        try {
            // If the cache does not release the read lock, write the lock
            if (map.get(id) == null) { 
                readLock.unlock();
                writeLock.lock();
                try {
                    // Prevent multiple writers from querying assignments repeatedly
                    if (value == null) {
                        // Select * from the database
                        value = "redis-value";  
                    }
                    // Add lock degrade write lock, do not understand the above can see the principle of lock degrade and keep read data atomicity explanation
                    readLock.lock(); 
                } finally {
                    // Release the write lockwriteLock.unlock(); }}}finally {
            // Finally release the read lock
            readLock.unlock(); 
        }
        returnvalue; }}Copy the code

If the lock degradation function is not used, for example, the write lock is released and the read lock is obtained. During the process of obtaining the read lock, other threads may compete for the write lock or update data, and the data obtained is the data updated by other threads. This may cause data pollution, that is, dirty read problems.

3.3 the necessity of lock degradation

Is read lock acquisition necessary in lock degradation?

Answers are necessary. In order to ensure the visibility of data, if the current thread does not acquire the read lock but directly releases the write lock, assuming that another thread (called thread T) acquires the write lock and changes the data, the current thread will not be aware of the data update of thread T. If the current thread acquires a read lock, that is, following the steps of lock degradation, thread T will be blocked until the current thread uses the data and releases the read lock. Thread T can acquire the write lock to update the data.