java read / write lock ReentrantReadWriteLock

summary

When we introduced AQS, they were basically exclusive locks (mutual exclusion locks). These locks only allowed one thread to access at the same time, while read-write locks allowed multiple threads to access at the same time.

When the read operation is much higher than the write operation, the read-write lock is used to make the read-read concurrent and improve the performance. Write operations must be mutually exclusive, because dirty reading of data should be prevented. Similar to select in database from ... lock in share modeļ¼Œ

Read / write locks are implemented based on shared locks, because multiple read threads can obtain locks at the same time, and there are multiple lock owners.

ReentrantReadWriteLock

The classes are roughly as follows.

public class ReentrantReadWriteLock implements ReadWriteLock {
    private final ReentrantReadWriteLock.ReadLock readerLock;
    private final ReentrantReadWriteLock.WriteLock writerLock;
    final Sync sync;
    //Write lock
    public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
    //Read lock
    public ReentrantReadWriteLock.ReadLock  readLock()  { return readerLock; }
}

First, let's create a class that can read and write:

@Slf4j
class DataContainer {
    private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    //Get write lock object
    private ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
    //Get read lock object
    private ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();

    private String s = "date";

    //Read data, as long as it does not involve writing data, as long as the read lock is used
    public String getData() throws InterruptedException {
        String data = null;
        readLock.lock();
        try {
            data = s;
            log.debug("Read data...");
            Thread.sleep(2000);
        } finally {
            readLock.unlock();
        }
        return data;
    }

    public void setData(String data) throws InterruptedException {
        writeLock.lock();
        try {
            s = data;
            log.debug("Write data...");
            Thread.sleep(200);
        } finally {
            writeLock.unlock();
        }
    }
}

The same lock is not used for reading and writing.  

Test class:

public static void main(String[] args) throws InterruptedException {
    DataContainer dc = new DataContainer();
    new Thread(() -> {
        log.debug("{}", dc.getData());
    }, "t1").start();
    new Thread(() -> {
        log.debug("{}", dc.getData());
    }, "t2").start();
    //10:35:01.435 [t2] read data
    //10:35:01.435 [t1] read data
    //10:35:03.445 [t2]   date
    //10:35:03.445 [t1]   date
}

You can see that the read operations of the two rereads are locked at the same time. The write read or write operations can only be mutually exclusive and get the lock.

Features:

  • Support fair lock and non fair lock
  • Support reentry
  • Support lock demotion (that is, obtain the write lock, obtain the read lock, and then release the write lock. The current thread will be demoted to the read lock, and the read lock can be obtained when the write lock is obtained)
  • Lock upgrading is not supported (obtaining a write lock while holding a read lock will cause permanent waiting for obtaining a write lock)

Cache application

Let's do a single thread cache first. When entering the play, the specific dao layer is ordinary search and modification, which is directly skipped

@Slf4j
//Decorator mode, add a cache function to dao layer
class StudentDaoCache extends StudentDao {
    private StudentDao dao = new StudentDao();
    //Cache container
    private Map<QueryObject, Student> cache = new HashMap<>();

    @Override
    public Student getStudent(String sno) throws SQLException {
        QueryObject queryObject = new QueryObject("getStudent", sno);
        //1. Find the cache first
        Student student = cache.get(queryObject);
        if (student != null) {
            log.debug("Get from cache{}data", sno);
            return student;
        }
        //2. If there is no cache, find it from the database
        student = dao.getStudent(sno);
        //Add the found data to the cache
        cache.put(queryObject, student);
        return student;
    }

    @Override
    public boolean updateStudent(Student student) throws SQLException {
        //You need to empty the cache first
        cache.clear();
        return dao.updateStudent(student);
    }

    //Take the whole sql statement and parameters of the query as the key
    class QueryObject {
        private String String;
        private Object args;

        public QueryObject(java.lang.String string, Object args) {
            String = string;
            this.args = args;
        }

        //Rewrite hashcode and equals
        @Override
        public boolean equals(Object o) {}
        @Override
        public int hashCode() {}
    }
}

Test class:

public class AQSTest {
    public static void main(String[] args) throws InterruptedException, SQLException {
        StudentDao dao = new StudentDaoCache();
        Student student = dao.getStudent("2019139001");
        Student student1 = dao.getStudent("2019139004");
        Student student2 = dao.getStudent("2019139001");
        Student student3 = dao.getStudent("2019139001");
        student.name = "Zhang San";
        dao.updateStudent(student);
        Student student4 = dao.getStudent("2019139001");
        //12:40:04.661 [main] search the database --- find the result Student{sno='2019139001', name =' Wang Wu ', age=20}
        //12:40:04.680 [main] search the database --- find the result Student{sno='2019139004', name =' ouyangchong ', age=19}
        //12:40:04.681 [main] get 2019139001 data from cache
        //12:40:04.681 [main] get 2019139001 data from cache
        //12:40:04.967 [main] update identity information of 2019139001
        //12:40:04.982 [main] search the database --- find the result Student{sno='2019139001', name =' Zhang San ', age=20}
    }
}

It can be seen that the cache can work correctly in a single thread, but if multiple users operate the database and maintain the cache at the same time, the following problems will occur:

  • At the beginning of querying the same data, multiple threads may detect that there is no data in the cache at the same time, and then these threads skip the cache to check the database. This problem is relatively small
  • If the thread updating the data empties the cache and is preparing to go to the database to update the data, but it is preempted by a query thread. When the query thread sees that there is no value in the cache, it goes to the database to find the value and put it into the cache, and then the update starts. The data read by the thread next time is dirty data
  • If the code updates the database first and then empties the cache, the thread that modifies the data updates the database first and has not emptied the cache, but is preempted by the reading thread. After reading the dirty data in the cache, the cache is emptied. This is also a problem, but it is smaller than the previous problem

We need locks to solve the problem:

@Slf4j
class StudentDaoCache extends StudentDao {
    private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
    private ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
    private StudentDao dao = new StudentDao();
    //Cache container
    private Map<QueryObject, Student> cache = new HashMap<>();

    @Override
    public Student getStudent(String sno) throws SQLException {
        QueryObject queryObject = new QueryObject("getStudent", sno);
        Student student = null;
        //Read lock
        readLock.lock();
        try {
            //1. Find the cache first
            student = cache.get(queryObject);
            if (student != null) {
                log.debug("Get from cache{}data", sno);
                return student;
            }
        } finally {
            readLock.unlock();//Release the read lock because lock upgrade is not supported
        }
        //Add write lock because write operation is involved
        writeLock.lock();
        try {
            //Double check to check whether the cache already exists. On the contrary, multiple read threads
            student = cache.get(queryObject);
            if (student != null) {
                log.debug("Get from cache{}data", sno);
                return student;
            }
            //Then find it from the database
            student = dao.getStudent(sno);
            //Add the found data to the cache
            cache.put(queryObject, student);
            return student;
        }finally {
            writeLock.unlock();
        }
    }
    @Override
    public boolean updateStudent(Student student) throws SQLException {
        writeLock.lock();
        try {
            //Emptying the cache and updating the library become integral, atomic
            cache.clear();
            return dao.updateStudent(student);
        } finally {
            writeLock.unlock();
        }
    }
}

After adding the read-write lock, those problems will be solved accordingly. The above cache is relatively low. Don't care too much.

StampedLock

JDK8 is added to further optimize the read performance. Its feature is that when using the read lock, the write lock must be used with the [stamp]. For example:

Lock:

long stamp= stampedLock.readLock();
stampedLock.unlockRead(stamp);

Add / remove write lock:

long stamp = stampedLock.writeLock();
stampedLock.unlock(stamp);

Optimistic reading. StampedLock supports the tryOptimisticRead() method (happy reading). After reading, a stamp verification needs to be done. If the verification passes, it indicates that there is no write operation during this period, and the data can be used safely. If the verification fails, the read lock needs to be obtained again to ensure data security.

public long tryOptimisticRead() {
    long s;
    return (((s = state) & WBIT) == 0L) ? (s & SBITS) : 0L;
}
//Check stamp
public boolean validate(long stamp) {
    U.loadFence();
    return (stamp & SBITS) == (state & SBITS);
}

Usage:

long stamp = stampedLock.tryOptimisticRead();
//Check stamp
if (stampedLock.validate(stamp)) {
    //Lock upgrade
}

The lock does not support reentry

Keywords: Java Back-end JUC

Added by chowwiie on Wed, 26 Jan 2022 10:40:24 +0200