Java geek

Related reading:

Java Concurrent programming (1) Knowledge map Java concurrent programming (2) Atomic Java concurrent programming (3) Visibility Java concurrent programming (4) Sequential Java concurrent programming (5) Introduction to Creating threads Java concurrent programming (6) Synchronized usage Java Concurrency programming Introduction (7) Easy to understand wait and Notify and use scenarios Java concurrency programming Introduction (8) Thread lifecycle Java concurrency programming Introduction (9) Deadlock and deadlock bit Java concurrency programming Introduction (10) lock optimization Introduction to Concurrent Programming in Java (11) Flow limiting scenarios and Spring Flow Limiting scenarios Introduction to Concurrent programming in Java (12) Producer and Consumer Patterns – Introduction to Concurrent programming in Java with code templates (13) Read/write lock and cache templates (14) CountDownLatch application scenarios Introduction to Concurrent Programming (CyclicBarrier) Introduction to Concurrent programming (CyclicBarrier) Introduction to Concurrent programming in Java (CyclicBarrier) Introduction to Concurrent programming in Java (CyclicBarrier) Introduction to Concurrent programming in Java (CyclicBarrier) Introduction to Concurrent programming in Java (CyclicBarrier) Introduction to Concurrent programming in Java (CyclicBarrier) Introduction to Concurrent programming in Java (CyclicBarrier) Introduction to Concurrent programming in Java (CyclicBarrier) Introduction to Concurrent programming in Java (CyclicBarrier Java Concurrent programming introduction (19) Asynchronous task scheduling tool CompleteFeature


1. Common locking scenarios and locking tools

Common locking scenarios and tools are as follows:

scenario Read and read Read and write Writing and writing implementation performance
1 Is not mutually exclusive Is not mutually exclusive The mutex A variety of ways, just write lock can be high
2 Is not mutually exclusive The mutex The mutex ReentrantReadWriteLock or StampedLock In the
3 The mutex The mutex The mutex Multiple ways to lock both read and write low

The above multiple methods refer to multiple methods, as long as the lock can be achieved, for example, synchronized keyword can be used to lock, or ReentrantLock can be used to achieve the lock.

Scenario 1 has only write mutexes, indicating that read and write inconsistencies (usually temporary) are tolerated, as CopyOnWriteArrayList does in Java and packages. In the open source connection pool HikariPool tool used CopyOnWriteArrayList, you can refer to HikariPool source code (4) resource status

Scenario 2 read only but not mutually exclusive, in which case the caller does not consume the read data, but simply uses it. The example reads the data for the id type, and reads the data only to select a certain id type, but does not consume it.

Scenario 3 is mutually exclusive in all cases. In this scenario, the read data is usually consumed and repeated reading by the next consumer is not allowed. For example, A pending task queue is read and processed by multiple worker threads, and after it is read by worker thread A, other worker threads cannot read again.

Scenario 1 is very similar to Scenario 2 in that once a read occurs, there is no control over writing while using the read data, so the effect is the same. When you want to read as much data as possible from the most recent write operation, the toolkit of scenario 2 is more appropriate. (For example, the write operation takes a little longer and the read operation is very frequent)

The difference between ReentrantReadWriteLock and StampedLock is that StampedLock supports optimistic read, which provides higher performance, but at a cost. Otherwise, you only need to keep one.

2. For example

2.1. Already

import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockDemo {
    private static int TOTAL_THREAD_NUM = 3;  // Total number of threads
    private static int WRITE_THREAD_NUM = 1;   // Number of write threads
    private static int READ_THREAD_NUM = TOTAL_THREAD_NUM - WRITE_THREAD_NUM; // Number of read threads

    public static void main(String[] args) {
        ReentrantLockCache reentrantLockCache = new ReentrantLockCache();
        ExecutorService executorService = Executors.newFixedThreadPool(TOTAL_THREAD_NUM);
        AtomicInteger value = new AtomicInteger();
        value.getAndIncrement();

        for (int i = 0; i < WRITE_THREAD_NUM; i++) {
            executorService.submit(() -> {
                while (true) {
                    int v = value.getAndIncrement();
                    reentrantLockCache.put(1001, v);
                    System.out.println(Thread.currentThread().getName() + "- write the value." + v + "" + newDate()); }}); }for (int i = 0; i < READ_THREAD_NUM; i++) {
            executorService.submit(() -> {
                int callTimes = 0;
                while (true) {
                    System.out.println(Thread.currentThread().getName() + "- read the value." + reentrantLockCache.get(1001) + "" + newDate()); }}); }}public static void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch(InterruptedException ie) { Thread.currentThread().interrupt(); }}}class ReentrantLockCache {
    final Map<Integer, Integer> map = new HashMap<>();
    ReentrantLock readLock = new ReentrantLock();

    public void put(Integer key, Integer val) {
        try {
            readLock.lock(); / / write locks
            ReentrantLockDemo.sleep(1000);  // Short sleep to verify that reads and writes are mutually exclusive
            map.put(key, val);
        } finally {
            readLock.unlock();  // Must be unlocked in finally block}}public Integer get(Integer key) {
        try {
            readLock.lock();  / / read lock
            ReentrantLockDemo.sleep(1000);  // Short sleep to verify that reads and writes are mutually exclusive
            return map.get(key);
        } finally {
            readLock.unlock();   // Must be unlocked in finally block}}}Copy the code

Output result:

pool-1-thread-1- write a value:1 Sun Apr 19 23:32:07 GMT+08:00 2020
pool-1-thread-2- read the value:1 Sun Apr 19 23:32:08 GMT+08:00 2020
pool-1-thread-3- read the value:1 Sun Apr 19 23:32:09 GMT+08:00 2020
pool-1-thread-1- write a value:2 Sun Apr 19 23:32:10 GMT+08:00 2020
pool-1-thread-2- read the value:2 Sun Apr 19 23:32:11 GMT+08:00 2020
pool-1-thread-3- read the value:2 Sun Apr 19 23:32:12 GMT+08:00 2020
Copy the code

ReentrantLock is a read/write mutex, read mutex, write mutex. If you don’t want to lock the read, you can cancel the lock in the read operation. Therefore, ReentrantLock can be used in scenario 1 and scenario 3.

2.2. synchronized

class SynchronizedCache {
    final Map<Integer, Integer> map = new HashMap<>();

    // This is for example only. For this scenario, a map that supports concurrency can be used directly
    public synchronized void put(Integer key, Integer val) {
        map.put(key, val);
    }

    public synchronized Integer get(Integer key) {
        returnmap.get(key); }}Copy the code

Here synchronized and ReentrantLock appear to have the same functionality, but synchronized does not have the following capabilities of ReentrantLock:

  1. Able to respond to interrupts. Synchronized holds lock A, and if an attempt to acquire lock B fails, the thread enters A blocked state. Once A deadlock occurs, there is no opportunity to wake up the blocked thread.
  2. Support for obtaining lock timeout. If the thread does not acquire the lock for a period of time, instead of entering a blocking state, it returns an error, then the thread has a chance to release the lock it once held.
  3. Locks can be acquired without blocking. If an attempt to acquire the lock fails and the thread does not block but returns, the thread has a chance to release the lock it once held.

It can be seen that ReentrantLock has the ability and is more powerful than synchronized, but it does not preclude the use of synchronized. In contrast to the above two examples, synchronized code is simpler for the same function in the example scenario.

2.3. ReentrantReadWriteLock

import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockDemo {
    private static int MAX_READ_TIMES = 2000;  // Maximum number of reads
    private static int TOTAL_THREAD_NUM = 10;  // Total number of threads
    private static int WRITE_THREAD_NUM = 1;   // Number of write threads
    private static int READ_THREAD_NUM = TOTAL_THREAD_NUM - WRITE_THREAD_NUM; // Number of read threads
    private static boolean isStop = false;
    private static long startTime = System.currentTimeMillis();

    public static void main(String[] args) {
        ReadWriteLockCache readWriteLockCache = new ReadWriteLockCache();
        ExecutorService executorService = Executors.newFixedThreadPool(TOTAL_THREAD_NUM);
        AtomicInteger value = new AtomicInteger();
        value.getAndIncrement();

        for (int i = 0; i < WRITE_THREAD_NUM; i++) {
            executorService.submit(() -> {
                while (true) {
// sleep(20); // Simulate no write operation for a period of time

                    int v = value.getAndIncrement();
                    readWriteLockCache.put(1001, v);
                    System.out.println(Thread.currentThread().getName() + "- write the value." + v + "" + new Date());

                    if (isStop) {
                        break; }}}); }for (int i = 0; i < READ_THREAD_NUM; i++) {
            executorService.submit(() -> {
                int callTimes = 0;
                while (true) {
                    System.out.println(Thread.currentThread().getName() + "- read the value." + readWriteLockCache.get(1001) + "" + new Date());

                    callTimes++;
                    if (callTimes == MAX_READ_TIMES) {
                        isStop = true;
                        System.out.println("cost time: " + (System.currentTimeMillis() - startTime));
                        readWriteLockCache.printTime();
                        break; }}}); }}public static void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch(InterruptedException ie) { Thread.currentThread().interrupt(); }}}class ReadWriteLockCache {
    final Map<Integer, Integer> map = new HashMap<>();
    private AtomicInteger totalReadTimes = new AtomicInteger();
    private AtomicInteger totalWriteTimes = new AtomicInteger();
    ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    Lock readLock = readWriteLock.readLock();
    Lock writeLock = readWriteLock.writeLock();

    public void put(Integer key, Integer val) {
        try {
            writeLock.lock(); / / write locks
            ReadWriteLockDemo.sleep(5); // Simulate the write time
            totalWriteTimes.getAndIncrement();
            map.put(key, val);
        } finally {
            writeLock.unlock();  // Must be unlocked in finally block}}public Integer get(Integer key) {
        try {
            readLock.lock();  / / read lock
            totalReadTimes.getAndIncrement();
            return map.get(key);
        } finally {
            readLock.unlock();   // Must be unlocked in finally block}}public void printTime(a) {
        System.out.println("Read times: " + totalReadTimes.get() + ", Write times: "+ totalWriteTimes.get()); }}Copy the code

Output result:

pool-1-thread-10- read the value:3233 Mon Apr 20 00:46:05 GMT+08:00 2020
pool-1-thread-10- read the value:3233 Mon Apr 20 00:46:05 GMT+08:00 2020
cost time: 18447
Read times: 18000, Write times: 3233
Copy the code

You can see that the reading time is the same, but the reading and writing time are different, and the writing and writing time are different. This indicates that reading is not mutually exclusive, but reading and writing are mutually exclusive.

2.4. StampedLock

import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.StampedLock;

public class StampedLockDemo {
    private static int MAX_READ_TIMES = 2000;  // Maximum number of reads
    private static int TOTAL_THREAD_NUM = 10;  // Total number of threads
    private static int WRITE_THREAD_NUM = 1;   // Number of write threads
    private static int READ_THREAD_NUM = TOTAL_THREAD_NUM - WRITE_THREAD_NUM; // Number of read threads
    private static boolean isStop = false;
    private static long startTime = System.currentTimeMillis();


    public static void main(String[] args) {
        StampedLockCache stampedLockCache = new StampedLockCache();
        ExecutorService executorService = Executors.newFixedThreadPool(TOTAL_THREAD_NUM);
        AtomicInteger value = new AtomicInteger();
        value.getAndIncrement();

        for (int i = 0; i < WRITE_THREAD_NUM; i++) {
            executorService.submit(() -> {
                while (true) {
// sleep(20); // Simulate no write operation for a period of time

                    int v = value.getAndIncrement();
                    stampedLockCache.put(1001, v);
                    System.out.println(Thread.currentThread().getName() + "- write the value." + v + "" + new Date());

                    if (isStop) {
                        break; }}}); }for (int i = 0; i < READ_THREAD_NUM; i++) {  // A large number of concurrent requests is a clear advantage of StampedLock; a small number may be a disadvantage
            executorService.submit(() -> {
                int callTimes = 0;
                while (true) {
                    System.out.println(Thread.currentThread().getName() + "- read the value." + stampedLockCache.get(1001) + "" + new Date());

                    callTimes++;
                    if (callTimes == MAX_READ_TIMES) {
                        isStop = true;
                        System.out.println("cost time: " + (System.currentTimeMillis() - startTime));
                        stampedLockCache.printTime();
                        break; }}}); }}public static void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch(InterruptedException ie) { Thread.currentThread().interrupt(); }}}class StampedLockCache {
    final Map<Integer, Integer> map = new HashMap<>();
    StampedLock stampedLock = new StampedLock();
    private AtomicInteger totalReadTimes = new AtomicInteger();
    private AtomicInteger totalWriteTimes = new AtomicInteger();
    private AtomicInteger conflictTimes = new AtomicInteger();

    public void put(Integer key, Integer val) {
        long stamp = stampedLock.writeLock();
        try {
            StampedLockDemo.sleep(5);  // Simulate the write time
            totalWriteTimes.getAndIncrement();
            map.put(key, val);
        } finally {
            stampedLock.unlock(stamp);  // Must be unlocked in finally block}}public Integer get(Integer key) {
        long stamp = stampedLock.tryOptimisticRead();  / / optimistic reading
        Integer val = map.get(key);
        totalReadTimes.getAndIncrement();
        if(! stampedLock.validate(stamp)) {// Check whether there is a write operation in the process of reading, if there is a re-read
            conflictTimes.getAndIncrement();
            try {
                stamp = stampedLock.readLock();    // Upgrade to pessimistic read lock
                val = map.get(key);
            } finally {
                stampedLock.unlockRead(stamp);   // Must be unlocked in finally block}}return val;
    }

    public void printTime(a) {
        System.out.println("Read times: " + totalReadTimes.get() + ", Write times: " + totalWriteTimes.get() +
                ", Conflict times: "+ conflictTimes.get()); }}Copy the code

Output result:

pool-1-thread-8- read the value:2140 Mon Apr 20 00:46:22 GMT+08:00 2020
pool-1-thread-8- read the value:2140 Mon Apr 20 00:46:22 GMT+08:00 2020
cost time: 12264
Read times: 18000, Write times: 2140, Conflict times: 13747
Copy the code

StampedLock performs better than ReentrantReadWriteLock. There are a total of 18,000 read times and 13747 write conflicts. In other words, only 13747 read operations need to be locked. ReentrantReadWriteLock locks read operations every time. The additional lock operation adds overhead.

Note that although StampedLock performs better here, StampedLock also has some usage constraints:

  1. Heavy entry is not supported
  2. Pessimistic read and write locks do not support conditional variables

In addition, change the total number of threads in both examples to 3, and the output is as follows:

//ReentrantReadWriteLockDemo
pool-1-thread-2- read the value:1667 Mon Apr 20 00:53:13 GMT+08:00 2020
pool-1-thread-2- read the value:1667 Mon Apr 20 00:53:13 GMT+08:00 2020
cost time: 9803
Read times: 4000, Write times: 1667

//StampedLockDemo
pool-1-thread-2- read the value:1817 Mon Apr 20 00:53:32 GMT+08:00 2020
pool-1-thread-2- read the value:1817 Mon Apr 20 00:53:32 GMT+08:00 2020
cost time: 10402
Read times: 4000, Write times: 1817, Conflict times: 3110
Copy the code

As you can see, StampedLock has a performance advantage under high concurrency.

3. Summary

  1. Different locking tools can be used in different scenarios. Determine which tool to use first.
  2. Synchronized has not been completely replaced by ReentrantLock, but is determined by the use scenario, and the advantage of synchronized is the simplicity of the code.
  3. The performance advantage of StampedLock takes effect only when the number of concurrent requests is high. If the number of concurrent requests is small, the performance of StampedLock is not higher than that of ReentrantReadWriteLock. In addition, StampedLock has usage constraints.

.end


<– Read the mark, left like!