一文读懂Java并发编程之AQS

庄子与惠子游于濠梁之上。庄子曰:“儵鱼出游从容,是鱼之乐也。”惠子曰:“子非鱼,安知鱼之乐?”庄子曰:“子非我,安知我不知鱼之乐?”,你不是我,怎知我走过的路,遇过的事,吃过的苦,看过的春夏秋冬,你不是我,怎知我在热闹后孤单离去的背影,光鲜亮丽过后的尴尬,你不是我,怎知我笑容背后有没有忧伤,眼泪背后有没有万家灯火的照耀!

AbstractQueuedSynchronizer数据结构

AbstractQueuedSynchronizer是一个双端队列,元素可以从队首进出,也可以从队尾进出,下面是AbstractQueuedSynchronizer 的成员变量。

abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
    private static final long serialVersionUID = 7373984972572414691L;
    protected AbstractQueuedSynchronizer() {}
    static final class Node {
        static final Node SHARED = new Node();
        static final Node EXCLUSIVE = null;
        static final int CANCELLED = 1;
        static final int SIGNAL = -1;
        static final int CONDITION = -2;
        static final int PROPAGATE = -3;
        volatile int waitStatus;
        volatile Node prev;
        volatile Node next;
        volatile Thread thread;
        Node nextWaiter;
    }
    private transient volatile Node head;
    private transient volatile Node tail;
    private volatile int state;
}
复制代码

Node

prev: prevNode的成员变量,代表队列中指向前一个节点的指针。

next: next是指向下一个节点的指针。

thread: thread是进入队列的线程。

SHARED:代表访问共享资源的线程被阻塞。

EXCLUSIVE: 代表访问独占资源的线程被阻塞。

waitStatus:代表线程的等待状态,CANCELLED为线程被取消,SIGNAL为线程需要唤醒,CONDITION为在条件队列中等待, PROPAGATE为节点释放时需要通知其他节点。

head , tail

head为队列的头指针,tail为队列的尾指针

state

state是一个状态值,不同实现AbstractQueuedSynchronizer的锁所代表的意思都不同,如果我们使用过CountDownLatch,那么它的方法countDown() 就是执行的就是-1操作,操作的其实就是这个state值,就相当于一个计数器,对于SemaphoreCyclicBarrier,他们也是通过操作 state来进行计数,ReentrantLock中,也是通过操作state来实现线程是否获取到锁,当state为0时,代表当前锁时空闲的,没有 被线程持有,如果state为1,则当前锁被线程持有,如果大于1,则证明线程重入了,state+1,而对state的操作又分为独占和共享。

独占操作

独占的意思就是同一时间只有一个线程能操作,其他线程过来都会被阻塞,只有当前线程完成任务后释放了资源,其他线程才能继续获取资源,每一个线程都与资源进行绑定, 上面我们说的ReentrantLock就是独占锁。

线程获取独占资源分析(以ReentrantLock为例)

acquire(arg)

首先进入acquire(arg)方法,然后做下一步的判断。

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

tryAcquire(arg)

tryAcquire(arg)判断资源是否被占用,在tryAcquire()中主要通过获取state的值判断资源是否被占用,getState()获取state的值,如果为0,则证明资源没被 占用,就使用CAS操作更新值,然后state的值为1,然后使用setExclusiveOwnerThread()设置独占资源的线程(独占资源和每一个线程进行绑定),然后返回,下面有一个 else if判断,如果当前线程等于独占资源的线程,证明线程重入了,那么更新state的值为2(state原本为1,重入后+1),最后返回。

protected final boolean tryAcquire(int acquires) {
    //获取当前线程
    final Thread current = Thread.currentThread();
    //获取state
    int c = getState();
    //资源没被独占
    if (c == 0) {
        //使用CAS对state进行更新
        if (!hasQueuedPredecessors() &&
        compareAndSetState(0, acquires)) {
            //设置独占资源的线程
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //如果线程等于服战资源的线程(重入)
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        //state+1
        setState(nextc);
        return true;
    }
    return false;
}
复制代码

acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

如果资源已经被独占了,那么就需要将当前线程插入AQS队列中,然后将当前线程挂起,我们看一下addWaiter(Node.EXCLUSIVE),可以看出将Node节点插入了AQS 的队尾。

private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }
复制代码

acquireQueued(final Node node, int arg)

acquireQueued就是将当前线程挂起,我们看里面的一个方法parkAndCheckInterrupt(), 可以看出使用 LockSupport.park(this)来将当前线程挂起。

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

release(int arg)

当一个线程调用了release(arg)方法释放资源时,它会调用tryRelease(arg)尝试释放资源。

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

tryRelease(int releases)

尝试释放资源的逻辑很简单,就是用AQS中的state-1,如果为0,则能证明可以释放,就会通过setState(arg)方法设置state的值,注意,如果c=0,则 证明此线程不是重入的线程,那么就直接释放资源,并且解除与资源绑定的线程(setExclusiveOwnerThread(null)),如果c != 0 ,则证明资源被重入, 则将state - 1 ,不解除与独占资源绑定的线程。

protected final boolean tryRelease(int releases) {
        int c = getState() - releases;
        if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
        boolean free = false;
        if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
        }
        setState(c);
        return free
复制代码

释放完资源后,就会唤醒使用LockSupport.unpark(s.thread)唤醒AQS中的线程,然后又重复上面的过程。

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);
    }
复制代码

共享操作(以CountDownLatch为例)

当线程进入countDownLatch.await()方式时,会调用AQS的acquireSharedInterruptibly方法,然后进入tryAcquireShared(arg)方法。

public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    if (tryAcquireShared(arg) < 0)
        doAcquireSharedInterruptibly(arg);
}
复制代码

tryAcquireShared(arg)

tryAcquireShared(arg)的作用就是判断AQS状态值state,如果state = 0 , 返回1,否则返回-1,如果返回1,则证明线程对资源的操作已经结束,如果为-1,则证明对资源的操作 尚未结束,此时就会调用doAcquireSharedInterruptibly(arg)方法。

protected int tryAcquireShared(int acquires) {
    return (getState() == 0) ? 1 : -1;
}
复制代码

doAcquireSharedInterruptibly(arg)

调用doAcquireSharedInterruptibly(arg)会通过addWaiter(Node.SHARED)将当前线程封装成SHARED类型的Node,然后插入AQS队列的尾部,然后调用 parkAndCheckInterrupt()使用LockSupport.park(this)将当前线程挂起。

private void doAcquireSharedInterruptibly(int arg) throws InterruptedException {
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
复制代码

releaseShared(int arg)

当使用countDownLatch.countDown()时,其实就是对资源的释放,最终会调用releaseShared(int arg)方法,然后调用tryReleaseShared(int releases)`尝试释放资源

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}
复制代码

tryReleaseShared(int releases)

调用tryReleaseShared(int releases)尝试释放资源,释放资源的逻辑时根据state来判断,如果state = 0,证明当前资源没有任何线程持有,所以就直接返回,如果state != 0 , 证明资源还被线程持有,所以当前线程 就将state - 1,然后继续向下执行doReleaseShared()

protected boolean tryReleaseShared(int releases) {
    for (;;) {
        int c = getState();
        if (c == 0)
            return false;
        int nextc = c-1;
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
  }
}
复制代码

doReleaseShared()

doReleaseShared()会调用unparkSuccessor(h)来唤醒线程(LockSupport.unpark(s.thread)),因为传递过去的参数h = head , 所以可知唤醒的线程 是队头节点的线程。

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;
    }
}
复制代码

ConditionObject条件变量

ConditionObjectAbstractQueuedSynchronizer的内部类,他是实现线程间的同步的基础设施,它是与锁结合使用的(如ReentrantLock),ConditionObject 实现了Condition接口,Condition接口提供了await(),signal()等方法,实现线程的挂起和唤醒,ConditionObject是一个 条件变量,每个条件变量对应一个条件队列,当调用Conditionawait()被挂起的线程将会存放在条件队列中,调用signal()时将从条件队列中移除并放入 AQS队列中。

 public class ConditionObject implements Condition, java.io.Serializable {
    private static final long serialVersionUID = 1173984872572414699L;
    /**条件队列第一个节点 */
    private transient Node firstWaiter;
    /** 条件队列最后一个节点 */
    private transient Node lastWaiter;
}
复制代码

示例

我们创建了一个ReentrantLock锁,然后使用lock.newCondition()创建了一个条件变量,其实它最终创建的时AQS中的ConditionObject条件变量,一个锁可以对应多个条件变量, 每个条件变量对应一个条件队列。

public class ConditionLockTest {
    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        Condition condition = lock.newCondition();
        new Thread(() -> {
            lock.lock();
            try {
                System.out.println("start1");
                condition.await();
                System.out.println("start1-1");
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }).start();

        new Thread(() -> {
            lock.lock();
            try {
                System.out.println("start2");
                condition.signal();
                System.out.println("start2-2");
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }).start();
    }
}
复制代码

img.png
从输出可以看出start1-1在最后输出,第一个线程在输出start1后就使用Conditionawait()方法将线程挂起,第二个线程输出start2后使用signal()唤醒 线程1,然后输出start2-2,最后输出线程1中的start1-1,到这里,我们就知道AQS的内部类ConditionObject的作用了。

关于AQS知识点,我们就分享到这里,AQS我们基本上不会直接去使用它,但是它对于我们来说却特别的重要,因为在JUC中,很多地方都是基于AQS来实现的,只有掌握了它, 我们在学习其他JUC的知识点的时候才会融会贯通,后面还会基于AQS实现一个自己的锁或者同步器,我是小四,我们下期见。

Guess you like

Origin juejin.im/post/7066767561530179620