First acquaintance with Lock and AbstractQueuedSynchronizer (AQS)

1. The structure level of the concurrent package

In concurrent programming, Master Doug Lea has provided us with a large number of practical and high-performance tools. Researching these codes will make our team's grasp of concurrent programming more thorough and greatly enhance our team's love for concurrent programming technology. These codes are under the java.util.concurrent package. The following figure shows the directory structure of the concurrent package.

concurrent directory structure.png

It contains two sub-packages: atomic and lock, and blocking queues and executors under concurrent. These are the essence of the concurrent package, and will be learned one by one later. The implementation of these classes mainly depends on volatile and CAS (see this article for volatile, and section 3.1 of this article for CAS ). On the whole, the overall implementation diagram of the concurrent package is shown in the following figure:

The overall schematic diagram of concurrent package implementation.png

2. Introduction to lock

Let's take a look at the lock subpackage under the concentrate package. Locks are used to control how multiple threads access shared resources. Generally speaking, a lock can prevent multiple threads from accessing shared resources at the same time. Before the appearance of the Lock interface, java programs mainly implemented the lock function by the synchronized keyword. After Java SE5, the lock interface was added to the concurrent package, which provides the same lock function as synchronized. **Although it loses the convenience of implicit locking and unlocking like the synchronize keyword, it has the operability of lock acquisition and release, interruptible acquisition of locks, and timeout acquisition of locks and other synchronized keywords. synchronization characteristics. **Usually use the form of display use lock as follows:

Lock lock = new ReentrantLock();
lock.lock();
try{
	.......
}finally{
	lock.unlock();
}

It should be noted that when the synchronized block is executed or an exception is encountered, the lock will be automatically released, and the lock must call the unlock() method to release the lock, so the lock is released in the finally block .

2.1 Lock interface API

Let's now take a look at what methods the lock interface defines:

void lock(); //Acquiring the lock void lockInterruptibly() throws InterruptedException;//The process of acquiring the lock can respond to the interrupt boolean tryLock();//The non-blocking response to the interrupt can return immediately, and the lock is returned to true, otherwise it returns fasle boolean tryLock(long time, TimeUnit unit) throws InterruptedException;//The lock is acquired over time, and the lock can be acquired within the timeout or without interruption. Condition newCondition();//To acquire the waiting notification component bound to the lock, the current thread must obtain The lock can be waited for, the lock will be released first when waiting, and the lock can be returned from waiting when the lock is acquired again.

The above are the five methods under the lock interface, and they are only translated from the Chinese-English translation of the source code. If you are interested, you can take a look at it yourself. So what classes in the locks package implement this interface? Let's start with the most familiar ReentrantLock.

public class ReentrantLock implements Lock, java.io.Serializable

Obviously ReentrantLock implements the lock interface, let's take a closer look at how it is implemented. When you look at the source code, you will be surprised to find that ReentrantLock does not have much code. Another obvious feature is that basically all the implementations of the methods actually call the methods in its static memory class Sync, and the Sync class inheritsAbstractQueuedSynchronizer(AQS) . _ It can be seen that the key to understanding ReentrantLock lies in the understanding of the queue synchronizer AbstractQueuedSynchronizer (referred to as the synchronizer).

2.2 Getting to know AQS for the first time

There is a very specific explanation about AQS in the source code:

 Provides a framework for implementing blocking locks and related
 synchronizers (semaphores, events, etc) that rely on
 first-in-first-out (FIFO) wait queues.  This class is designed to
 be a useful basis for most kinds of synchronizers that rely on a
 single atomic {@code int} value to represent state. Subclasses
 must define the protected methods that change this state, and which
 define what that state means in terms of this object being acquired
 or released.  Given these, the other methods in this class carry
 out all queuing and blocking mechanics. Subclasses can maintain
 other state fields, but only the atomically updated {@code int}
 value manipulated using methods {@link #getState}, {@link
 #setState} and {@link #compareAndSetState} is tracked with respect
 to synchronization.
 
 <p>Subclasses should be defined as non-public internal helper
 classes that are used to implement the synchronization properties
 of their enclosing class.  Class
 {@code AbstractQueuedSynchronizer} does not implement any
 synchronization interface.  Instead it defines methods such as
 {@link #acquireInterruptibly} that can be invoked as
 appropriate by concrete locks and related synchronizers to
 implement their public methods.

The synchronizer is the basic framework for building locks and other synchronization components. Its implementation mainly relies on an int member variable to represent the synchronization state and constitutes a waiting queue through a FIFO queue. Its subclasses must override several protected methods of AQS to change the synchronization state , and other methods mainly implement queuing and blocking mechanisms. The update of the state uses the three methods of getState, setState and compareAndSetState .

The subclass is recommended to be defined as the static inner class of the custom synchronization component . The synchronizer itself does not implement any synchronization interface. It only defines a number of synchronization state acquisition and release methods for the use of the custom synchronization component. The synchronizer supports both The synchronization state can be obtained exclusively or shared, so that different types of synchronization components can be easily implemented.

Synchronizers are the key to implementing locks (or any synchronization component). Synchronizers are aggregated in the implementation of locks, and synchronizers are used to implement lock semantics. The relationship between the two can be understood in this way: the lock is user-oriented, it defines the interface between the user and the lock, and hides the implementation details; the synchronizer is the implementer of the lock, which simplifies the implementation of the lock and shields the synchronization Low-level operations such as state management, thread queuing, waiting and waking up . Locks and synchronizers nicely isolate the areas of concern for users and implementers.

2.3 Template method design pattern of AQS

The design of AQS is to use the template method design pattern, which opens some methods to subclasses for overriding, and the template method provided by the synchronizer to the synchronization component will call the method overridden by the subclass again . For example, the method tryAcquire needs to be overridden in AQS:

protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
}

NonfairSync (inheriting AQS) in ReentrantLock will override this method as:

protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}

And the template method acquire() in AQS:

 public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
 }

The tryAcquire method will be called, and when the template method acquire is called by NonfairSync that inherits AQS, the tryAcquire method that has been overridden by NonfairSync will be called. This is the way to use AQS. After understanding this, the implementation understanding of lock will be greatly improved. It can be summed up in the following points:

  1. The implementation of synchronization components (not only value locks, but also CountDownLatch, etc.) depends on the synchronizer AQS. In the implementation of synchronization components, the use of AQS is recommended to define static memory classes that inherit AQS;
  2. AQS is designed using the template method. The protected modification method of AQS needs to be rewritten by the subclass that inherits AQS. When the method of the subclass of AQS is called, the overridden method will be called;
  3. AQS is responsible for the management of synchronization state, thread queuing, waiting and waking up these underlying operations, while synchronization components such as Lock mainly focus on implementing synchronization semantics;
  4. When rewriting the AQS method, use the getState(),setState(),compareAndSetState()methods provided by AQS to modify the synchronization state

The rewriteable methods of AQS are as follows (from the book "The Art of Java Concurrent Programming"):

AQS Overridable Methods.png

The template method provided by AQS when implementing the synchronization component is as follows:

Template method provided by AQS.png

The template methods provided by AQS can be divided into 3 categories:

  1. Exclusive acquisition and release of synchronization state;
  2. Shared acquisition and release synchronization state;
  3. Query the status of waiting threads in the synchronization queue;

Synchronization components implement their own synchronization semantics through the template methods provided by AQS.

3. An example

The following uses an example to further understand the use of AQS. This example is also derived from the example in the AQS source code.

class Mutex implements Lock, java.io.Serializable {
    // Our internal helper class
    // 继承AQS的静态内存类
    // 重写方法
    private static class Sync extends AbstractQueuedSynchronizer {
        // Reports whether in locked state
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }

        // Acquires the lock if state is zero
        public boolean tryAcquire(int acquires) {
            assert acquires == 1; // Otherwise unused
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        // Releases the lock by setting state to zero
        protected boolean tryRelease(int releases) {
            assert releases == 1; // Otherwise unused
            if (getState() == 0) throw new IllegalMonitorStateException();
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        // Provides a Condition
        Condition newCondition() {
            return new ConditionObject();
        }

        // Deserializes properly
        private void readObject(ObjectInputStream s)
                throws IOException, ClassNotFoundException {
            s.defaultReadObject();
            setState(0); // reset to unlocked state
        }
    }

    // The sync object does all the hard work. We just forward to it.
    private final Sync sync = new Sync();
    //使用同步器的模板方法实现自己的同步语义
    public void lock() {
        sync.acquire(1);
    }

    public boolean tryLock() {
        return sync.tryAcquire(1);
    }

    public void unlock() {
        sync.release(1);
    }

    public Condition newCondition() {
        return sync.newCondition();
    }

    public boolean isLocked() {
        return sync.isHeldExclusively();
    }

    public boolean hasQueuedThreads() {
        return sync.hasQueuedThreads();
    }

    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

    public boolean tryLock(long timeout, TimeUnit unit)
            throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }
}

MutexDemo:

public class MutextDemo {
    private static Mutex mutex = new Mutex();

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(() -> {
                mutex.lock();
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    mutex.unlock();
                }
            });
            thread.start();
        }
    }
}

Implementation:

Execution of mutex.png

The above example implements the semantics of an exclusive lock, where only one thread is allowed to hold the lock at a time. MutexDemo created 10 new threads and sleep for 3s respectively. It can also be seen from the execution that the current Thread-6 is executing the lock while other threads such as Thread-7 and Thread-8 are in the WAIT state. According to the recommended method, Mutex defines a static inner class Sync that inherits AQS , and rewrites AQS's tryAcquire and other methods, and the update of state also uses setState(), getState(), compareAndSetState() these three method. The method in the implementation of the lock interface also just calls the template method provided by AQS (because Sync inherits AQS). From this example, it can be clearly seen that AQS is mainly used in the implementation of synchronization components, and AQS "shields" the modification of synchronization state, thread queuing and other underlying implementations. The template method of AQS can easily give The implementer of the synchronous component makes the call. For users, it is only necessary to call the method provided by the synchronization component to realize concurrent programming. At the same time, two key points that need to be grasped when creating a new synchronization component are:

  1. When implementing a synchronization component, it is recommended to define a static memory class that inherits AQS, and override the required protected modified methods;
  2. The implementation of synchronous component semantics relies on AQS template methods, which in turn rely on methods overridden by subclasses of AQS.

In layman's terms, because the overall design idea of ​​AQS adopts the template method design mode, the functions of synchronization components and AQS are actually divided into two parts:

Synchronous component implementer's perspective:

Through overridable methods: Exclusive : tryAcquire() (exclusively acquire synchronization state), tryRelease() (exclusively release synchronization state); Shared : tryAcquireShared() (shared acquisition synchronization state), tryReleaseShared()( Shared release synchronization state); tells AQS how to judge whether the current synchronization state is successfully acquired or released . The synchronization component focuses on the logical judgment of the current synchronization state, so as to realize its own synchronization semantics. This sentence is relatively abstract. For example, the above Mutex example implements its own synchronization semantics through the tryAcquire method. In this method, if the current synchronization state is 0 (that is, the synchronization component is not acquired by any thread), the current thread can acquire At the same time, change the state to 1 and return true, otherwise, the component has been occupied by the thread and return false. Obviously, the synchronization component can only be occupied by the thread at the same time, and Mutex focuses on the acquisition and release logic to achieve the synchronization semantics that it wants to express.

AQS's perspective

For AQS, you only need the true and false returned by the synchronization component, because AQS will have different operations on true and false. If true, it will think that the current thread obtains the synchronization component and returns directly, and if it is false, AQS will also A series of methods such as inserting the current thread into the synchronization queue.

In general, the synchronization component implements the synchronization semantics it wants to express by rewriting the AQS method, and AQS only needs to synchronize the true and false expressions of the component. AQS will do different processing for different situations of true and false. As for the underlying implementation, you can read this article .

references

The Art of Java Concurrent Programming

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325388073&siteId=291194637