Detailed gnaw through Java Concurrency -AQS

Previous LockSupport our interpretation of the source, which mentioned an important cornerstone of the JUC is AbstractQueuedSynchronizer class, referred to as AQS, then this is a formal study on this class. Since I was to learn Leveling, certainly there are many places is not enough understanding in place, welcome to discuss the message Ha! Or Tips, JDK version of this paper is to analyze 8.

AQS works Overview

Why in the opening chapter describes the principles AQS it? Because first of a number of knowledge points have a general idea, we can help when looking at the source code easier to understand, it is targeted more efficiently.

Here I summarize three more crucial point, we need to know.

  1. AQS has a volatile internal state variable, and provides compareAndSetState method, the thread can modify the value of state security, the needs of different scenarios, state will have a different meaning, vulgar point that is to define the rules of the game according to our own needs, As long as we comply with the rules, the game will be able to play together.
  2. When the competition lock failure, in fact, can be understood as CAS update state failure, the current thread will be packaged into a Node object, and then placed in a two-way Node queue, and then call LockSupport.park, thread to wait.
  3. When the lock is released, if there is a thread in the waiting queue AQS will check if there is, to unpark wake up the thread, and deleting of the Node (the first node to node) from the waiting queue.

Realization of ideas AQS is still very clear, the use of a state to maintain the competitive status, CAS is used to update the state security, access to the lock failed thread into the wait queue unpark, the lock is released, wake up a thread from the queue to continue to try to get lock.

AQS use a custom class Lock

AQS support exclusive and shared two modes, exclusive mode is relatively easier to understand, handle and a spout lip off, let's use AQS achieve an exclusive lock SmartLock to deepen understanding.

public class SmartLock {

    private class Sync extends AbstractQueuedSynchronizer {

        @Override
        protected boolean tryAcquire(int arg) {
            if (getExclusiveOwnerThread() == Thread.currentThread()) return true;
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            } else {
                return false;
            }
        }

        @Override
        protected boolean tryRelease(int arg) {
            setState(0);
            setExclusiveOwnerThread(null);
            return true;
         }
    }

    private Sync mSync;

    public SmartLock() {
        mSync = new Sync();
    }

    public void lock() {
        mSync.acquire(1);
    }

    public void unLock() {
        mSync.release(1);
    }
}
复制代码

We create an inner class Sync inherit AQS, rewrite its tryAcquire and tryRelease method, you can try to understand which correspond to acquire exclusive mode under lock and try to release the lock, returns true if successful, false indicates failure.

Here we stop to think about, since there is internal AQS can take advantage of a state, then we can set the rules of the game, state = 1 Shi indicates that the lock is occupied, state = 0 indicates that the lock is not held by a thread.

        protected boolean tryAcquire(int arg) {
            // 先判断当前持有锁的线程是不是本线程,如果是,直接返回true,所以我们这个锁是支持可冲入的
            if (getExclusiveOwnerThread() == Thread.currentThread()) return true;
            // CAS的方式更新state,只有当state=0时会成功更新为1
            if (compareAndSetState(0, 1)) {
                // 当前线程已经获取了锁,设置为Owner thread
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            } else {
                // 返回true,当前线程会被加入等待队列中
                return false;
            }
        }
        
        protected boolean tryRelease(int arg) {
            // 状态更新为0,
            setState(0);
            // Owner thread设置为null
            setExclusiveOwnerThread(null);
            return true;
        }
复制代码

We define two methods in SmartLock class lock and unLock, respectively, call acquire and release all the parameters are not used here, 1 can pass.

    public void lock() {
        mSync.acquire(1);
    }

    public void unLock() {
        mSync.release(1);
    }
复制代码

We use SmartLock to implement a thread-safe accumulator logic is simple increase is to provide a method to carry out counter ++ operator, we know ++ operation is not atomic, so we use SmartLock to ensure atomicity.

public class SmartAdder {
    private volatile int counter;
    private SmartLock lock;

    public SmartAdder() {
        lock = new SmartLock();
    }

    public void increase() {
        lock.lock();
        try {
            counter++;
        } finally {
            lock.unLock();
        }
    }

    public int getCounter() {
        return this.counter;
    }
}
复制代码

We write for a test case to verify, create a fixed core 20 thread pool threads, and then submit a cumulative 40 tasks, each cycle of 100,000 times, so get the correct result should be four million.

    public static void main(String[] args) {
        int threadCount = 20;
        int addCount = 100000;
        SmartAdder smartAdder = new SmartAdder();
        CountDownLatch countDownLatch = new CountDownLatch(threadCount);
        ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
        for (int i = 0; i < threadCount * 2; i++) {
            executorService.submit(() -> {
                for (int j = 0; j < addCount; j++) {
                    smartAdder.increase();
                }
                countDownLatch.countDown();
            });
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("count:" + smartAdder.getCounter());
        executorService.shutdown();
    }
    
    // 打印结果
    count:4000000
复制代码

Printing on the results confirm our SmartLock really work properly, such a simple mutex is complete, in fact, is not complicated thing! CountDonwLatch which is also a concurrent synchronization classes provided by JUC usage will explain about it later, here we only need to know await allows the current thread to wait for task execution thread pool can be completed.

AQS source Interpretation

With the front of the bedding, we now realize acquire and two specific methods of release in exclusive mode AQS look at.

Exclusive acquire method Code Explanation

Look acquire method

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
    
    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }
复制代码

You can see acquire a final method, we can not rewrite it, but has set aside a tryAcquire approach allows us to rewrite our class in the above SmartLock is rewriting tryAcquire this method, if tryAcquire returns false, calls acquireQueued method, its argument is the result of addWaiter (Node.EXCLUSIVE), we first take a look at the specific follow-up of realization addWaiter.

    private Node addWaiter(Node mode) {
        // 新建一个Node
        Node node = new Node(Thread.currentThread(), mode);
        Node pred = tail;
        // 如果队列的尾标兵tail不为空,将新加入的node插入到队尾,并更新tail
        if (pred != null) {
            node.prev = pred;
            // 如果CAS设置tail成功,直接返回
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        // 如果tail为空,或者CAS设置tail失败
        enq(node);
        return node;
    }
复制代码

The idea here is to the new node is inserted into the tail, but due to taking into account the thread safety problems, a CAS update, if the update fails, call the enq method, continue to follow up look at the implementation.

    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            // 如果检查到队列没有初始化,先执行初始化,注意head对头是标兵
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                // 在循环中执行CAS插入尾部操作
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }
复制代码

So look down, addWaiter logic is clear, is to get the current thread, the package is inserted into the end of the queue node. Look at acquireQueued implementation

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                // 返回node前驱
                final Node p = node.predecessor();
                // 如果前驱是head,说明当前队列中该线程排在第一顺位,再次调用tryAcquire
                // 因为后面调用的parkAndCheckInterrupt会让线程等待,当锁被release时,线程会被unpark
                // 所以重新tryAcquire来获取锁,如果获取成功,会将当前node设为头结点,相当于将当前
                // node从队列中删除了,因为头结点只是一个标兵,
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    // 这里之所以可以直接将.next置为null,而没有考虑node的next,因为是刚加入的node
                    // 它在队尾,而又是head的next,说明队列中就它一个,直接将head.next = null就可以了
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                // 先对head设置waitStatus标示位,然后park线程
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
    
    final Node predecessor() throws NullPointerException {
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }
复制代码

shouldParkAfterFailedAcquire This method is very interesting, it is the head of waitStatus set to SINGLE, there used to identify tasks that need to be awakened in time behind unpark will check the flag.

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        // 获取node的pre的waitStatus,
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            // 如果已经是Node.SIGNAL,可以安全的park,直接返回
            return true;
        if (ws > 0) {
            // 说明pred被取消了,并发时会出现这种情况,因为我们没有加锁
            // 继续向前查找waitStatus <= 0的node,
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            // 将pred的waitStatus设为SIGNAL
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
复制代码

Look at parkAndCheckInterrupt implement this method

    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }
复制代码

Relatively simple, direct call LockSupport.park, so let AQS in the way the thread is waiting for the park, which is why our previous article to LockSupport source cause analysis.

Then thread the park waiting for, then of course there should wake up, we look to achieve the release of AQS.

Exclusive release method Code Explanation

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

Also in the release AQS is a final method can not be overridden, we can rewrite tryRelease method. When the head is not empty, cut waitStatus not 0, calls unparkSuccessor method, see the next follow-up to achieve

    private void unparkSuccessor(Node node) {
        
        int ws = node.waitStatus;
        if (ws < 0)
           // 先将waitStatus设为0
            compareAndSetWaitStatus(node, ws, 0);

        // 一般需要被唤醒的是node.next,但是如果next的node被取消了,或者waitStatus>0,这时候这里的
        // 策略是从尾部开始重新选择一个node来unpark
        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)
            // unpark唤醒线程
            LockSupport.unpark(s.thread);
    }
复制代码

The release achieved relatively simple, the previously described tryAcquire fails, the current thread will wait is inserted into the queue, then the head of waitStatus SINGAL set, then upon release, first checks this flag, then to unpark, here a small details, if head.next been canceled or waitStatus> 0, will start from the tail of the queue look forward to the first eligible node to unpark.

Here's a detail we should note, release only the first wake up in the queue waiting to meet the conditions of the thread, so the next logical or in acquireQueued method, continue to try to call tryAcquire, if successful, it will be a queue (the current node set head node), the thread to continue, or continue to wait.

Introduced over exclusive mode, and then look in shared mode. And similar exclusive mode, AQS also sharing model provides a template method. Are acquireShared and releaseShared, they are final, we can rewrite the method is tryAcquireShared and tryReleaseShared.

Interpretation sharing mode acquireShared

    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }
    
    protected int tryAcquireShared(int arg) {
        throw new UnsupportedOperationException();
    }
复制代码

acquireShared first call tryAcquireShared method, if the return value is less than 0, doAcquireShared SHARED same type of construct added Node queue. Here we must mention, tryAcquireShared so we need to override the method, note that it is the return value of type int, and we analyze the above exclusive mode tryAcquire return value is boolean, because the return value in shared mode requires three states, it needs to be of type int.

  • tryAcquireShared <0, acquire a shared lock failure
  • tryAcquireShared = 0, succeed, but does not require a subsequent node queue wakeup
  • tryAcquireShared> 0, succeed, we need to wake up the subsequent node in the queue

Well, we continue to look doAcquireShared implementation

        // 添加当前节点到队列,与独占模式类似,不再赘述
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                // 获取node的前驱
                final Node p = node.predecessor();
                if (p == head) {
                    // 再次尝试获取共享锁
                    int r = tryAcquireShared(arg);
                    // 如果获取成功
                    if (r >= 0) {
                        // 检查队列中后续的节点,是否需要被唤醒
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                // park让线程等待,与独占模式类似,不再赘述
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
        
    private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        setHead(node);
        // propagate > 0, 或者 当前头结点或者当前节点node的waitStatus < 0时,调用doReleaseShared
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }
复制代码

Most of the logic with exclusive mode is similar, but more logical follow-up to check whether a node needs to be awakened.

Interpretation sharing mode releaseShared

    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }
    
    private void doReleaseShared() {
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }
复制代码

DoReleaseShared can be seen in a loop, if in the call, head changes, continue to cycle, or pick cycle, and are under the exclusive mode, no such concurrency issues, so no circulation in exclusive mode, another work is unparkSuccessor method, it is to wake up waiting threads, above in the analysis of exclusive mode has been analyzed, and will not repeat them here.

to sum up

First, the article outlines about the basic principles to achieve AQS and then use AQS implements a simple mutex, and finally a detailed analysis of the key methods to achieve AQS exclusive and shared in two modes. the above.

Guess you like

Origin juejin.im/post/5d73b2d4518825570327e2cf