Take a look at LockSupport and AQS

This time, we can see the principle of lock in concurrency. We will talk about AQS, ReentrantLock, ReentrantReadWriteLock and the newly added StampedLock in JDK8. These are very important things in java concurrency. Take a look!

 

1, LockSupport tool class

LockSupport tool class is in rt.jar of jdk. Its main function is to suspend and wake up threads. This class is the basis for creating locks and other synchronization classes. We also need to know that the LockSupport class is based on the Unsafe class. After a brief look at Unsafe, do you think it's still familiar!

Let's first look at LockSupport's park and unpark methods. Note that these two methods are similar to wait and notify functions, but I prefer to call them authorization here!

A simple example:

package com.example.demo.study;
import java.util.concurrent.locks.LockSupport;

public class Study0130 {

    public static void main(String[] args) {
        System.out.println("main begin");                
        LockSupport.park();       
        System.out.println("main end");
    }
}

 

We can see that if we call the park method directly, the current thread will be blocked and can't go to the back. Here we can say that the current thread is not authorized by LockSupport class and has no license, so we can only hang at the intersection of park(). So how can we make the current thread authorized? We need the unpark() method for authorization

package com.example.demo.study;
import java.util.concurrent.locks.LockSupport;

public class Study0130 {

    public static void main(String[] args) {
        //Here is the authorization for the current thread. The current thread can run at will and encounter park Will not hang up
        LockSupport.unpark(Thread.currentThread());
        
        System.out.println("main begin");
        LockSupport.park();
        System.out.println("main end");

    }
}

 

Remember the use of wait and notify? A thread A calls the wait method, then thread A is hung up. If the notify method is invoked in thread B, then the A thread will be waken up. The park and unpark methods here can also achieve this.

package com.example.demo.study;

import java.util.concurrent.locks.LockSupport;

public class Study0130 {

    public static void main(String[] args) {

        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("thread1 start");
                //Thread 1 will block
                LockSupport.park();
                System.out.println("thread1 end");
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("thread2 start");
                //Authorize thread 1
                LockSupport.unpark(thread1);
                System.out.println("thread2 end");
            }
        });
        thread1.start();
        thread2.start();
    }
}

 

When we turn on the park and unpark methods of LockSupport, we can find that they are implemented by calling Unsafe. Unfortunately, we can't see the source code

 

If we call the park method to block the thread for too long, it's not what we want to see. We can also use parkNanos to set the blocking time. When the time is up, it will return automatically:

 

Finally, when you call the park method, you can pass in an object, such as LockSupport.park(this). When you use the jstack pid command to view the stack information, you can see that the class is blocked!

So far, it should be the common method of LockSupport!

 

2, Understanding AQS

The full name of AQS is AbstractQueuedSynchronizer, which is called abstract synchronization queue. It is used to implement various synchronization components. For example, locks in concurrent packages are implemented with this. Make this clear, and the mechanism of those locks is almost understood!

So what is the so-called AQS? In fact, it is a two-way linked list with sequence (or FIFO two-way queue, the same meaning). In this two-way linked list, each node can store a thread, and all the attributes of the node are shown in the figure below. Let's say a few;

prev refers to the former node, next refers to the latter node, thread refers to a thread stored in the current node, SHARED refers to the thread stored in the current node is lost to the linked list because of the blocking of obtaining SHARED resources; EXCLUSIVE refers to the thread stored in the current node is lost to the linked list because of the blocking of acquiring EXCLUSIVE resources;

waitStatus indicates the status of the thread stored in the current node. There are several possible statuses: (1) cancel = 1; indicates that the thread has been CANCELLED; (2) SIGNAL = - 1; indicates that the thread needs to wake up; (3) CONDITION = -2; indicates that the thread is waiting in the list; (4) PROPAGATE = -3); indicates that the thread needs to notify other nodes when releasing shared resources;

Note that there is also a status here, that is, waitStatus is 0, indicating that the current node is in the initial state, so you can know that when waitStatus is greater than 0, it is invalid, and when it is less than zero, it is valid

  

This Node class is an internal class of AQS, so how to access the linked list through AQS? Next, let's see what AQS attributes can help us access the two-way linked list;

//field
//Head node pointing to linked list
private transient volatile Node head;
//Tail node pointing to linked list
private transient volatile Node tail;
//The meaning of this field is different in each implementation class, such as ReentrantLock Indicates the number of times that can be re entered,
//stay Semaphore The number of available signals in
private volatile int state;

//Obtain Unsafe Object, used before, remember why it can be used getUnsafe But not in our own classes
private static final Unsafe unsafe = Unsafe.getUnsafe();

//The following properties are obtained AQS The offset of the field in the class, which has been mentioned in the previous blogs, is useful
private static final long stateOffset;
private static final long headOffset;
private static final long tailOffset;
private static final long waitStatusOffset;
private static final long nextOffset;

//Method
//These methods are all trying to acquire the lock
public final void acquire(int arg) {}//exclusive mode
protected boolean tryAcquire(int arg) {}
public final void acquireShared(int arg) {}//Sharing mode
public final void acquireInterruptibly(int arg){}//exclusive mode
public final void acquireSharedInterruptibly(int arg){}//Sharing mode
//These are all attempts to release the lock public final boolean release(int arg) {}//exclusive mode public final boolean releaseShared(int arg) {}//Sharing mode protected boolean tryRelease(int arg) {} protected boolean tryReleaseShared(int arg) {}

 

In AQS, the synchronization of threads mainly involves the operation of state, which can be divided into two modes: exclusive mode and sharing mode. As for the two modes, their respective methods of obtaining and releasing locks have been identified above!

What's a lock here? In java multithreading, an object can be treated as a lock. Why? We can simply look at the components of a common java object (not an array) in the java heap:

 

A java object is composed of object Header and Instance Data Data) and Padding are composed of three parts. Instance Data and Padding can be regarded as one class, because Padding by alignment plays the role of filling in blanks. Because the number of bytes of java objects must be a multiple of 8 (the object Header must be a multiple of 8, in fact, it is just a multiple of 8 to fill the Instance Data), so Padding by alignment may or may not exist;

The object header is generally composed of two parts (if there is an array, there is another part, namely the array length), as follows:

The first part: it is used to store the runtime data of the object itself, such as HashCode, GC generation age, lock status flag, lock held by the thread, biased thread ID, biased timestamp, etc. the length of this part of data is 32bit and 64bit in 32-bit and 64 bit virtual machines (unopened compression start pointer), which is officially called "MarkWord".

The second part: the other part of the object header is the klass type pointer, which is the pointer of the object to its class metadata. The virtual machine uses this pointer to determine which class instance this object is.

We can think of an object as a lock. If a thread acquires a lock, the number of a thread is stored in the markword of the object header of the lock object, which means that the thread holds the lock!

As mentioned above, we probably know that the so-called AQS is as shown in the figure below. It maintains a linked list. Each time, only the threads in the head node are running. When the threads in the head block or interrupt for some reasons, the next thread will try to obtain resources. Repeat this

Then let's talk about a thread obtaining resources exclusively or by sharing;

 

3, Exclusive mode

When a thread wants to acquire the resource in an exclusive way, to be frank is to implement an exclusive lock. Similar to synchronized code block, operations on shared resources are all in this code block. Only when a thread acquires the lock first can it enter the code block to operate shared resources. When other threads try to acquire the lock, the thread of the object header in the lock is the same If the number comparison is not the same, you can only put this thread in the linked list and save it, then hang it, wake it up after the conditions are met, which is realized by using the park and unpark methods of LockSupport.

Take ReentrantLock as an example. After a thread obtains the lock of ReentrantLock, it will first use CAS to change the state from 0 to 1 in AQS, and then set the current lock to be held by the thread. If the current thread continues to try to acquire the lock, it will only change the state from 1 to 2, and the rest will not change, which is also called the number of reentrant times. When other threads try to acquire the lock When locking, it is found that the object header of the lock object is not its own thread number, so it is dropped into the blocking queue and suspended;

1. When a thread obtains exclusive resources through acquire(int arg):

public final void acquire(int arg) {
     //1.tryAcquire Method is not implemented. This method is mainly reserved for specific subclasses to implement and use through specific scenarios CAS modify state Value of, modified successfully return true,otherwise false
     //2.If modified state If the value of fails, the second condition will be here, which encapsulates the current thread as a Node.EXCLUSIVE Type, then save to the end of the list, and finally acquireQueued Method is called internally
      LockSupport.park(this);Method block thread
//3.call selfInterrupt Method to interrupt the current thread. Why? Because a thread is waiting in the blocking queue, when it is interrupted in some way, it will not immediately see the effect,
   //The self interrupt method will only be called after this thread obtains the resource to make up the interrupt
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } //Interrupt current thread static void selfInterrupt() { Thread.currentThread().interrupt(); }

 

2. When the thread releases exclusive resources through release(int arg):

public final boolean release(int arg) {
    //tryRelease The method is not implemented. The subclass is implemented according to the specific scenario. In fact, it is modified state Value
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
        //This method is below, which will call LockSupport.unpark(s.thread)Method to activate the thread blocking a node in the queue, and the activated thread will pass the tryAcquire Try the current state Whether to meet their own needs
     //Run if the conditions are met, or suspend if the conditions are not met unparkSuccessor(h); return true; } return false; }

Through a simple look at the access and release of resources, we can see whether the underlying or the Unsafe park and unpark methods are used. In addition, the tryAcquire() method and tryRelease() method need to be implemented in the specific subclass itself, which is the modification of state in AQS. The subclass also needs to define the meaning of the increase or decrease of state value;

For example, ReentrantLock inherits from the implementation of AQS. A state of 0 indicates that the lock is idle, and a state of 1 indicates that the lock is occupied. When overriding the tryAcquire() method, CAS needs to be used to change the state value from 0 to 1, and the current lock holder is the current thread. When overriding the tryRelease() method, CAS needs to be used to change the state value from 1 to 0, and then the current lock holder The person is null.

 

4, How to share

After knowing the exclusive mode, the sharing mode is simple. What is sharing? At the same time, multiple threads can obtain resources, which is called sharing!!!

After a thread attempts to acquire resources successfully, another thread can also directly use CAS to attempt to acquire resources. If it succeeds, it will be modified, and if it fails, it will be put into the linked list for storage. For example, Semaphore semaphore Semaphore semaphore. When a thread acquires Semaphore through acquire() method, Semaphore will be acquired through CAS if it meets the conditions. If it does not meet the requirements, it will be thrown into the linked list Inside the list;

The sharing mode is similar to the previous exclusive mode. Let's take a look at it briefly:

1. When a thread obtains a shared resource through acquieshared (int ARG):

 public final void acquireShared(int arg) {
    //tryAcquireShared The method is also not implemented, leaving the specific subclass to be implemented according to the actual situation, and setting state If the setting is successful, it will be returned directly
    //If the setting fails, enter the doAcquireShared Method, which encapsulates the current thread as Node.SHARED Type, and then put it at the back of the blocking queue
   //Suspend yourself using the LockSupport.park(this) method
if (tryAcquireShared(arg) < 0) doAcquireShared(arg); }

 

2. When a thread releases a shared resource through releaseShared(int arg):

public final boolean releaseShared(int arg) {
    //tryReleaseShared Method implemented by subclass, modified state Value of, trying to free resources
    //Release resources successfully, and then use LockSupport.unpark(thread)To wake up a thread in a blocked queue
    //The active thread will use tryReleaseShared View current state Whether the value of is in line with your own needs. If it is, it will be activated and run down. Otherwise, it will be placed in the AQS Pending in block queue
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

For example, ReentrantReadWriteLock inherits from the implementation of AQS. Since state is of type int, 32 bits, and the high 16 bits represent the number of times to acquire the read lock, in the implementation of tryAcquireShared method of read lock, first check whether the write lock is held by other threads, if it is, return false, otherwise use CAS to add the high 16 bits of state + 1; in the implementation of tryreleased shared of read lock, CAS is used internally to reduce the high 16 bits of state by one. If successful, true will be returned. If unsuccessful, false will be returned

Tags: Java JDK less REST

Posted on Sat, 01 Feb 2020 04:48:38 -0500 by thelinx