Java并发AQS源码解析

前言

原文地址Java并发之AQS详解 - waterystone - 博客园 (cnblogs.com)

根据这篇文章摘取了部分进行完善,加入自己理解

AQS是什么?

AQS全称AbstractQueueSynchronizer,翻译为:抽象队列同步器,它的作用主要用来构造锁和同步器。ReentranLock与Semaphore都是基于AQS实现。

AQS原理

AQS的核心思想是如果被请求的共享资源空闲,那么将当前请求资源的线程设置为有效的工作线程,并且为共享资源设置为锁定状态。如果被请求的共享资源被占,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制。这个机制AQS是用CLH队列锁(CLH是一个虚拟的双向队列,只存在节点Node之间的关联关系)实现的,即将暂时获取不到锁的线程加入到队列当中。一个Node就表示一个线程,保存了线程引用,当前节点在队列中的状态,前驱节点,后继节点。

CLH队列结构如下所示(两者是同一个东西)

同时AQS还维护一个volatile int state变量用来表示资源的同步状态。空闲资源的state为0,当有线程执行lock()方法时state++,其他线程再去执行lock方法发现state不为0并且当前加锁的线程不是自己时就会阻塞。由于ReentrantLock是可重入锁,因此同一线程对同一资源多次lock时,state也会进行多次自增,释放锁时state--

AQS从资源访问方式可以划分为两种:独占(一个线程访问)和共享(多线程可以同时执行)

AQS源码分析

CLH队列锁AQS已经帮我们实现好了,在查看AQS的实现代码之前,我们先了解Node中维护的节点状态(waitStatus)的值都代表什么意思

  • 1:表示当前节点已经取消调度,timeout或是被中断,进入该状态的节点不会再变化
  • -1:表示后继节点在等待当前节点唤醒,后句节点入队后,就将前继节点状态修改为-1。
  • -2:表示节点等待在Condition上,当其他线程调用Condition的signal方法后,该状态的节点从等待队列转移到同步队列,等待获取同步锁。
  • -3:共享模式下前驱节点不仅会唤醒后继节点,同时也会唤醒后继的后继节点。
  • 0:新节点入队时的默认状态

接下来我们就开始分析实现源码,以ReentrantLock的非公平锁为例。

final void lock() {
    // CAS将state从0设置为1
    if (compareAndSetState(0, 1))
        // 设置资源的持有线程为当前线程
        setExclusiveOwnerThread(Thread.currentThread());
    else
        // 如果加锁失败,那么执行acquire
        acquire(1);
}

if块中的代码很好理解,接下来去查看acquire方法做了什么

public final void acquire(int arg) {
    // 尝试去加锁,具体加锁逻辑由不同的同步器去实现
    if (!tryAcquire(arg) &&
        // 尝试去加入等待队列
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
  • tryAcquire():尝试再次获取锁,如果成功直接返回,这也是非公平锁的体现,每次新加入的线程在加入队列前都有一次再次获取锁的机会。
  • addWaiter()将线程加入等待队列的队尾,并标记为独占模式
  • acquireQueued()使线程阻塞在等待队列中获取资源,一直获取到资源后才返回,期间如果被打断则返回ture

需要注意的是,如果线程在等待期间被中断是不会有任何影响的,当线程获取到资源后发现自己被打断才会去执行selfInterrupt方法。

private Node addWaiter(Node mode) {
    // 将当前线程构造为Node对象,mode有两种值:独占与共享
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    // 如果尾节点不为空则直接将当前节点添加到尾节点之后
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 如果不存在尾节点或是将当前节点设为尾节点失败
    enq(node);
    return node;
}
private Node enq(final Node node) {
    // 自选的方式来保证在多线程下节点一定添加成功
    for (;;) {
        Node t = tail;
        // 如果是尾节点为空的情况。这种情况将当前节点设置为头节点即可
        if (t == null) { 
            if (compareAndSetHead(new Node()))
                tail = head;
        } else { // 如果是设置头节点失败或是设置尾节点失败处理逻辑 
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

以上两个方式就是将未获得锁的线程放入等待队列尾部的实现代码,放入队列尾部之后,我们应该让线程进入等待状态,等待其他线程唤醒自己,具体实现代码如下

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;
            }
            // 如果没有拿到资源,那么就应该执行park进入等待状态
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;// 如果被打断则修改打断标志
        }
    } finally {
        // 如果没有获取到资源,则通过该方法修改线程状态以及修改队列连接状态
        if (failed)
            cancelAcquire(node);
    }
}

接下来去查看线程如何进入等待状态

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // 得到前驱节点的线程状态
    int ws = pred.waitStatus;
    // 如果前驱节点状态为-1,则直接返回
    if (ws == Node.SIGNAL)
        return true;
    // 前驱节点只要大于0说明都不会再参与资源获取,自然没有唤醒下一个线程的义务
    if (ws > 0) {
        // 循环获取前面节点线程状态小于0的节点
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        // 将获取的节点后驱节点设置为当前节点
        pred.next = node;
    } else {
        // 如果前驱节点为0,那么将0设置为-1
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}
private final boolean parkAndCheckInterrupt() {
    // 执行park进行等待
    LockSupport.park(this);
    // 如果被唤醒则判断自己是否被打断
    return Thread.interrupted();
}

经过这两个方法,只要线程不被打断那么通过自旋一定会获取到资源。

private void cancelAcquire(Node node) {
    if (node == null)
        return;
    // 清除node信息,代表当前节点无效
    node.thread = null;

    Node pred = node.prev;
    // 找到前驱节点中第一个等待状态还有效的
    while (pred.waitStatus > 0)
        node.prev = pred = pred.prev;
    // 获取有效前驱的后继节点(已经无效了),后续需要修改有效前驱的后续节点为null
    Node predNext = pred.next;
    // 修改当前节点的等待状态为打断或超时
    node.waitStatus = Node.CANCELLED;

    // 如果当前节点为尾节点,那么将前驱节点设为尾节点
    if (node == tail && compareAndSetTail(node, pred)) {
        // 清空无效节点
        compareAndSetNext(pred, predNext, null);
    } else {
        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 {
            unparkSuccessor(node);
        }
        // 自己指向自己,没有其他Node引用自己,等待被回收即可
        node.next = node; // help GC
    }
}

获取锁的流程讲解完毕接下来看释放锁的流程

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

与tryAcquire相似的是,tryRelease的实现由子类同步器去实现。只需要注意的是tryRelease方法返回true的情况只有state为0的时,只要state不为0说明线程还没有完全释放锁。

private void unparkSuccessor(Node node) {
    // node一般为当前线程所在的结点。
    int ws = node.waitStatus;
    // 如果有唤醒下一个线程的义务
    if (ws < 0)
        //置零当前线程所在的结点状态,允许失败。
        compareAndSetWaitStatus(node, ws, 0);
    //找到后继结点s
    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)//从这里可以看出,<=0的结点,都是还有效的结点。
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);//唤醒
}

我们可能会好奇为什么从后先前找,明明从前往后找下一个节点是最快速的,那么我们来去看将节点添加到队列尾部的源码

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            // 将新节点设置为tail成功,但是此时CPU时间片耗尽,
            // 正好换到了释放锁的线程,那么从前往后找就会出现null的情况
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

  • 19
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zmbwcx2003

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值