Java Concurrency - depth Java AQS synchronization with the application of the principle - essential knowledge thread lock

Concurrent programming, we often see the word AQS, many of my friends do not know what stuff, bloggers read through some information finally understand, directly into the theme.

a brief introdction

AQS is an abbreviation AbstractQueuedSynchronizer class , this goes without saying, we enter this class in Eclipse, you'll know the class is an abstract class in java.util.concurrent.locks package. Why do we need to focus on the analysis of this abstract class, because ReentrantLock, Semaphore, CountDownLatch, ReentrantReadWritLock, ThreadPoolExecutor are all implemented based on AQS, AQS is a base class JUC (java.util.concurrent) toolkit. All said AQS is very important to learn concurrent programming must be aware of a class.

View source ReentrantLock can see that there is an internal abstract class inherits Sync AbstractQueuedSynchronizer class, other classes are related to a similar use, so you can learn AQS is to achieve its corresponding function through inheritance, and is an internal class.

public class ReentrantLock implements Lock, java.io.Serializable {
   abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = -5179523762034025860L;
        .......
    }

    # 非公平锁
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;
        .....
    }
    
    #公平锁
    static final class FairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;
        .....
    }
}

AQS official comments given an explanation as follows (this section can be ignored if you feel too much to see, looking directly at the summary section):

Based on a framework to provide first in first out (FIFO) queue and the associated synchronizer blocking locks (semaphores, events, etc.). This class is designed to be most useful type synchronizer base, the synchronizers depends on the individual atoms {@code int} value shows a state. Subclass must define the method of changing the protected status of these methods define what this means for the state of the object is acquired or released. In view of these, this class perform all other methods of queuing and blocking mechanisms. Subclasses can maintain other state fields, but only use {@link #getState}, {@ link #setState} and {@link #compareAndSetState} automatically updated values ​​will {@code int} is the synchronization tracking.

Subclasses should help define the non-common internal class, which is used for synchronization properties enclosing class. {@Code AbstractQueuedSynchronizer} class does not implement any synchronous interface. Instead, it defines {@link #acquireInterruptibly} other methods that can be invoked particularly suitably locks and the associated synchronizer to achieve their common method.

Such support one or both of the default exclusive mode and shared mode. When acquired in exclusive mode, other threads trying to acquire will not succeed. Acquired by multiple threads sharing model may (but need not) be successful. This class does not understand these differences, in addition to the mechanical sense, when a shared model for success, a next thread to wait (if any) must also determine whether it can also be obtained. Waiting threads in different modes share the same FIFO queue. Typically, implementation subclasses support only one mode, the two modes have a role, for example in the {@link ReadWriteLock}. Only support exclusive mode only or shared mode supporting method is not used does not need to subclass definition mode support.

This class defines a nested class {@link ConditionObject}, {} may be used as the exclusive mode supporting method for achieving a Link @ subclass {@link # isHeldExclusively} report a synchronization only for the current thread holds, {@ the method of call release link #} and {@link # getState} current value of the object is completely released, {@ link #} is obtained, in view of the stored state value, eventually return to the state of this object previously obtained. Any {@code AbstractQueuedSynchronizer} method does not create such conditions, so if you can not meet this constraint, do not use it. Of course, {@ link ConditionObject} its behavior depends on the semantics of synchronization is achieved.

This class provides the queue internal inspection, testing and monitoring method and process conditions similar object. Can be used as desired {@code AbstractQueuedSynchronizer} class exposed to them in order to achieve synchronization.

Sequence of this class of atoms stores only integer underlying maintenance state, so deserialized objects available thread queue. Typical of the subclass needs to define a sequence {@code readObject} method when the object deserialization restored to a known initial state.

AQS summary

Realization of the principle: the AQS subclass should be defined as non-public internal helper class, which is used for synchronization properties closed class and its corresponding synchronization is achieved by means of a method call;

It requires a deep understanding of content:

First In First Out (FIFO) queue waiting to realize what principle? - AQS based on FIFO (FIFO) queue blocking locks and related synchronizers

What is the role of state from the AQS? - This class is designed to be most useful type synchronizer base, the synchronizers depends on the individual atoms {@code int} value shows a state

AQS exclusive mode locks and how to achieve shared mode lock, how to achieve lock and release the lock?

CLH queue implementation principle

AQS core idea is that if the requested shared resource is free, then the current request thread resource to work effectively thread, and shared resource to a locked state. If the shared resource is requested is occupied, then you need a thread to block and wait for the lock mechanism assigned to be awakened, AQS this mechanism is implemented by CLH lock queue, is about to get less than temporarily lock a thread is added to the queue.

CLH (Craig, Landin, and Hagersten) virtual queue is a bidirectional queue (deque virtual queue that is not present example, there is only a relationship between the nodes). AQS each requesting thread is packaged as a shared resource a lock queue CLH node (Node) allocated to achieve lock.

 CLH queue structure is as follows:

Red node is the first node, it can be used as the node is holding the lock, the source Node inner class is a class queue implementation CLH with the head of the queue (head) and the AQS tail (tail)

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    static final class Node {...}
    private transient volatile Node head;
    private transient volatile Node tail;
    private volatile int state;//同步状态

 

From the foregoing, it is set for the head and tail volatile, modify these two nodes will be seen by other threads, in fact, we have largely done by the team and the team to modify the two nodes.

static final class Node {
    //该等待同步的节点处于共享模式
    static final Node SHARED = new Node();
    //该等待同步的节点处于独占模式
    static final Node EXCLUSIVE = null;

    //等待状态,这个和state是不一样的:有1,0,-1,-2,-3五个值
    volatile int waitStatus;
    static final int CANCELLED =  1;
    static final int SIGNAL    = -1;
    static final int CONDITION = -2;
    static final int PROPAGATE = -3;

    volatile Node prev;//前驱节点
    volatile Node next;//后继节点
    volatile Thread thread;//等待锁的线程
    //和节点是否共享有关
    Node nextWaiter;
    //Returns true if node is waiting in shared mode
    final boolean isShared() {
            return nextWaiter == SHARED;
    }

Explain the meaning of waitStatus under the following five:

  • CANCELLED (1): Thread the node may be due to a timeout or interrupted in was canceled (void) state, once in this state, node status will remain in CANCELLED (void), and should therefore be removed from the queue.
  • SIGNAL (-1): this is the SIGNAL node, the subsequent node is suspended, and must be awakened (unparking) its successor node in the current node after the cancellation or release the lock.
  • CONDITION (-2) node of the thread in the waiting state conditions, is not deemed to be a node in the synchronous queue until it is awakened (Signal), a value of 0 set, re-enter the blocked state.
  • 0: The new node

When the lock is acquired, not necessarily only one thread can hold the lock (otherwise known as synchronous state), so in this case with the exclusive mode and shared mode difference, that is, Node node identified by nextWait. For example ReentrantLock is an exclusive lock, only one thread to obtain a lock, but the lock is able to read WriteAndReadLock acquired simultaneously by multiple threads, but it can only write locks held by a thread. The first introduced acquire and release the lock in exclusive mode (otherwise known as synchronous state) This class uses to. Template model designed (particularly understood to see the subsequent design patterns associated Bowen) : skeleton definition of an operation in the algorithm, and delay will achieve some of the steps to subclasses.

Exclusive acquire locks

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

The first attempts to acquire the lock (tryAcquire (arg) is embodied in a defined subclass), if acquired, is finished, or by addWaiter (Node.EXCLUSIVE), arg) method to the end of the current queue node and set to exclusive

private Node addWaiter(Node mode) {
        //把当前线程包装为node,设为独占模式
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        //如果tail不为空,把node插入末尾
        if (pred != null) {
            node.prev = pred;
            //此时可能有其他线程插入,所以重新判断tail
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
}

private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            //此时可能有其他线程插入,所以重新判断tail是否为空
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
}

If the tail node is empty, execution ENQ (node); retry, the final node is inserted after the end of the queue node is inserted, it does not immediately suspend threads in the node, because during its insertion, in front of the thread. may have been executed, so it will carry out a spinning operation acquireQueued (node, arg), try to get the thread to reacquire lock! lock the exit from the spin process to obtain when the conditions are met, otherwise continue.

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                //如果它的前继节点为头结点,尝试获取锁,获取成功则返回           
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
}

If you do not get the lock, it is determined whether it should suspend, and this must be determined by the judge waitStatus its predecessor node:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
	int ws = pred.waitStatus;
	if (ws == Node.SIGNAL)
		return true;
	if (ws > 0) {
		do {
			node.prev = pred = pred.prev;
		} while (pred.waitStatus > 0);
		pred.next = node;
	} else {       
		compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
	}
	return false;
}

If waitStatus predecessor node is:

  • SIGNAL, returns true if the current thread should be suspended, pending the thread, and waiting to be awakened, after detection interrupt wake up, if we find the current thread is interrupted, throw InterruptedException and exit the loop.
  • > 0, the precursor kicked node queue, returns false
  • When <0, also returns false, but the first precursor to the SIGNAL waitStatus node, so that the next determination, the current node will hang.

Finally, we make a summary of the process of acquiring exclusive locks:

AQS template by calling the method of the subclass acquire a custom implementation tryAcquire acquiring state synchronization failure -> Node threads configured node (addWaiter) -> Node Adding nodes to the synchronous queue tail (addWaiter) -> node to spin the method of acquiring synchronization state (acquirQueued). When the node synchronization acquiring spin state, only when its precursor node is head node will attempt to acquire synchronization state, if the precursor is a precursor of this node is not a head node or that a single node is head node acquires synchronization failed state, it is determined the current thread needs to block, if it needs to block the return needs to be awakened after.

Release exclusive mode synchronization status

Since it is released, it is certainly the thread holding the lock release operation is performed, the thread releases the lock that is head node.

AQS the release state and a released synchronization acquire synchronization acquisition state, are template method, the specific operation tryRelease release has subclasses to achieve, parent AQS algorithm only provides a backbone.

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0){
            unparkSuccessor(h);
        }
        return true;
    }
    return false;
}

/**如果node的后继节点不为空且不是作废状态,则唤醒这个后继节点,否则从末尾开始寻找合适的节点,如果找到,则唤醒*/
private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        if (ws < 0){
            compareAndSetWaitStatus(node, ws, 0);
        }
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev){
                if (t.waitStatus <= 0){
                    s = t;
                }
            }
        }
        if (s != null){
            LockSupport.unpark(s.thread);
        }
}

 Process: first call tryRelease subclass () method to release the lock, and then wake up the subsequent node in the wake of, the need to determine whether a subsequent node to meet the situation, if the successor node is not and is not invalid state, then wake up the subsequent node, otherwise Looking forward from the tail node appropriate node, if found, then awaken.

Shared lock

Lock acquisition process:

  1. When a thread calls acquireShared () application to acquire the lock resources, if successful, enter the critical region.
  2. When acquiring the lock fails, create a shared type of node and into a FIFO queue, then suspended to wait wake.
  3. When the thread queue waiting to be awakened again attempt to acquire the lock on the resource, if successful wake behind a shared node and is still waiting for the wake-up event to pass along, that will in turn awaken all shared nodes in the back of the node , then enter critical section, otherwise continue to hang wait.

Lock release process:

  1. When a thread calls releaseShared () lock resources be released, if the release is successful, the node waiting in the queue is awakened, if any.

Share lock-depth analysis of source code

Based on the above mentioned shared lock execution process, we then look at the source code implementing logic:
first look at acquireShared method to acquire the lock (), as follows

 

   public final void acquireShared(int arg) {
        //尝试获取共享锁,返回值小于0表示获取失败
        if (tryAcquireShared(arg) < 0)
            //执行获取锁失败以后的方法
            doAcquireShared(arg);
    }

Here tryAcquireShared () method is left to the user to achieve a specific logical acquire lock. About implementation of the method There are two things in particular:

First, the method must check themselves whether to support the current context acquire a shared lock, if supported by further acquisition.

Second, the method return value is key. First, the above code fragment can be seen that the return value is less than 0 indicates a failure to acquire the lock, need to enter the queue. Second, if the return value is equal to 0 indicates that the current thread acquires a shared lock success, but it can not continue to follow the thread is obtained, that is, do not need to wake node waiting behind it. Finally, if the return value is greater than 0 indicates that the current thread acquires a shared lock and its subsequent success waiting node is also likely to continue to acquire a shared lock is successful, that is to say at this time need to follow them to wake node attempts to acquire a shared lock.

With the above agreement, we will look at methods to achieve doAcquireShared:

 

    //参数不多说,就是传给acquireShared()的参数
    private void doAcquireShared(int arg) {
        //添加等待节点的方法跟独占锁一样,唯一区别就是节点类型变为了共享型,不再赘述
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                //表示前面的节点已经获取到锁,自己会尝试获取锁
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    //注意上面说的, 等于0表示不用唤醒后继节点,大于0需要
                    if (r >= 0) {
                        //这里是重点,获取到锁以后的唤醒操作,后面详细说
                        setHeadAndPropagate(node, r);
                        p.next = null;
                        //如果是因为中断醒来则设置中断标记位
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                //挂起逻辑跟独占锁一样,不再赘述
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            //获取失败的取消逻辑跟独占锁一样,不再赘述
            if (failed)
                cancelAcquire(node);
        }
    }

Set the first node and then return to the interrupt mode acquiring an exclusive lock state after successful, the process ends. After obtaining the successful shared lock mode, call setHeadAndPropagate method, from the name of the method can be seen in addition to setting a new head node has a transitive action, see next Code:

 

    //两个入参,一个是当前成功获取共享锁的节点,一个就是tryAcquireShared方法的返回值,注意上面说的,它可能大于0也可能等于0
    private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; //记录当前头节点
        //设置新的头节点,即把当前获取到锁的节点设置为头节点
        //注:这里是获取到锁之后的操作,不需要并发控制
        setHead(node);
        //这里意思有两种情况是需要执行唤醒操作
        //1.propagate > 0 表示调用方指明了后继节点需要被唤醒
        //2.头节点后面的节点需要被唤醒(waitStatus<0),不论是老的头结点还是新的头结点
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            //如果当前节点的后继节点是共享类型或者没有后继节点,则进行唤醒
            //这里可以理解为除非明确指明不需要唤醒(后继等待节点是独占类型),否则都要唤醒
            if (s == null || s.isShared())
                //后面详细说
                doReleaseShared();
        }
    }

    private void setHead(Node node) {
        head = node;
        node.thread = null;
        node.prev = null;
    }

The final wake-up operation is also very complex, specialized out to analyze:
Note: This operation will be called in the wake releaseShare () method in.

 

private void doReleaseShared() {
        for (;;) {
            //唤醒操作由头结点开始,注意这里的头节点已经是上面新设置的头结点了
            //其实就是唤醒上面新获取到共享锁的节点的后继节点
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                //表示后继节点需要被唤醒
                if (ws == Node.SIGNAL) {
                    //这里需要控制并发,因为入口有setHeadAndPropagate跟release两个,避免两次unpark
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;      
                    //执行唤醒操作      
                    unparkSuccessor(h);
                }
                //如果后继节点暂时不需要唤醒,则把当前节点状态设置为PROPAGATE确保以后可以传递下去
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                
            }
            //如果头结点没有发生变化,表示设置完成,退出循环
            //如果头结点发生变化,比如说其他线程获取到了锁,为了使自己的唤醒动作可以传递,必须进行重试
            if (h == head)                   
                break;
        }
    }

Then look at the process of sharing the lock release:

 

public final boolean releaseShared(int arg) {
        //尝试释放共享锁
        if (tryReleaseShared(arg)) {
            //唤醒过程,详情见上面分析
            doReleaseShared();
            return true;
        }
        return false;
    }

Note: The above setHeadAndPropagate () method represents the waiting queue thread successfully acquires a shared lock, this time it needs to wake up sharing node behind it (if any), but when by releaseShared () method to release a shared lock time, Then wait for an exclusive lock with a shared lock thread can be awakened to try to obtain.

Third, the summary

Compared with an exclusive lock, the main feature is that when a shared lock after waiting queue sharing node successfully acquired the lock (it gets to a shared lock), since it is shared, then it must be followed with all wake behind share it with the current node lock resources, there is no doubt that these nodes must also be waiting for a shared lock (this is the premise that if an exclusive lock wait, that already have a shared node to acquire the lock, and it certainly does not get to). When a shared lock is released, it can be an example to think in read-write locks, when a read lock is released, then either a read lock or write lock can all be competing for resources.

The author of the micro-channel public number, a good day article:

 

Published 116 original articles · won praise 83 · views 280 000 +

Guess you like

Origin blog.csdn.net/belvine/article/details/104798795