2022-01-28 aqs逐行分析

aqs总结

ReentrantLock实现Lock的lock()接口
ReentrantLock 的实现是
sync.acquire(1)
这个sync 是ReentrantLock的一个属性
Sync 又 继承自 AQS 所以sync是一个队列器

所以调用 lock()
这个方法
ReentrantLock reentrantLock = new ReentrantLock(true);
会先调用 reentrantLock的lock方法

public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
}

在构造函数中 如果选择了true 那么这个 sync属性 就是一个公平锁的sqs队列器
static final class FairSync extends Sync {...}

public void lock() {
        //acquire 这个1 是为了可重入和初始赋值用的,后边会看到
        sync.acquire(1);
}

这里就是 对一个公平锁的队列器提出申请

acquire()方法 进入到aqs这个抽象类中

aqs一共有三个属性 它的父类(AbstractOwnableSynchronizer)还有一个属性
//阻塞队列的头 which node 占了head 谁相当于获取到了锁
private transient volatile Node head;
//阻塞队列的尾巴 那个node 占了tail 相当于刚进队列
private transient volatile Node tail;
//队列的同步状态 0是没人占 1是有人占 大于1 就是可重入的意思 初始化的时候这里是null
private volatile int state;
//持有锁的线程 当node占了head之后 node里面的 属性thread 和exclusiveOwnerThread 的线程是一个
private transient Thread exclusiveOwnerThread;

Node 是aqs 中的一个静态内部类 同样有四个属性
//这个相当于 node的状态 等于1时 (CANCEL)代表线程取消了等待
// 正常情况刚进队列为0 (好像是)
// 等于-1 (SIGNAL)线程的后继 需要唤醒?
volatile int waitStatus;
volatile Node prev;
volatile Node next;
// 线程自己
volatile Thread thread;
//线程的后继节点?如果当前线程执行完毕后该去唤醒谁
Node nextWaiter;

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

正常情况下 应该是拿到锁 跳出这个方法继续执行 lock()后边的代码
从这个方法看不出来有任何获取的动作
tryAcquire(arg) -- 尝试获取 如果返回true 那么判断整体为false 跳出方法继续执行
如果返回是false 就会走 acquireQueued(addWaiter(Node.EXCLUSIVE), arg) --把当前线程变成node 放到队列中等待
比较疑惑的是 锁的获取和 进入阻塞队列都没有看出来 它们具体的运行在这两个方法中,这里都简化写了 初看有些难以理解

static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;
        @ReservedStackAccess
        protected final boolean tryAcquire(int acquires) {
            //获取当前线程
            final Thread current = Thread.currentThread();
            //获取队列当前状态
            int c = getState();
            //当前队列没有人占 如果是刚初始化 它可能是null 这是整一个方法 也会返回null
            if (c == 0) {
                //如果状态为0 那么表示 可以获取锁,但是考虑到存在竞争这个时候还要看看队列里有没有其他的线程在等待
                //如果有 true 那么方法直接返回false 在上一个方法中!tryAcquire()方法返回的就是true 这样就进入第二个判断 没有返回false 证明它是第一个 进入第二个判断
                if (!hasQueuedPredecessors() &&
                    //cas对state进行更改 这时候依然存在竞争 成功返回true 那么可以将队列器的eot属性进行赋值表示 锁已经是当前线程的了
                    //这时候返回true 在上一个方法中 !tryAcquire(...)这里 第一个判断就是false 这样层层返回 执行lock()后边的方法
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //如果进入这个else那么证明当前锁被其他线程拿到了
            //先看看这个线程是不是自己 因为存在可重入的问题
            //如果是自己那么证明是可重入的需要给状态加一 多次进入会不会溢出?如果过大了 可能就会抛出异常 提示也是这么写的
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }

//检查 还有没有其他线程在队列中等待
public final boolean hasQueuedPredecessors() {
        Node h, s;
        //如果head为null 证明队列里没有等待的线程直接返回false 上一步的!hasQueuedProdecessors就是true 可以继续cas了
        if ((h = head) != null) {
            //如果head的后继是不是null进入下一个if 如果后继是null 或者前一个节点(就是head吧)这个时候取消了  只有取消的waitStatus才是大于0
            //里边这个属实看不懂了 head<-->node1<-->node2<-->node3(tail) 不过从循环的判断来说大致的意思应该是看看队列中的node 有没有有效的 等待的线程
            //因为 循环里面排除了cancel(等于1)的情况
            if ((s = h.next) == null || s.waitStatus > 0) {
                s = null; // traverse in case of concurrent cancellation
                for (Node p = tail; p != h && p != null; p = p.prev) {
                    if (p.waitStatus <= 0)
                        s = p;
                }
            }
            //如果后继不是null 并且后继node的thread属性不是这个线程那么返回true 一直往上传 可以看到进入到了acquireQueued方法 就是将当前线程放到队列中
            if (s != null && s.thread != Thread.currentThread())
                return true;
        }
        return false;
    }

此时 返回false证明队列中没有其他线程 可以cas 返回true 证明队列中有 有效的等待线程 那么最初的!tryAcquire()就是false 程序下一步开始运行 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
进入队列

进入队列的第一步就是先把当前线程构建成一个node 进行入队 此时又回到了 aqs这个抽象类

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

    //构建node 并且把node放到队列队尾
    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;
            //如果tail不是null 证明tail有值 node节点可以添加上去 让node的前驱指向tail
            //castail 成功后将上一个节点的后继指向 当前节点 当前节点这时候也是tail
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }
    //自旋方式入队

    //有两种可能 进入到这个方法中 上一步中第一个if 也就是 tail为null 这种情况下 走下面循环第一个if 
    //第二种可能是如果在上一步cas tail的时候竞争失败了 那么反复自旋入队
    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                //如果能明白初始化时state是null 然后必然会在第一个线程进来的时候走到这里 cas初始化head 然后tail = head
                //这个地方比较难理解 主要是调用链太长了可能记不住了 并没有什么逻辑上难的
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                //可以看到这里的方法和上一步类似
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }   

之后进入到acquireQueued方法中 此时 这个线程已经在阻塞队列里面了 然后线程在这个方法中会被挂起 等待唤醒

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                //先找出node节点的前驱节点
                final Node p = node.predecessor();
                //如果前驱节点是头结点 并且 能够获取获取到锁那么开始set 这个node变为头结点 此时返回interrupted 为false 
                //回到第一步 acquire方法 就会跳过selfInterrupt方法 之后执行lock方法后边的代码
                //如果不是head
                //如果没有竞争上 比如head里面那个node还在运行没有释放 或者非公平锁里面也会有入队的线程一上来就竞争一下
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //到这里 就是要把线程挂起了
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
                    //注意这里没有return
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

这个方法是 一直搜索前驱 找到一个前驱是-1的节点 只有前驱节点是-1的节点才可以唤醒它的后续节点

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        //判断前驱节点是不是-1
        if (ws == Node.SIGNAL)
            return true;
        //如果不是-1 大于0的 目前我知道的就是1 代表cancel
        if (ws > 0) {
        //循环向前找 直到找到-1的节点
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            //把前驱节点的状态设置为-1
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        //这里返回false没关系 上一步中if是个循环会再一次进入到这个里面 然后下次进入到这里时 前驱就是-1了,最终会返回true
        return false;
    }

线程就在这里停住了 等待唤醒

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

这里要说名一下 interrupt这几个方法
interrupt()设置中断标志 只是设置并不代表给线程stop了
interrupted()这个方法是看看中断标志 然后给清楚了 如果中断过那么第一次返回true 第二次再调用返回false
这里如果正常点没有被中断 那么最后会返回的是false 这里返回了false 然后继续循环这个线程当head然后 返回interrupted 这个标志->false
到了最外边 acquire方法 就会跳过selfInterrupt这个 然后继续执行lock

还有一种可能如果被中断了 就会返回true 最后执行一下selfInterrupt这个方法
这个方法

    static void selfInterrupt() {
        Thread.currentThread().interrupt();
    }

好像也没有什么特别的就是设置一个中断 然后回到lock之后继续走代码
这里也是我比较疑惑的地方 结合还有一个lockInterruptibly这个锁 ReentrantLock 这个锁如果想要响应中断需要用后者创建

    public final void acquireInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (!tryAcquire(arg))
            doAcquireInterruptibly(arg);
    }

可以看到它在外边catch了几个中断的异常 这样业务代码里面再catch就可以检测到中断了

unlock()方法
//这里的1 相当于-1

    public void unlock() {
        sync.release(1);
    }


//release 方法就是释放的方法
    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            //如果释放成功了 head现在还是它自己 肯定不是null 并且线程它自己的状态也一定不是0 
            if (h != null && h.waitStatus != 0)
                //唤醒后继节点
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

        protected final boolean tryRelease(int releases) {
            //状态先-1
            int c = getState() - releases;
            //肯定得是这个线程
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            //如果是0 代表不是可重入的 并且把队列器的eot变成null
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            //比如现在是3变成2 证明可重入还得释放两次 free此时是false 回到tryRelease是false 继续返回false
            //然后release也是返回了false 但是没啥用好像 没人继续判断了
            setState(c);
            return free;
        }

到这里就是唤醒的过程

    private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        //如果状态小于0比如-1 那么cas把状态改为0
        if (ws < 0)
            node.compareAndSetWaitStatus(ws, 0);
        Node s = node.next;
        //这里s就是目前head的后继 普通case下 s不是null 然后如果它的状态是1 就是被取消了 后续就是遍历找出来下一个不是1的node 设置为s
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node p = tail; p != node && p != null; p = p.prev)
                if (p.waitStatus <= 0)
                    s = p;
        }
        //到这里 s肯定不是null了 就把这个唤醒
        if (s != null)
            LockSupport.unpark(s.thread);
    }

这之后是最麻烦的的我感觉 唤醒之后这个后继节点从这里开始执行
acquireQueued->parkAndCheckInterrupt 如果这里面没有中断 那就返回false 然后继续走上边的循环
又一次进来执行

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;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

head这个时候还是刚才那个执行unlock的线程node p是被唤醒的线程的前驱 肯定是head
然后争锁
如果争锁 成功 那么head就会被新node取代了 然后interrupted=true 返回

protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            //这里肯定是0对吧 因为之前cas设置为0了
            if (c == 0) {
                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");
                setState(nextc);
                return true;
            }
            return false;
        }
    }

然后就会进入到has这个方法里面 你说说这个has 和唤醒有啥关系 难理解 但是确实是里面判断了

public final boolean hasQueuedPredecessors() {
        Node h, s;
        //head不是null 对的进入 
        if ((h = head) != null) {
            //s是head的后继 上边已经很确定了它不是null
            if ((s = h.next) == null || s.waitStatus > 0) {
                s = null; // traverse in case of concurrent cancellation
                for (Node p = tail; p != h && p != null; p = p.prev) {
                    if (p.waitStatus <= 0)
                        s = p;
                }
            }
            //进到这个if s不是null s的线程是当前线程 也不对
            if (s != null && s.thread != Thread.currentThread())
                return true;
        }
        //所以判断了一圈 最后还是走了这个false 我觉得比较恶心的就是上边这几个判断和唤醒有啥关系没有!但确实生效了
        return false;
    }

返回false之后!has方法为true 然后设置状态为0 设置eot为这个被唤醒的线程返回true
在acquireQueued方法里面继续执行把被唤醒的线程设置成头结点 然后把上一个线程 的后继设置为null 返回interrupted 如果没有被中断 这个时候这个标志是false

false了之后不会执行selfInterrupt方法 继续执行lock后边的业务代码

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,701评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,649评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,037评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,994评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,018评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,796评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,481评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,370评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,868评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,014评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,153评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,832评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,494评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,039评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,156评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,437评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,131评论 2 356

推荐阅读更多精彩内容