JUC concurrent programming and source code analysis

Posted by johnnyboy16 on Tue, 30 Nov 2021 18:46:37 +0100

1, AQS(AbstractQueuedSynchronizer) Abstract queue synchronizer

Pre knowledge:

  • Fair lock and unfair lock
  • Reentrant lock
  • Spin lock
  • LockSupport
  • Linked list of data structure
  • Template design mode of design mode

1.1 what is it?

  • Abstract queue synchronizer
  • It is a heavyweight basic framework used to build locks or other synchronizer components and the cornerstone of the whole JUC system. It completes the queuing of resource acquisition threads through the built-in FIFO queue, and represents the state of holding locks through an int class variable

1.2 why is AQS the most important cornerstone of JUC content

1.2.1 AQS related

  • ReetrantLock
  • CountDownLatch
  • ReentrantReadWriteLock
  • Semaphore
  • . . .

1.2.2 further understand the relationship between lock and synchronizer

Lock, facing the user of the lock

It defines the use layer API for programmer lock interaction, hides the implementation details, and can be called.

Synchronizer, lock oriented implementer

Doug Lee, the great God of Java concurrency, proposed a unified specification and simplified the implementation of locks, shielding synchronization state management, blocking thread queuing and notification, wake-up mechanism, etc.

1.3 what can I do?

  • Locking can cause blocking
    • If there is congestion, you need to queue. To realize queuing, you must queue
  • interpretative statement
    The thread that grabs the resource directly processes the business. If it cannot grab the resource, it must involve a queuing mechanism. The thread that fails to preempt resources continues to wait (similar to the bank business processing window is full, and customers who do not have an acceptance window can only wait in line in the waiting area), but the waiting thread still retains the possibility of obtaining the lock and the lock acquisition process continues (customers in the waiting area are also waiting for a call, and it is their turn to go to the acceptance window to handle business).

When it comes to the queuing mechanism, there must be some kind of queue. What data structure is such a queue?

If shared resources are occupied, a certain blocking waiting wake-up mechanism is needed to ensure lock allocation. This mechanism is mainly implemented by a variant of CLH queue. Threads that cannot obtain locks temporarily are added to the queue. This queue is the abstract representation of AQS. It encapsulates the thread requesting shared resources into the Node of the queue, and maintains the state of the state variable through CAS, spin and LockSupport.park(), so that concurrency can achieve the effect of synchronization.

1.4 AQS preliminary

1.4.1 initial knowledge of AQS

Official website explanation

If there is congestion, you need to queue. To realize queuing, you must queue

AQS uses a volatile int type member variable to represent the synchronization State, completes the queuing of resource acquisition through the built-in FIFO queue, encapsulates each thread to preempt resources into a Node node to realize lock allocation, and modifies the State value through CAS.

1.4.2 AQS internal architecture


1.4.2.1 int variable of AQS

  • Synchronization State member variable of AQS
  • Status of acceptance window for bank business
    • Zero means no one, and the free state can be handled
    • Greater than or equal to 1, someone is occupying the window and waiting to go

1.4.2.2 CLH queue of AQS

  • CLH queue (composed of three Daniel names) is a two-way queue

1.4.2.3 summary

  • If there is congestion, you need to queue. To realize queuing, you must queue
  • state variable + CLH double ended queue

1.4.2.4 internal class Node(Node class is inside AQS class)

int variable of Node
  • Waiting state of Node waitState member variable
  • Waiting status of other customers (other threads) in the waiting area
  • Each queued individual in the queue is one
internal structure

Attribute description

1.4.3 basic structure of AQS synchronization queue


1.5 interpretation of AQS from ReetrantLock

  • The implementation classes of Lock interface basically complete thread access control by aggregating a subclass of queue synchronizer

1.5.1 principle of reetranlock

1.5.2 look at fairness and unfairness from the simplest lock method


1.5.3 unfair lock method (lock)

Compared with the implementation code of tryAcquire() method of fair lock and non fair lock, the difference is that there is less judgment when obtaining a lock for a non fair lock than in a fair lock! hasQueuedPredecessors()

In hasQueuedPredecessors(), it is judged whether queuing is required. The difference between fair locks and non fair locks is as follows:

Fair lock: Fair lock pays attention to first come, first come. When a thread obtains a lock, if there are already threads waiting in the waiting queue of the lock, the current thread will enter the waiting queue;

Unfair lock: whether there is a waiting queue or not, if the lock can be obtained, the lock object will be occupied immediately. That is, the first queued thread of the queue still needs to compete for locks after unpark() (in case of thread Contention)

lock()

acquire()


tryAcquire(arg)

  • This is an unfair lock
  • return false
    Continue to advance the conditions and move on to the next method
  • return true
    Lock grabbing succeeded

addWaiter(Node.EXCLUSIVE)

  • addWaiter(Node mode)

  • enq(node);

  • In a two-way linked list, the first node is a virtual node (also known as sentinel node). In fact, it does not store any information, but occupies a space. The real first node with data starts from the second node.

  • If ThreadC thread 3 comes in

    • prev
    • compareAndSetTail
    • next

acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

  • acquireQueued
  • If you fail again, you will enter
    • shouldParkAfterFailedAcquire and parkAndCheckInterrupt methods
    • If the waitStatus of the precursor node is in the SIGNAL state, that is, the shouldParkAfterFailedAcquire method will return true, and the program will continue to execute the parkAndCheckInterrupt method downward to suspend the current thread

1.5.4 unlock()

  • sync.release(1);

  • tryRelease(arg)

  • unparkSuccessor

summary

flow chart
https://www.processon.com/view/5e29b0e8e4b04579e40c15a7

2, ReentrantLock, ReentrantReadWriteLock, StampedLock

2.1 what is it?

Read write locks are defined as
A resource can be accessed by multiple read threads or by a write thread, but there cannot be read and write threads at the same time.

evolution

Read write lock meaning

"ReentrantReadWriteLock" is not a real separation of reading and writing. It only allows reading and reading to coexist, while reading, writing and writing are still mutually exclusive. In most actual scenarios, there is no mutually exclusive relationship between "read / read" threads, and only the operations between "read / write" threads or "write / write" threads need to be mutually exclusive. Therefore, ReentrantReadWriteLock is introduced.

A ReentrantReadWriteLock can only have one write lock at the same time, but multiple read locks can exist, but write locks and read locks cannot exist at the same time. That is, a resource can be accessed by multiple read operations or one write operation, but the two cannot be performed at the same time.

Only in the situation of reading more and writing less, the read-write lock has high performance.

2.2 features

  • Reentrant
  • Read write separation

No lock disorder - > lock - read / write lock case

class MyResource {
    Map<String,String> map = new HashMap<>();
    //=====ReentrantLock is equivalent to = = = = = synchronized
    Lock lock = new ReentrantLock();
    //=====ReentrantReadWriteLock integrates two sides, reading and writing are mutually exclusive and shared
    ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

    public void write(String key,String value) {
        rwLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName()+"\t"+"---Writing");
            map.put(key,value);
            try { TimeUnit.MILLISECONDS.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println(Thread.currentThread().getName()+"\t"+"---Write complete");
        }finally {
            rwLock.writeLock().unlock();
        }
    }

    public void read(String key) {
        rwLock.readLock().lock();
        try
        {
            System.out.println(Thread.currentThread().getName()+"\t"+"---Reading");
            String result = map.get(key);
            //Pause the thread for a few seconds
            try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println(Thread.currentThread().getName()+"\t"+"---Read complete result:  "+result);
        }finally {
            rwLock.readLock().unlock();
        }
    }

}

public class ReentrantReadWriteLockDemo {
    public static void main(String[] args) {
        MyResource myResource = new MyResource();

        for (int i = 1; i <=10; i++) {
            int finalI = i;
            new Thread(() -> {
                myResource.write(finalI +"", finalI +"");
            },String.valueOf(i)).start();
        }

        for (int i = 1; i <=10; i++) {
            int finalI = i;
            new Thread(() -> {
                myResource.read(finalI +"");
            },String.valueOf(i)).start();
        }

        for (int i = 1; i <=3; i++) {
            int finalI = i;
            new Thread(() -> {
                myResource.write(finalI +"", finalI +"");
            },"Ma Yucheng"+ i).start();
        }

    }
}

Reetrantredwritelock can be degraded from write lock - > read lock

  • Lock demotion: demote a write lock to a read lock (similar to the understanding of Linux file read-write permission, just as the write permission is higher than the read permission)

Description of lock degradation in the art of Java Concurrent Programming:

  • Increasing the severity of a lock is called an upgrade, and vice versa is called a downgrade

Demotion of read / write lock

Lock demotion: follow the sequence of acquiring a write lock → acquiring a read lock → releasing a write lock. A write lock can be demoted to a read lock.
If a thread occupies a write lock, it can also occupy a read lock without releasing the write lock, that is, the write lock is degraded to a read lock.
The purpose of lock degradation is to make the current thread aware of data changes and to ensure data visibility

Java8 official website description

Reentry also allows you to move from the write lock to the read lock by acquiring the write lock, then reading the lock, and then releasing the write lock

code
/**
 * Lock demotion: follow the sequence of acquiring a write lock → acquiring a read lock → releasing a write lock. A write lock can be demoted to a read lock.
 *
 * If a thread occupies a write lock, it can also occupy a read lock without releasing the write lock, that is, the write lock is degraded to a read lock.
 */
public class LockDownGradingDemo {
    public static void main(String[] args) {
        ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

        ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
        ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();

        //Obtain the write lock first, and then obtain the read lock
        /*writeLock.lock();
        readLock.lock();
        System.out.println("Write lock - > read lock ");
        writeLock.unlock();
        readLock.unlock();*/

        //The read lock is obtained first. The write lock can be obtained only after the read lock is released
        readLock.lock();
        System.out.println("Acquire read lock");
        writeLock.lock();
        System.out.println("Write lock -> Read lock");
        writeLock.unlock();
        readLock.unlock();
       

    }
}

conclusion
If a thread is reading, the write thread cannot obtain the write lock, which is a pessimistic lock strategy
A thread acquiring a read lock cannot be directly upgraded to a write lock.


In ReentrantReadWriteLock, when a read lock is used, if a thread attempts to obtain a write lock, the write thread will be blocked.
Therefore, you need to release all read locks before you can obtain write locks,

Write lock and read lock are mutually exclusive

Write locks and read locks are mutually exclusive (here mutual exclusion refers to mutual exclusion between threads,
The current thread can acquire both the write lock and the read lock, but cannot acquire the write lock after acquiring the read lock. This is because the read-write lock needs to maintain the visibility of the write operation.
Because if the read lock is allowed to acquire the write lock when it is acquired, other running read threads cannot perceive the operation of the current write thread.

So,
Analyzing the readwritelock ReentrantReadWriteLock, you will find that it has a potential problem:
When the read lock is completed, the write lock is expected; the write lock is exclusive, and the read and write are blocked;
If a thread is reading, the write thread needs to wait for the read thread to release the lock before obtaining the write lock. See the previous Case "LockDownGradingDemo"
That is, ReadWriteLock is not allowed to write in the process of reading. The current thread can obtain the write lock only when the waiting thread releases the read lock, that is, writing must wait. This is a pessimistic read lock, o(╥﹏╥) o. people are still reading it. Don't write first to save data chaos.

By analyzing StampedLock, you will find that its improvements are:
In the process of reading, it is also allowed to obtain the write lock (quite cow B, and the read and write operations also allow you to "share" (pay attention to the quotation marks)), which may lead to the inconsistency of the data we read!
Therefore, additional methods are needed to judge whether there are writes in the process of reading. This is an optimistic read lock. Obviously, the concurrency efficiency of optimistic lock is higher. However, once a small probability of writing leads to inconsistent read data, it needs to be detected and read again.

Oracle ReentrantWriteReadLock source code summary

Lock demotion the following example code is extracted from the ReentrantWriteReadLock source code:
ReentrantWriteReadLock supports lock degradation. It follows the order of obtaining a write lock, obtaining a read lock, and then releasing a write lock. A write lock can be degraded to a read lock, and lock upgrading is not supported.
The interpretation is at the bottom:

2.3 postmark lock StampedLock

  • No lock → exclusive lock → read / write lock → postmark lock

2.3.1 what is it?

  • StampedLock is a new read-write lock in JDK1.8 and an optimization of ReentrantReadWriteLock in JDK1.5.
  • Postmark lock, also known as Bill lock
  • Stamp (stamp, long type)
    • Represents the state of the lock. When stamp returns zero, it indicates that the thread failed to obtain the lock. In addition, when releasing the lock or converting the lock, the initially obtained stamp value must be passed in.

2.3.2 lock starvation

Lock starvation:

ReentrantReadWriteLock realizes read-write separation, but once there are many read operations, it becomes more difficult to obtain the write lock,
If there are currently 1000 threads, 999 reads and 1 write, it is possible that 999 read threads have grabbed the lock for a long time, and that one write thread is tragic
At present, there may always be a read lock, but the write lock cannot be obtained. There is no chance to write at all, o(﹏╥) o

How to solve the problem of lock hunger?

  • The use of "fairness" strategy can alleviate this problem to a certain extent
    • new ReentrantReadWriteLock(true);
  • However, the "fair" strategy is at the expense of system throughput

2.3.3 optimistic read lock of stampedlock class comes on stage

ReentrantReadWriteLock
Multiple threads are allowed to read at the same time, but only one thread is allowed to write. When the thread obtains the write lock, other write operations and read operations will be blocked,
Read locks and write locks are also mutually exclusive, so writing is not allowed when reading. Read-write locks are much faster than traditional synchronized locks,
The reason is that ReentrantReadWriteLock supports read concurrency

StampedLock was born
When the read lock of ReentrantReadWriteLock is occupied, other threads will be blocked when trying to obtain the write lock.
However, after StampedLock adopts optimistic lock acquisition, other threads will not be blocked when trying to obtain write locks. This is actually an optimization of read locks. Therefore, after obtaining optimistic read locks, the results need to be verified.

2.3.4 characteristics of stampedlock

  • All lock acquisition methods return a Stamp. A Stamp of zero indicates acquisition failure, and the rest indicates success;
  • All lock release methods require a Stamp, which must be consistent with the Stamp obtained when the lock is successfully obtained;
  • StampedLock is non reentrant and dangerous (if a thread already holds a write lock and then obtains the write lock, it will cause deadlock)

Three access modes of StampedLock

  • ① Reading mode: the function is similar to that of ReentrantReadWriteLock
  • ② Writing mode: the function is similar to the write lock of ReentrantReadWriteLock
  • ③ Optimistic reading: the lock free mechanism is similar to the optimistic lock in the database. It supports read-write concurrency. It is optimistic that no one will modify it when reading. If it is modified, it will be upgraded to pessimistic reading mode

Optimistic reading code presentation

  • Access to write locks is also allowed during read
public class StampedLockDemo
{
    static int number = 37;
    static StampedLock stampedLock = new StampedLock();

    public void write()
    {
        long stamp = stampedLock.writeLock();
        System.out.println(Thread.currentThread().getName()+"\t"+"=====Write thread ready to modify");
        try
        {
            number = number + 13;
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            stampedLock.unlockWrite(stamp);
        }
        System.out.println(Thread.currentThread().getName()+"\t"+"=====Write thread end modification");
    }

    //Pessimistic reading
    public void read()
    {
        long stamp = stampedLock.readLock();
        System.out.println(Thread.currentThread().getName()+"\t come in readlock block,4 seconds continue...");
        //Pause the thread for 4 seconds
        for (int i = 0; i <4 ; i++) {
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println(Thread.currentThread().getName()+"\t Reading......");
        }
        try
        {
            int result = number;
            System.out.println(Thread.currentThread().getName()+"\t"+" Get member variable value result: " + result);
            System.out.println("The writer thread did not modify the value because stampedLock.readLock()When reading, you can't write. Reading and writing are mutually exclusive");
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            stampedLock.unlockRead(stamp);
        }
    }

    //Optimistic reading
    public void tryOptimisticRead()
    {
        long stamp = stampedLock.tryOptimisticRead();
        //Get the data first
        int result = number;
        //After an interval of 4 seconds, we are optimistic that no other thread has modified the number value. We have good wishes. The actual situation depends on judgment.
        System.out.println("4 Seconds ago stampedLock.validate value(true No modification, false Modified)"+"\t"+stampedLock.validate(stamp));
        for (int i = 1; i <=4 ; i++) {
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println(Thread.currentThread().getName()+"\t Reading......"+i+
                    "Seconds later stampedLock.validate value(true No modification, false Modified)"+"\t"
                    +stampedLock.validate(stamp));
        }
        if(!stampedLock.validate(stamp)) {
            System.out.println("Someone moved--------Write operation exists!");
            //Someone has moved and needs to switch from optimistic reading to normal reading.
            stamp = stampedLock.readLock();
            try {
                System.out.println("Upgrade from optimistic read to pessimistic read and retrieve data");
                //Retrieve data
                result = number;
                System.out.println("The member variable value obtained by re reading the lock result: " + result);
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                stampedLock.unlockRead(stamp);
            }
        }
        System.out.println(Thread.currentThread().getName()+"\t finally value: "+result);
    }

    public static void main(String[] args)
    {
        StampedLockDemo resource = new StampedLockDemo();

        //1. Pessimistic reading is the same as ReentrantReadWriteLock
        /*new Thread(() -> {
            //Pessimistic reading
            resource.read();
        },"readThread").start();*/

        //2 optimistic reading, success
        /*new Thread(() -> {
            //Optimistic reading
            resource.tryOptimisticRead();
        },"readThread").start();

        //6 Successfully read resource.tryOptimisticRead() in seconds
        try { TimeUnit.SECONDS.sleep(6); } catch (InterruptedException e) { e.printStackTrace(); }*/

        //3 optimistic reading, failure, turn to pessimistic reading again, and reread the data once
        new Thread(() -> {
            //Optimistic reading
            resource.tryOptimisticRead();
        },"readThread").start();

        //Failed to read resource.tryOptimisticRead() for 2 seconds
        try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }


        new Thread(() -> {
            resource.write();
        },"writeThread").start();

    }
}

2.3.5 disadvantages of stampedlock

  • StampedLock does not support reentry and does not start with Re
  • The pessimistic read lock and write lock of StampedLock do not support Condition variables, which should also be noted.
  • When using StampedLock, you must not call the interrupt operation, that is, do not call the interrupt() method

Topics: Algorithm leetcode JUC