AQS排他锁源码分析

介绍

说到java中的锁就会想到synchronized和lock,而说到lock一般就会想到ReentrantLock,而ReentrantLock的底层实现就是依赖于AbstractQueuedSynchronizer,也就是AQS。

AQS已经实现了底层的逻辑,对于想要自己实现锁,只需要继承AQS然后维护其state状态就可以了。

 

源码解析

状态解析

waitStatus状态解析:

CANCELLED(1):表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。

SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。

CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。

PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。

0:新结点入队时的默认状态。

获取锁

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&  // 尝试获取锁,实现由自己实现
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 新建一个Node,加入到等待队列
            selfInterrupt(); // 自我打断
    }

tryAcquire主要是尝试获取锁,在AQS中并没有实现,只是抛出了异常,这个是留给具体实现锁的类去做的,比如ReentrantLock等。

    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // 尝试快速加入queue,如果失败或者是还没初始化tail则使用enq方法
        Node pred = tail;
        if (pred != null) {
            // 将node的前一个节点设置成原来的tail
            node.prev = pred;
            // cas将此Node设置为tail
            if (compareAndSetTail(pred, node)) {
                // 将原来tail的下一个节点设置成此node
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }
    private Node enq(final Node node) {
        // 自旋
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                // 初始化head和tail,此时tail和head一致,继续自旋走else逻辑
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                // 上面已经把head和tail初始化完成
                // 将此节点的前一个节点设置成tail
                node.prev = t;
                // cas将此节点设置成新的tail
                if (compareAndSetTail(t, node)) {
                    // 将原来的tail节点的下一个节点指向新的tail节点
                    t.next = node;
                    return t;
                }
            }
        }
    }

这样就完成了新的Node加入到尾部的逻辑中了,但是仅仅这样还是不够。因为从此时,我们并没有看到线程的中断,那么它并没有阻塞。还有就是它中断之后该如何处理,如何唤醒等等的问题还没有处理,这些问题都在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
                    failed = false;
                    return interrupted;
                }
                // 校验前一个节点的waitStatus是不是single
                if (shouldParkAfterFailedAcquire(p, node) && 
                    parkAndCheckInterrupt()) // 线程park
                    interrupted = true;
            }
        } finally {
            if (failed) // 如果获取锁失败
                // 设置node的状态为取消,后续unparkSuccessor时候会进行指针调整和垃圾回收
                cancelAcquire(node);  
        }
    }
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        // 前一个节点的waitStatus是否是SINGAL如果是就返回,线程就会执行parkAndCheckInerrupt            
        // 进行park,等待被唤醒或者被打断
        if (ws == Node.SIGNAL)
            return true;
        if (ws > 0) {
            // 如果前一个节点的waitStatus状态是大于0,也就是CANCELED的,那么就将向前找到
            // 没有打断的Node,并且将此Node的前一个节点指向那个没有打断的Node,其下一个节点
            // 指向当前节点
            // 流程结束,到外层继续自旋获取锁,如果拿不到继续设置前一个节点的waitStatus
            /*
              这边是在cancelAcquire时候,当不是tail节点时,会有一些多余数据无法释放,在这边就会
              取消指针指向,从而垃圾回收
             */    
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            // 尝试去更改上一个节点的状态是Single,继续到外层自旋获取锁,如果拿不到继续设置前一个        
            // 节点的waitStatus,和上面 ws > 0 一致
            // 设置前一个节点的waitStatus为SINGAL成功之后,再次自旋到ws == Node.SIGNAL,然后
            // 线程就会执行parkAndCheckInerrupt,进行park
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
    private final boolean parkAndCheckInterrupt() {
        // 线程此时park,等待唤醒
        LockSupport.park(this);
        // 线程是否被打断过
        return Thread.interrupted();
    }

其实到这边正常流程就已经走完了,可以大概梳理下,一个新的线程尝试获取锁时,如果获取失败,那么就要进入队列中;在进入队列的过程中,先加入到队列的尾部并设置成tail,并将指针改变。

然后在acquireQueued中继续尝试获取锁:

1. 如果前面一个节点是head,那么继续尝试获取锁,可以想象一下,这边为什么是前一个指针是head时候才获取锁,因为这是一个双向链表,每次unpark时候都是向后unpark。此时这个线程既然能运行,那就有可能是head把它unpark的,说明head可能已经释放了,所以这边再次尝试获取下锁。

2.如果获取不到,那么就检查前一个节点的waitStatus是不是SINGAL,如果是则线程park。如果不是则尝试把前一个节点设置成SINGLE,在每次设置时候会尝试继续获取锁,因为可能这段时间他的前一个节点变成了head,自己可以获取锁了。

3. 前一个节点设置完waitStatus为SINGAL之后,此节点线程就可以park了,等待别的节点唤醒,或者被打断。

整理玩了正常的获得锁的流程,看下异常或者取消的时候的流程。

    private void cancelAcquire(Node node) {
        // Ignore if node doesn't exist
        if (node == null)
            return;

        node.thread = null;

        Node pred = node.prev;
        // 将当前节点指向成没有取消过的前任节点
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;
        // 前一个节点的下个节点
        Node predNext = pred.next;
        // 设置当前节点的状态为CANCELLED
        node.waitStatus = Node.CANCELLED;
        // 如果当前node是tail,那么设置成前一个node为tail
        if (node == tail && compareAndSetTail(node, pred)) {
            // 设置新的tail的next为null
            // 那么当前的node就没有被引用了,就会被下一次垃圾回收回收掉
            compareAndSetNext(pred, predNext, null);
        } else {
            int ws;
            // 如果当前节点前一个节点不是head
            if (pred != head &&
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                pred.thread != null) {
                // 当前节点的下一个节点
                Node next = node.next;
                // 如果next是空说明到了tail,或者后面的被取消了也都没有影响               
                // 在shouldParkAfterFailedAcquire那里会进行调整Node的指针
                if (next != null && next.waitStatus <= 0)
                    // 更新前一个节点的下一个节点为当前节点的下一个节点
                    compareAndSetNext(pred, predNext, next);
            } else {
                // 如果当前是节点的上一个节点是head,尝试唤醒下一个没有取消的线程
                unparkSuccessor(node);
            }

            node.next = node; // help GC
        }
    }
    private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
        // 找到下一个节点,如果下一个节点是空,或者是取消的,就从后向前查找节点
        // 其实这边有两个问题?
        // 1. 为什么是从后向前去唤醒
        // 2. 如果后面的被唤醒了,那么公平锁岂不是不公平了?
        // 后来仔细又阅读了下源码发现了答案
        // 1. 为什么要从后向前去唤醒呢?其实我个人认为可能是因为addWaiter时候设置tail时候不是            
        // 原子性的,先是node.prev = tail,然后设置新的tail,最后才把之前的tail的next更新到新的
        // tail上。这就会无法找到新加的那个node
        // 2. 后面的线程会唤醒,会不会导致公平锁不公平。答案是不会为,因为这时候如果被唤醒的话,
        // 此时继续接着去走parkAndCheckInterrupt,然后再去自旋获取锁,如果它前一个节点不是head,
        // 那么它将继续park,并不会拿到锁
        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);
    }

附上cancelAcquire后节点的变更。

当节点是tail时:

当节点的前一个节点不是head时,在cancelAcquire后,Node2的前一个正常节点的next会变成Node3。当Node3被唤醒时,会尝试获取锁,如果前面节点不是head的时候,会继续执行shouldParkAfterFailedAcquired,然后将前一个节点设置成Node1节点。

 

当前一个节点是head时,直接唤醒下一个线程,当前node的waitStatus设置成CANCELLED,将下一个Node线程唤醒。下一个线程在唤醒之后尝试获取锁,由于前面一个节点不是head,所以继续执行shouldParkAfterFailedAcquired。

 

释放锁

前面分析了获取锁,后面释放锁就变的很简单了。

    public final boolean release(int arg) {
        // 尝试释放锁,由继承后实现,也就是维护state
        // 例如在ReentrantLock中state = 0 就可以释放锁
        if (tryRelease(arg)) {
            Node h = head;
            // 唤醒下一个节点
            // 如果waitStatus == 0,那么可能是新加如进来没有后续节点或者是在unparkSuccessor时修        
            // 改成了0,不需要再去通知
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

 

至此AQS的排他锁的获取和释放就分析完了,由于能力有限而且才刚开始去读AQS源码,可能其中有些问题欢迎指正。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值