jdk锁知识(五)—— AQS

本文深入探讨了AQS(AbstractQueuedSynchronizer)中的排它锁和共享锁的获取及释放机制。详细分析了addWaiter、acquire、release等关键方法,阐述了节点入队、线程挂起、锁状态传播等过程,同时解释了中断响应和锁状态的检查。通过对源码的解读,帮助读者理解AQS在并发控制中的核心逻辑。
摘要由CSDN通过智能技术生成

排它锁获取(这里留意一下addWaiter方法,之所以有尾部遍历,就是为了和这块代码互补):

public final void acquire(int arg) {
   //1、先尝试获取锁,成功则直接返回,失败再继续后续流程 (这个方法由具体的锁类实现)
   //2、以排他模式创建新节点,并放在同步队列尾部,随后尝试竞争锁或挂起
   //3、尝试获取锁失败,放到同步队列再次竞争锁或挂起失败,则设置当前线程中断
   if (!tryAcquire(arg) &&
      acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
      selfInterrupt();
}

private Node addWaiter(Node mode) {
        //1、新建节点
        Node node = new Node(Thread.currentThread(), mode);
        //2、获取同步队列尾部节点,如果尾部节点不为空,则以CAS方式将新建的节点添加到队列尾部
        Node pred = tail;
        if (pred != null) {
            //2.1、设置新节点前驱为尾节点
            node.prev = pred;
            // 2.2、CAS方式将尾节点next索引指向新节点,如果失败,此时只有新节点指向尾节点,而尾节点没指向新节点,所以遍历队列时一般从后向前遍历,以防止此情况出现。
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        //3、如果同步队列尾部节点为空,或CAS设置尾部引用失败,则调用循环入队直至成功的方法
        enq(node);
        return node;
    }

private Node enq(final Node node) {
        for (;;) {
            //1、获取同步队列尾部节点,如果为空,则初始化创建空节点,并将头尾引用指向它
            Node t = tail;
            if (t == null) { 
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                //2、尾部引用不为空,则for循环中以CAS方式循环放置节点到队列尾部
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

至此,AQS中获取锁的地一小步入队已经完成,这里需要留意的就是addWaiter中CAS添加失败与尾部遍历的关系。节点入队之后,下面来看一下如何获取锁:

这里需要留意下挂起的代码,要清楚挂起和中断的区别,中断是设置中断位,线程可以继续执行,而挂起则是线程直接不执行,大家也可以自己测试一下,下面获取锁的流程大致是在一个死循环中重复判断首节点是否头节点,然后抢占锁。抢占失败则判断是否满足挂起条件,满足则挂起当前线程(挂起的条件是前驱节点为-1状态,前驱为0或者-3时则尝试用CAS方式修改为-1,因为在整个同步队列中首节点的后继节点会被挂起,其它非取消态节点则会循环自选监听前驱节点状态(这块网上有纯理论文章说所有节点都会阻塞挂起,这个我在源码里没看出来,有知道的博友希望能告知一波)):

final boolean acquireQueued(final Node node, int arg) {
        //1、设置当前获取是否失败的标志位,仅当获取到锁时才设置为false
        boolean failed = true;
        try {
            //2、中断标志位,用于挂起恢复后判断当前线程是否中断
            boolean interrupted = false;
            //3、for循环获取锁直至成功或者线程挂起
            for (;;) {
                final Node p = node.predecessor();
                //3.1、当前节点的前驱节点如果是头节点,则尝试获取锁
                if (p == head && tryAcquire(arg)) {
                    //3.1.1、获取锁成功则将当前节点设置为头节点,并设置其next索引为空以及失败标识符
                    setHead(node);
                    p.next = null; 
                    failed = false;
                    //3.1.2 返回中断标识,便于上一级方法中是否要执行中断方法
                    return interrupted;
                }
                //3.2、如果当前节点前驱节点不是头节点或者获取锁失败,则判断是否要挂起当前线程
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            //4、如果获取锁异常失败,则取消当前节点竞争锁
            if (failed)
                cancelAcquire(node);
        }
    }

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        //1、获取前驱节点状态,如果为signal,则可以挂起当前节点,因为signal状态节点释放资源时会唤醒后继节点。
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            return true;
        //2、如果前驱节点为取消状态,则跳过这些取消节点,直至前驱节点是非取消态
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            //3、同步队列状态没有-2,前面又有-1和1的逻辑拦截,所以到这里节点状态为0或-3.他们的执行需要前驱节点为signal,所以以CAS的方式去尝试设置前驱节点为-1
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        //4、前驱节点不是signal状态,都不能挂起线程
        return false;
    }

private final boolean parkAndCheckInterrupt() {
        //1、这个方法会挂起线程,即线程不再执行,等待唤醒
        LockSupport.park(this);
        //2、挂起恢复后,返回线程的中断状态
        return Thread.interrupted();
    }

private void cancelAcquire(Node node) {
        //1、如果取消节点不存在,则直接返回
        if (node == null)
            return;
        //2、节点内线程引用设置为空,便于GC
        node.thread = null;
        //3、跨过当前节点前取消状态的节点
        Node pred = node.prev;
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;
        //4、获取非取消态前驱结点的后继节点信息
        Node predNext = pred.next;
        //5、设置当前节点为取消态
        node.waitStatus = Node.CANCELLED;
        //6、如果当前节点为尾部节点,则将非取消态的前驱节点设置为尾部节点
        if (node == tail && compareAndSetTail(node, pred)) {
            //6.1、将尾部节点的后继引用设置为空
            compareAndSetNext(pred, predNext, null);
        } else {
        //7、如果当前节点非尾部节点或CAS设置尾部节点失败
            int ws;
            //7.1、如果前驱节点不是头节点 且 前驱节点状态为-1或前驱节点不是-1但CAS设置-1成功 且前驱节点线程非空,则表明此时线程处于挂起状态,所以直接修改后继链接就行。
            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 {
            //7.2、如果前驱节点为头节点或者节点状态不是-1,则唤醒后继线程(这里有两种情况,一个是前驱节点是头节点,此时唤醒的目的是单纯的竞争锁,而如果前驱节点不是头节点但状态为-3(这里只有可能是-3,因为-1已经被过滤,-2不在同步队列),此时唤醒则是尝试共享锁的获取传播)
                unparkSuccessor(node);
            }
            //7.3、设置取消节点自引用
            node.next = node; // help GC
        }
    }

排他锁释放:

public final boolean release(int arg) {
        //1、尝试释放锁,tryRelease由子类实现
        if (tryRelease(arg)) {
            //1.1、释放锁成功,则唤醒头节点的后续节点
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

private void unparkSuccessor(Node node) {
        //1、唤醒后继节点前,将当前节点状态设置为0初始态
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
        //2、查找不为空或不为取消态的节点,这里遍历查找的时候是从后向前查的,原因在addWaiter源码里。
        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;
        }
        //3、如果节点不为空则唤醒
        if (s != null)
            LockSupport.unpark(s.thread);
    }

至此,排它锁的获取释放已经查看完毕,比较难懂的地方一个是addwaiter方法中引起的节点尾部遍历;还有一个就是线程的挂起,是只有首节点的后继节点挂起还是所有节点都会挂起,这块我的理解在源码解析了(如果有不对,特希望大佬能解惑!)。

共享锁的获取:

共享锁的获取大致与排它锁相同,但是排它锁不会传播锁状态,共享锁会共享给后继线程,这块要留意看一下:

public final void acquireShared(int arg) {
        //1、尝试获取共享锁,tryAcquireShared由子类实现
        if (tryAcquireShared(arg) < 0)
            //2、获取失败则进入公平竞争环节
            doAcquireShared(arg);
    }

private void doAcquireShared(int arg) {
        //1、创建共享模式节点,并放到队列尾部
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                //2、获取当前节点的前驱节点
                final Node p = node.predecessor();
                //3、如果前驱节点为头节点,则进入锁获取环节
                if (p == head) {
                    //3.1、再次尝试获取共享锁,成功则进入共享锁的传播流程(这是与排它锁获取最不同的地方)
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        //3.2、设置头节点并传播锁状态
                        setHeadAndPropagate(node, r);
                        //3.3、设置后继节点为空便于GC
                        p.next = null; 
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                //4、如果前驱节点不是头节点,则判断是否需要中断当前节点(通过&&的截断功能,判断当需要挂起时再执行后续的挂起方法)
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    //4.1、获取当前节点的中断状态,主要是parkAndCheckInterrupt的返回值即当前线程的中断状态
                    interrupted = true;
            }
        } finally {
            //5、如果获取锁的过程中出现异常,则取消锁的获取
            if (failed)
                cancelAcquire(node);
        }
    }

private void setHeadAndPropagate(Node node, int propagate) {
        //1、记录旧的头节点便于后续的检查使用
        Node h = head; 
        //2、设置头节点
        setHead(node);
        //3、如果传播标识大于0、或头节点为空、或头节点状态为-3传播态、或重新获取头节点后 头节点为空或状态为-3传播态,表明可以进入共享锁传播流程
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            //3.1、当前节点如果不为空,且节点类型为共享节点,则进入锁释放共享流程(锁只有一个,如果要共享,获取锁的节点要先释放)
            Node s = node.next;
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }

private void setHead(Node node) {
        head = node;
        //1、因为头节点线程已经获取了同步状态,处于执行后状态,所以这里将节点内的线程引用设置为null,便于后续垃圾回收
        node.thread = null;
        node.prev = null;
    }

private void doReleaseShared() {
        //1、for循环释放头节点为共享模式的节点
        for (;;) {
            //1.1、获取头节点,如果头节点不为空且不是尾节点,则进入节点状态修改流程
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                //1.1.1、如果头节点还是-1状态,表明其后继节点还没有被唤醒,所以这里先修改节点状态,成功后唤醒后继节点运行,此时头节点也会改变。这样就可以配合下面1.2的逻辑完成锁的传播
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);
                }
                //1.1.2、如果头节点不是-1,而是0,那么说明后继节点已经拿到锁运行,表明当前节点实现了锁的传播,所以这里修改节点状态为-3.
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            //1.2、如果循环过程中头节点没变化,表明共享锁传播结束,所以跳出循环(如果共享锁正常传播,1.1.1流程中会更改头节点状态,这样在共享锁没传播结束前,不会跳出循环)
            if (h == head)                   
                break;
        }
    }

注意:锁传播时会判断后继节点是否为共享态节点,如果当前节点的后一个节点为共享模式节点会传播锁,如果不是共享模式节点则不会传播。

共享锁的获取基本看完了,下面来看一下共享锁的释放:

public final boolean releaseShared(int arg) {
        //1、先尝试释放锁,成功后进入锁传播释放流程,tryReleaseShared由子类实现,doReleaseShared前面讲过,这里不再叙述
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

至此,排它锁的获取释放,共享锁的获取释放已经叙述完毕。里面有很多难懂的点,我大多再源码注解中直接进行了叙述,如果有疑惑或者我有不对的地方,欢迎大家留言交流。

另外排他锁和共享锁的获取时还有一种中断响应的方式获取,这个主要是在获取过程中检查线程是否中断,中断则抛异常,限于篇幅原因,此处就不进行讲解了。有兴趣可以自行查阅。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值