多线程与高并发三:AQS底层源码分析及其实现类

文章目录

  • 1:AQS
    • 1.1AQS介绍
    • 1.2AQS源码分析
    • 1.3:如何利用 AQS 自定义一个互斥锁
  • 2:AQS的一些实现类
    • 2.1:Reentranlock
    • 2.2:CountDownLatch
    • 2.3:CycilcBarier
    • 2.4:Phase
    • 2.5:ReadWriteLock
    • 2.6:Semaphore
    • 2.7:Exchanger
    • 2.8:LockSupport

上篇说了CAS和volatile,这一篇主要介绍综合了CAS和volatile的AQS。

1:AQS

1.1AQS介绍

AQS是AbstractQueuedSynchronizer的简称。
有一个共享资源变量state:AQS state是一个volatile标志的Int整形数,支持多线程下的可见性。根据子类取不同的意义。
例如:
ReentranLock底层的state如果是1,表示当前线程已经获取锁;从1升到2,表示加了可重入锁,又加了一次;如果state是0,表示已经释放锁。
CountDownLatch底层的state表示需要countdown的次数。
跟随着state的还有一个FIFO等待队列,是一个双向链表,一个节点中表示一个线程,有前节点,后节点。多线程竞争state被阻塞会进入此队列,实现方式为CAS。当等待队列中的一个节点拿到了state的值,就相当于节点中的线程拿到了锁。上面解释了为什么AQS=CAS+Volatile
多线程与高并发三:AQS底层源码分析及其实现类_第1张图片

1.2AQS源码分析

具体的源码实现大家可以自行阅读,但是阅读源码是很辛苦的,一般遵循跑不起来不读,解决问题就好(目的性),一条线索到底,无关细节略过,一般不读静态,一般动态读法,读源码先读框架等。当然,参考网上的一些博客也是一种不错的方法,例如:
https://www.jianshu.com/p/0f876ead2846 -浅谈Java的AQS
下面是我对AQS的一些认识(只涉及独占式获取资源),如何获取锁:

//进入到ReentrantLock的内部类NonfairSync 中的lock()方法
       //  使用 CAS 来获取 state 资源,如果成功设置 1,代表 state 资源获取锁成功,
        // 此时记录下当前占用 state 的线程 setExclusiveOwnerThread(Thread.currentThread());
        // 如果 CAS 设置 state 为 1 失败(代表获取锁失败),则执行 acquire(1) 方法,再次尝试
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
//这里是获取锁的acquire(1)方法
//首先 调用 tryAcquire 尝试着获取 state,如果成功,则跳过后面的步骤。如果失败,则执行 //acquireQueued 将线程加入 CLH 等待队列中
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

首先尝试获取锁,进入tryAcquire(1)方法,进入到

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

继续跟,进入到
此段代码可知锁的获取主要分两种情况
1:state 为 0 时,代表锁已经被释放,可以去获取,于是使用 CAS 去重新获取锁资源,如果获取成功,则代表竞争锁成功,使用 setExclusiveOwnerThread(current) 记录下此时占有锁的线程,看到这里的 CAS,大家应该不难理解为啥当前实现是非公平锁了,因为队列中的线程与新线程都可以 CAS 获取锁啊,新来的线程不需要排队
2:如果 state 不为 0,代表之前已有线程占有了锁,如果此时的线程依然是之前占有锁的线程(current == getExclusiveOwnerThread() 为 true),代表此线程再一次占有了锁(可重入锁),此时更新 state,记录下锁被占有的次数(锁的重入次数),这里的 setState 方法不需要使用 CAS 更新,因为此时的锁就是当前线程占有的,其他线程没有机会进入这段代码执行。所以此时更新 state 是线程安全的

        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            // 如果 c 等于0,表示此时资源是空闲的(即锁是释放的),再用 CAS 获取锁
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // 此条件表示之前已有线程获得锁,且此线程再一次获得了锁,获取资源次数再加 1,
           // 这也映证了ReentrantLock 为可重入锁
            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;
        }
        

如果 tryAcquire(arg) 执行失败,代表获取锁失败,则执行 acquireQueued 方法,将线程加入 FIFO 等待队列.
首先查看addWaiter(Node.EXCLUSIVE) 将包含有当前线程的 Node 节点入队, Node.EXCLUSIVE 代表此结点为独占模式

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    //获取尾节点,如果尾节点不为空,采用CAS的方式将获取锁失败的线程入队
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 如果结点为空,执行 enq 方法
    enq(node);
    return node;
}

跟踪enq()方法,首先判断 tail 是否为空,如果为空说明 FIFO 队列的 head,tail 还未构建,此时先构建头结点,构建之后再用 CAS 的方式将此线程结点入队. head 结点为虚结点,它只代表当前有线程占用了 state,至于占用 state 的是哪个线程,其实是调用了上文的 setExclusiveOwnerThread(current) ,即记录在 exclusiveOwnerThread 属性里。

    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
             // 尾结点为空,说明 FIFO 队列未初始化,所以先初始化其头结点
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
            // 尾结点不为空,则将等待线程入队
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

下面问题来了,等待队列中的线程是如何获取锁的,AQS 对这种入队的线程采用的方式是让它们自旋来竞争锁。自旋一两次竞争不到锁后识趣地阻塞以等待前置节点释放锁后再来唤醒它。另外如果锁在自旋过程中被中断了,或者自旋超时了,应该处于「取消」状态。
首先来看一个 Node 结点的属性定义了

    static final class Node {
        //标识等待节点处于共享模式
        static final Node SHARED = new Node();
       //标识等待节点处于独占模式
        static final Node EXCLUSIVE = null;
        //由于超时或中断,节点已被取消
        static final int CANCELLED =  1;
      // 节点阻塞(park)必须在其前驱结点为 SIGNAL 的状态下才能进行,如果结点为 SIGNAL,
      //则其释放锁或取消后,可以通过 unpark 唤醒下一个节点,
        static final int SIGNAL    = -1;
      //表示线程在等待条件变量(先获取锁,加入到条件等待队列,然后释放锁,等待条件变量满足条件;
      //只有重新获取锁之后才能返回)
        static final int CONDITION = -2;
      //表示后续结点会传播唤醒的操作,共享模式下起作用
        static final int PROPAGATE = -3;
       //等待状态:对于condition节点,初始化为CONDITION;其它情况,默认为0,通过CAS操作原子更新
        volatile int waitStatus;
        volatile Node prev;
        volatile Node next;
        volatile Thread thread;
        Node nextWaiter;
//通过状态的定义,我们可以猜测一下 AQS 对线程自旋的处理:如果当前节点的上一个节点不为 head,
//且它的状态为 SIGNAL,则结点进入阻塞状态。

查看等待队列中尝试获取锁

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)) {
                // 将 head 结点指向当前节点,原 head 结点出队
                setHead(node);
                // 说明前继节点已经释放掉资源了,将其next置空,以方便虚拟机回收掉该前继节点
                p.next = null; // help GC
                // 标识获取资源成功
                failed = false;
                // 返回中断标记
                return interrupted;
            }
            // 若前继节点不是头结点,或者获取资源失败,
            // 则需要通过shouldParkAfterFailedAcquire函数
            // 判断是否需要阻塞该节点持有的线程
            // 若shouldParkAfterFailedAcquire函数返回true,
            // 则继续执行parkAndCheckInterrupt()函数,
            // 将该线程阻塞并检查是否可以被中断,若返回true,则将interrupted标志置于true
            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)
      // 1. 如果前置顶点的状态为 SIGNAL,表示当前节点可以阻塞了
            return true;
        if (ws > 0) {
     // 2. 移除取消状态的结点 
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
     // 3. 如果前置节点的 ws 不为 0,则其设置为 SIGNAL,
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

如何让线程阻塞,调用parkAndCheckInterrupt()方法

    private final boolean parkAndCheckInterrupt() {
        // 阻塞线程
        LockSupport.park(this);
        // 返回线程是否中断过,并且清除中断状态(在获得锁后会补一次中断)
        return Thread.interrupted();
    }

但为啥要判断线程是否中断过呢,因为如果线程在阻塞期间收到了中断,唤醒(转为运行态)获取锁后(acquireQueued 为 true)需要补一个中断

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        // 如果是因为中断唤醒的线程,获取锁后需要补一下中断
        selfInterrupt();
}

至此,获取锁的流程已经分析完毕,不过还有一个疑惑我们还没解开:前文不是说 Node 状态为取消状态会被取消吗,那 Node 什么时候会被设置为取消状态呢。
回头看cancelAcquire()方法

    private void cancelAcquire(Node node) {
       // 如果节点为空,直接返回
        if (node == null)
            return;
        node.thread = null;
      // 下面这步表示将 node 的 pre 指向之前第一个非取消状态的结点
      //(即跳过所有取消状态的结点),waitStatus > 0 表示当前结点状态为取消状态
        Node pred = node.prev;
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;
     // 获取经过过滤后的 pre 的 next 结点,这一步主要用在后面的 CAS 设置 pre 的 next 节点上
        Node predNext = pred.next;
        // 将当前结点设置为取消状态
        node.waitStatus = Node.CANCELLED;
// 如果当前取消结点为尾结点,使用 CAS 则将尾结点设置为其前驱节点,如果设置成功,
//则尾结点的 next 指针设置为空
    if (node == tail && compareAndSetTail(node, pred)) {
        if (node == tail && compareAndSetTail(node, pred)) {
            compareAndSetNext(pred, predNext, null);
        } else {
         // 这一步看得有点绕,我们想想,如果当前节点取消了,那是不是要把当前节点的前驱节点指向
         //当前节点的后继节点,但是我们之前也说了,要唤醒或阻塞结点,须在其前驱节点的状态为 
         //SIGNAL 的条件才能操作,所以在设置 pre 的 next 节点时要保证 pre 结点的状态为 SIGNAL
            int ws;
            if (pred != head &&
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                pred.thread != null) {
                Node next = node.next;
                if (next != null && next.waitStatus <= 0)
                    compareAndSetNext(pred, predNext, next);
            } else {
              // 如果 pre 为 head,或者  pre 的状态设置 SIGNAL 失败,则直接唤醒后继结点去竞争锁,
              //之前我们说过, SIGNAL 的结点取消(或释放锁)后可以唤醒后继结点
                unparkSuccessor(node);
            }
            node.next = node; // help GC
        }
    }

所释放:
不管是公平锁还是非公平锁,最终都是调的 AQS 的如下模板方法来释放锁

public final boolean release(int arg) {
    // 锁释放是否成功
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
      // 锁释放成功后该干嘛,显然是唤醒之后 head 之后节点,让它来竞争锁
     //1: 如果 h == null, 这有两种可能,一种是一个线程在竞争锁,现在它释放了,
     //当然没有所谓的唤醒后继节点,一种是其他线程正在运行竞争锁,只是还未初始化头节点,
     //既然其他线程正在运行,也就无需执行唤醒操作
     //2:如果 h != null 且 h.waitStatus == 0,说明 head 的后继节点正在自旋竞争锁,
     //也就是说线程是运行状态的,无需唤醒。
     //3:如果 h != null 且 h.waitStatus < 0, 此时 waitStatus 值可能为 SIGNAL,
     //或 PROPAGATE,这两种情况说明后继结点阻塞需要被唤醒
            unparkSuccessor(h);
        return true;
    }
    return false;
}

tryRelease 方法定义在了 AQS 的子类 Sync 方法里

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    // 只有持有锁的线程才能释放锁,所以如果当前锁不是持有锁的线程,则抛异常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    // 说明线程持有的锁全部释放了,需要释放 exclusiveOwnerThread 的持有线程
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

来看一下唤醒方法 unparkSuccessor

private void unparkSuccessor(Node node) {
    // 获取 head 的 waitStatus(假设其为 SIGNAL),并用 CAS 将其置为 0,为啥要做这一步呢,之前我们分析过多次,其实 waitStatus = SIGNAL(< -1)或 PROPAGATE(-·3) 只是一个标志,代表在此状态下,后继节点可以唤醒,既然正在唤醒后继节点,自然可以将其重置为 0,当然如果失败了也不影响其唤醒后继结点
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    // 以下操作为获取队列第一个非取消状态的结点,并将其唤醒
    Node s = node.next;
    // s 状态为非空,或者其为取消状态,说明 s 是无效节点,此时需要执行 if 里的逻辑
    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);
}

这里的寻找队列的第一个非取消状态的节点为啥要从后往前找呢,因为节点入队并不是原子操作,如下

    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;
    }

线程自旋时时是先执行 node.pre = pred, 然后再执行 pred.next = node,如果 unparkSuccessor 刚好在这两者之间执行,此时是找不到 head 的后继节点的,如下
多线程与高并发三:AQS底层源码分析及其实现类_第2张图片

1.3:如何利用 AQS 自定义一个互斥锁

AQS 通过提供 state 及 FIFO 队列的管理,为我们提供了一套通用的实现锁的底层方法,基本上定义一个锁,都是转为在其内部定义 AQS 的子类,调用 AQS 的底层方法来实现的,由于 AQS 在底层已经为了定义好了这些获取 state 及 FIFO 队列的管理工作,我们要实现一个锁就比较简单了,我们可以基于 AQS 来实现一个非可重入的互斥锁,如下所示

public class Mutex  {

    private Sync sync = new Sync();
    
    public void lock () {
        sync.acquire(1);
    }
    
    public void unlock () {
        sync.release(1);
    }

    private static class Sync extends AbstractQueuedSynchronizer {
        @Override
        protected boolean tryAcquire (int arg) {
            return compareAndSetState(0, 1);
        }

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

        @Override
        protected boolean isHeldExclusively () {
            return getState() == 1;
        }
    }
}

2:AQS的一些实现类

2.1:Reentranlock

可重入锁,独占锁:
synchronized方法的代替:Lock lock=new ReentrantLock(); lock.lock(); lock.unlock();Synchronized是自动解锁的,但Reentranlock却不是手动解锁的,要在finally块中解锁。同时:可以传递参数为true,公平锁,按照等待顺序执行,会检查等待队列是否为空。 可 以进行tryLock,synchronized拿不到锁会直接进入阻塞状态还可以lockinterupptibly(被打断)。

2.2:CountDownLatch

倒数的门栓:等着多少个线程结束才开始执行,latch.wait(); latch.countdown();

2.3:CycilcBarier

栅栏,达到多少线程数才开始发车

2.4:Phase

按照不同的阶段对线程开始执行

2.5:ReadWriteLock

读写锁,共享锁和排它锁。读锁,允许读,不 允许写;写锁,读写都不允许。

2.6:Semaphore

信号量,限流,必须先获得许可,允许多少个线程同时进行,比如车道和收费站

2.7:Exchanger

交换数据,线程一执行exchange方法阻塞,将值T1保存;线程二执行exchange方法,将值T2保存,进入阻塞;T1与T2交换值,两个线程继续往下跑。

2.8:LockSupport

锁支持,LockSupport.park();是当前线程阻塞 LockSupport.unpark();解封,使线程继续运行,onpack可以现在pack之前调用,用来代替wait和notify,更灵活,可以唤醒特定的线程。底层是是unsafe的park方法。

注意:本文仅代表菜鸟博主的个人观点,如果哪里不对或者路过技术大大有更好的想法,欢迎留言告知,分享和交流使我们进步,谢谢。

你可能感兴趣的:(JUC,java,多线程,并发编程)