java并发编程学习,AQS源码解读

121 阅读4分钟

用途

AQS是一种提供了原子式管理同步状态阻塞和唤醒线程功能以及队列模型的简单框架。 决定线程是否能够使用共享资源。

使用到的设计模式

模板设计模式

设计整体思路

多个线程在访问共享资源前,首先先经过AQS判断当前同步器的状态,是否可以获取资源,而这个判断的规则由 我们开发者来定义。如ReetrantLock定义state状态大于0则代表有线程已经占用。对状态判断后的入队,唤醒操作,aqs已经实现好。 总结:开发者使用aqs的时候,只需要关注state的实际含义。

源码解读

类属性和方法

属性:

  • state:用来判断当前是否获取到资源,对于每个实现AQS方法的工具而言,定义不同
  • head:记录队列的头指针
  • tail:记录队列的尾指针

imagepng 需要子类实现的抽象方法(对state状态的判断)

  • boolean tryAcquire(独占式获取资源)
  • boolean tryRelease (独占式释放资源)
  • int tryAcquireShared (共享式获取资源)
  • int tryReleaseShared (共享式释放资源)
  • boolean isHeldExclusively (是否独占)

什么是独占:资源只能由一个线程使用 什么是共享:资源可以由多个线程同时使用

注意:独占式获取的返回参数,是boolean类型,而共享式返回对象为int类型。即独占式只需要对状态判断返回false代表获取失败,共享式通过state值小于0代表获取失败。

接下来源码解读以ReetrantLock为例子。

判断是否可以获取锁(由开发者实现)

首先ReetrantLock是独占式,且可重入的锁。 什么是可重入?如下代码:

public class ReentranLockTest  implements Runnable{
    ReentrantLock lock = new ReentrantLock();

    public void get() {
        lock.lock();
        try{
            set();
        }
        catch(Exception e){

        }
        finally{     
            lock.unlock();
        }
    }

    public void set() {
        lock.lock();
        try{
            // do something
        }
        catch(Exception e){

        }
        finally{     
            lock.unlock();
        }
    }

    @Override
    public void run() {
        get();
    }

    public static void main(String[] args) {
        ReentranLockTest ss = new ReentranLockTest();
        new Thread(ss).start();
    }
}

线程调用get()方法,获取一次锁,get()中调用set()还会再一次获取锁,此时该线程已经拥有了锁,还可以再次同样的锁,就是可重入。 ReetrantLock有公平和非公平获取锁方式,这是题外话,不影响对aqs的理解,以默认的非公平的实现方式为例。

// reetrantLock中这个方法就类似 tryAcquire 是默认调用的方法
final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                // 状态值为0,说明没有人获取,则可以获取资源
                if (compareAndSetState(0, acquires)) {
                    // 记录当前线程为锁对象拥有者
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // 不为0,说明已经名花有主,这个时候判断是否是本身线程,实现可重入功能
            else if (current == getExclusiveOwnerThread()) {
                // 重入的时候,state就累加1,释放的时候state就减一
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            // state不为0,且当前资源拥有者也不是当前线程,则获取失败,由aqs将该线程放入等待队列
            return false;
        }

tryAcquire方法,由开发者实现锁的时候定义,实现的过程主要关注的是state的状态,和当前资源的拥有者。

根据state的判断结果,阻塞线程

这段代码的关键点就是,tryAcquire负责判断线程是否有资格可以获取资源,有则设定锁对象拥有者,不存在则返回fasle,进入等待队列的操作。而这一部分的操作,aqs已经帮我们实现好,无需开发者实现。这个公共方法的入口如下:

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

该方法可以说是获取锁的核心逻辑入口:

  • 判断是否获取到锁 tryAcquire(获取锁失败,到这个方法返回值为false,继续执行后面的方法)

  • 没有获取到锁,加入等待队列addWaiter

  • 加入队列后,暂停线程acquireQueued(使用LockSupport.park阻塞当前获取失败的线程)

    加入等待队列

    首先这个等待队列有几个特点:

  • 队列是双向列表结构

  • 先进先出

  • 队列的首节点是哨兵节点。

什么是哨兵节点? 刚开始的时候,队列中的head,tail指针为null。当有线程进入以后,会执行两次循环,一次创建哨兵节点,一次将失败的线程包装成Node加入队列中。

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

图示过程:

为什么需要哨兵节点? 反证法,如果没有哨兵节点的话,双向队列,如何出队列呢?

// 无哨兵出队列


// 获取当前头结点
Node pre = head;
// 获取下一个节点,重新设置头结点为下一个节点
Node next = head = pre.next;
if(next != null){
    // 下个节点的前置节点设置为null
    next.pre = null;
}else{
    // 头节点下一个节点为null,说明队列为空,重新设置tail
    tail =  null;
}
// 方便gc
pre.next = null;

// 有哨兵出队列(node是当前需要出的节点)
// 获取头节点(此时是哨兵节点)
Node pre = head;
// 重新设置头结点为此时要出队的线程的节点
head = node;
// 原先存储线程的节点,变成哨兵节点
node.thread = null;
node.pre = null;

// 方便回收旧哨兵节点
pre.next = null;

有哨兵节点出队列如图: 首先代码上:减少了一次判空操作。 其次,并发场景下,如果没有哨兵节点,假设只有一个节点要出队列的情况,要同时把 head 和tail 变成null。如果这个时候才把head变成null了,同时发生入队,此时tail还没变null,造成数据错乱。 使用哨兵节点的情况,tail只负责入队操作,head只负责出队操作,职能更单一。不存在只剩最后一个节点出队时要同时维护head和tail数据为null的可能。

加入队列后,自旋判断是否有机会出队,无则阻塞

出队列的条件:前驱节点为头结点 阻塞前的条件:前驱节点状态设置为-1

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;
                }
                // 不满足出队列条件,判断是否可以暂停线程
                // 线程暂停的条件:前驱节点状态为-1
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    // 说明线程被打断过
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }


private final boolean parkAndCheckInterrupt() {
        // 注意,LockSupport.park(this);若此时线程被其他线程打断,会虚假唤醒
        LockSupport.park(this);
        // 故需要重新设置打断标识,此时被打断会返回true,且清除标记位
        return Thread.interrupted();
    }

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        // 前驱节点的状态为-1
        if (ws == Node.SIGNAL)
            return true;
        // 前驱节点状态大于0说明前驱节点被其他线程取消了
        // 向前遍历没被取消的节点,挂载到该节点后面,且设置前驱节点状态为-1
        // 如果是这种被取消的情况,会执行shouldParkAfterFailedAcquire 3次,由外层循环控制
        // 第一重新挂载,第二次设置前驱节点状态为-1,第三次判断前驱节点状态为-1返回true
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {

            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

总结: imagepng

释放资源

释放资源后,需要做2件事,修改state状态,唤醒等待队列中的线程。 解锁步骤aqs的入口为

public final boolean release(int arg) {
        // 这个tryRelease和上面tryAcquire一样,需要开发者对state状态判断和修改
        if (tryRelease(arg)) {
            Node h = head;
            // 结合进入队列,在线程暂停之前,会把前面的,非取消的线程的节点状态设置为-1,
            // 被取消的节点状态是为1
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

和加锁一样tryRelease(arg)由各个锁对state状态判断是否具备解锁条件,以reetrantLock为例

protected final boolean tryRelease(int releases) {
            // 获取state,计算
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            // 为0 代表可以解锁
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

解锁需要,改变state状态(tryRelease方法实现),唤醒等待队列中的线程(unparkSuccessor实现)

private void unparkSuccessor(Node node) {

        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
        // 前驱节点为哨兵节点,所以要唤醒的是哨兵的后一个节点,且状态不为取消(取消的状态值为1)
        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)
            LockSupport.unpark(s.thread);
    }

条件队列,阻塞

该方法作用与synchronized中wait作用相同。 条件变量的唤醒,将当前队列加入等待条件中

public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            // 加入等待条件队列
            // 阻塞队列与获取锁的队列不同,
            // 没有哨兵节点,因为此时已经获取到锁了,不存在竞争条件
            Node node = addConditionWaiter();
            // 释放锁
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            // 节点中的线程阻塞
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                // 若中途被打断,则结束循环
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            // 获取锁
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

private Node addConditionWaiter() {
            Node t = lastWaiter;
            // 清理条件队列中,被取消的线程
            if (t != null && t.waitStatus != Node.CONDITION) {
                unlinkCancelledWaiters();
                t = lastWaiter;
            }
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
            if (t == null)
                firstWaiter = node;
            else
                t.nextWaiter = node;
            lastWaiter = node;
            return node;
        }


private void unlinkCancelledWaiters() {
            Node t = firstWaiter;
            Node trail = null;
            while (t != null) {
                Node next = t.nextWaiter;
                if (t.waitStatus != Node.CONDITION) {
                    t.nextWaiter = null;
                    if (trail == null)
                        firstWaiter = next;
                    else
                        trail.nextWaiter = next;
                    if (next == null)
                        lastWaiter = trail;
                }
                else
                    trail = t;
                t = next;
            }
        }

条件队列,唤醒

该方法作用与synchronized中notify/notifyAll作用相同。

 public final void signal() {
             // 判断当前线程是否是有资格唤醒,只有持有锁才能释放条件队列
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignal(first);
        }


        public final void signalAll() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignalAll(first);
        }


private void doSignal(Node first) {
            do {
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                first.nextWaiter = null;
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }


final boolean transferForSignal(Node node) {

        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;

        Node p = enq(node);
        int ws = p.waitStatus;
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }

执行 transferForSignal 流程,将该 Node 加入 AQS 队列尾部,将阻塞队列中,加入前前一个节点设置状态为-1,改为-1就有责任去唤醒自己的后继节点

参考资料

blog.csdn.net/m0_37989980… cloud.tencent.com/developer/a… concurrent.redspider.group/article/02/…