java并发系列三(深入理解AQS和CAS)

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/lc138544/article/details/84496134

一,AQS详解

参考博文https://www.cnblogs.com/waterystone/p/4920797.html
在这里插入图片描述
首先,AQS维护了一个共享变量state,和一个FIFO队列。state的访问方式有三种:
getState()
setState()
compareAndSetState()
AQS有独占和共享这两种资源共享方式。
自定义同步器只需要实现共享资源的state的获取与释放即可。需要实现如下方法:
isHeldExclusively():该线程是否正在独占资源
tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。

源码详解!!!!!!
1,acquire方法
此方法是独占模式下线程获取state的顶层入口。源码如下

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

用到的函数如下
1,tryAcquire :尝试去获取state,如果成功则直接返回
2,addWaiter将该线程加入等待队列的尾部,并标记为独占模式
3,acquireQueued使线程在等待队列中获取资源,一直到获取到资源才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
4,selfInterrupt:进行自我中断

下面我们开始逐个方法分析
1,,tryAcquire 源码如下

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

是不是有点蒙蔽,为啥就单单只是抛了个异常。AQS毕竟只是一个框架,具体的实现还要靠自定义的同步器自己去设计。
这里我们简要分析下lock中非公平锁的tryAcquire实现,源码如下

final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

这段代码还是比较容易理解的。。。我就不多加赘述了。。。
2,addWaiter
源码如下

 private Node addWaiter(Node mode) {
        //构造结点
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        //找到等待队列的tail结点
        Node pred = tail;
        //如果不为空的话,就把node放在tail后面
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        //如果不存在tail节点执行enq()
        enq(node);
        return node;
    }
//这里进行了循环,如果此时存在了tail就执行同上一步骤的添加队尾操作,如果依然不存在,就把当前线程作为head结点。
 private Node enq(final Node node) {
        for (;;) {
            Node t = 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;
                }
            }
        }
    }

3,acquireQueued(这个函数相当关键)
上源码

  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 将p.next设为null,是为了方便回收以前的node结点,也就意味着之前拿完资源的结点出队了
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

shouldParkAfterFailedAcquire源码附上

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;//拿到前驱的状态
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        if (ws > 0) {
           * 如果前驱放弃了,那就一直往前找,直到找到最近一个正常等待的状态,并排在它的后边。
           * 注意:那些放弃的结点,由于被自己“加塞”到它们前边,它们相当于形成一个无引用链,稍后就会被保安大叔赶走了(GC回收)!
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
          //如果前驱正常,那就把前驱的状态设置成SIGNAL,告诉它拿完号后通知自己一下。
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

parkAndCheckInterrupt源码

private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);//调用park()使线程进入waiting状态
        return Thread.interrupted();//如果被唤醒,查看自己是不是被中断的
    }

acquireQueued函数流程总结如下
(1)结点进入队尾后,检查状态,找到安全休息点
(2)调用park()进入waiting状态,等待unpark()或interrupt()唤醒自己
(3)被唤醒后,看自己是不是有资格能拿到号。如果拿到,head指向当前结点,并返回从入队到拿到号的整个过程中是否被中断过;如果没拿到,继续流程1。)

分割线----------------------------
acquire总结,先附上一个流程图
在这里插入图片描述
现在我们重新梳理下
调用自定义同步器的tryAcquire()尝试直接去获取资源,如果成功则直接返回;
没成功,则addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;
acquireQueued()使线程在等待队列中休息,有机会时(轮到自己,会被unpark())会去尝试获取资源。获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上

接下来开始梳理release源码
上源码
release也是释放的顶层啦,这段代码还是蛮简单易懂的。其中tryRelease同上面讲的tryAcquire,没有什么具体的实现。自定义同步器的话要自己去实现,大家可以去参考ReentrantLock中的实现。

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;//找到头结点
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);//唤醒等待队列里的下一个线程
        return true;
    }
    return false;
}

unparkSuccessor源码附上

private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        Node s = node.next;//找到下一个等待唤醒的结点
        if (s == null || s.waitStatus > 0) {//s为空或者已取消(其实没太明白状态大于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);//唤醒
    }

下面来讲一下共享模式下资源的获取和释放
1,acquireShared
上源码先

 public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

这里tryAcquireShared()依然需要自定义同步器去实现。但是AQS已经把其返回值的语义定义好了:负值代表获取失败;0代表获取成功,但没有剩余资源;正数表示获取成功,还有剩余资源,其他线程还可以去获取。所以这里acquireShared()的流程就是:

tryAcquireShared()尝试获取资源,成功则直接返回;
失败则通过doAcquireShared()进入等待队列,直到获取到资源为止才返回。

doAcquireShared(int)
  此方法用于将当前线程加入等待队列尾部休息,直到其他线程释放资源唤醒自己,自己成功拿到相应量的资源后才返回。下面是doAcquireShared()的源码:
  
这段代码跟上面讲的acquireQueued的流程基本是一样的,区别之处在于setHeadAndPropagate,自己拿到资源后还要去唤醒其他线程。这才是共享

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);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

下面附上setHeadAndPropagate源码

private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; 
    setHead(node);//head指向自己
     //如果还有剩余量,继续唤醒下一个邻居线程
    if (propagate > 0 || h == null || h.waitStatus < 0) {
        Node s = node.next;
        if (s == null || s.isShared())
            doReleaseShared();
    }
}

接下来讲解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;
                unparkSuccessor(h);//唤醒后继
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;
        }
        if (h == head)// head发生变化
            break;
    }
}

二 CAS详解

CAS:compareAndSwamp,比较并交换
java.util.concurrent包中借助CAS实现了区别于synchronouse同步锁的一种乐观锁。
CAS有3个操作数,内存值V,预期值A,更新值B。只有当V=A时,才把V更新为B
非阻塞算法
一个线程的失败或者挂起不应该影响其他线程的失败或挂起的算法。
下面来看一下原子操作的++i是如何借助CAS实现的,上源码

public final int incrementAndGet() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return next;
    }
}
public final boolean compareAndSet(int expect, int update) {   
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

其中,compareAndSwapInt是借助C来调用CPU底层指令实现的。

CAS缺点
1,循环时间长,开销大。CAS自旋时,如果长时间不成功。就会带来非常大的开销。
2, ABA问题。因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。

展开阅读全文

没有更多推荐了,返回首页