java源码阅读---AbstractQueuedSynchronizer解析

本文详细分析了AbstractQueuedSynchronizer(AQS)的源码,包括构造方法、内部Node类的结构、同步状态的管理以及关键方法如enq、addWaiter、setHead、acquireQueued的工作原理。AQS是Java并发编程中重要的基础组件,用于实现如ReentrantLock等同步器。
摘要由CSDN通过智能技术生成

AbstractQueuedSynchronizer源码解

本篇文章我们来看一下AbstractQueuedSynchronizer也就是俗称的AQS的源码解析,直接进入正题。

构造方法

AbstractQueuedSynchronizer只有一个无参构造方法,没有什么好说的。

protected AbstractQueuedSynchronizer() { }
内部类

静态内部类Node类,是一个双向链表。

static final class Node {
    
    static final Node SHARED = new Node();//用于指示节点正在共享模式下等待的标记
    
    static final Node EXCLUSIVE = null;//用于指示节点正在以独占模式等待的标记

    static final int CANCELLED =  1;//waitStatus值,指示线程已取消
    
    static final int SIGNAL    = -1;//waitStatus值,指示后续线程需要取消标记
    
    static final int CONDITION = -2;//waitStatus值,指示线程正在等待条件
 
    static final int PROPAGATE = -3;//waitStatus值,表示下一个获取共享应无条件传播的waitStatus值

    volatile int waitStatus;//状态字段,仅接受以上4个值和默认的0

    volatile Node prev;//指向当前节点的前置节点

    volatile Node next;//指向当前节点的后置节点

    volatile Thread thread;//当前节点的线程
    
    Node nextWaiter; //指向下一个条件等待节点

    //判断该节点是否共享模式
    final boolean isShared() {
        return nextWaiter == SHARED;
    }

    //获取当前节点的前置节点
    final Node predecessor() throws NullPointerException {
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }

    Node() {    //用于建立初始头节点或共享标记
    }

    Node(Thread thread, Node mode) {     //addWaiter方法所使用
        this.nextWaiter = mode;
        this.thread = thread;
    }

    Node(Thread thread, int waitStatus) { //用于Condition中使用
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
}

以上是Node类的解析。

参数

头节点,即当前持有锁的线程

private transient volatile Node head;

尾结点,等待队列的尾部,已延迟初始化。仅通过enq方法进行修改以添加新的等待节点。

private transient volatile Node tail;

同步状态,初始为0,使用volatile关键字修饰,保证了其他线程的可见性,当值发生改变是其他线程能够第一时间获取到最新值。

private volatile int state;
方法
compareAndSetState

//通过CAS的方式将同步状态设置为给定的更新值,如果当前状态值等于预期值。

protected final boolean compareAndSetState(int expect, int update) {
    // See below for intrinsics setup to support this
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
enq
private Node enq(final Node node) {
    //CAS"自旋",直到成功加入队尾
    for (;;) {
        Node t = tail;
        if (t == null) { //队列为空,创建一个空的标志结点作为head结点,并将tail也指向它。
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {//正常流程,放入队尾
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}
addWaiter
private Node addWaiter(Node mode) {
    //以给定模式构造结点。mode有两种:EXCLUSIVE(独占)和SHARED(共享)
    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;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);//调用enq方法放入队列
    return node;
}
setHead

将队列的头设置为节点,从而出列。仅由调用获取方法。为了GC,还将未使用的字段清空并且抑制不必要的信号和遍历

private void setHead(Node node) {
    head = node;
    node.thread = null;
    node.prev = null;
}
acquireQueued
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;//标记是否成功拿到资源
    try {
        boolean interrupted = false;//标记等待过程中是否被中断过
        //又是一个“自旋”!
        for (;;) {
            final Node p = node.predecessor();//拿到前驱节点
            //如果前驱是head,即该结点已成老二,那么便有资格去尝试获取资源(可能是老大释放完资源唤醒自己的,当然也可能被interrupt了)
            if (p == head && tryAcquire(arg)) {
                setHead(node);//拿到资源后,将head指向该结点。所以head所指的标杆结点,就是当前获取到资源的那个结点或null
                p.next = null; //setHead中node.prev已置为null,此处再将head.next置为null,就是为了方便GC回收以前的head结点。也就意味着之前拿完资源的结点出队了
                failed = false;// 成功获取资源
                return interrupted;//返回等待过程中是否被中断过
            }
            //如果自己可以休息了,就通过park()进入waiting状态,直到被unpark()。如果不可中断的情况下被中断了,那么会从park()中醒过来,发现拿不到资源,从而继续进入park()等待
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed) // 如果等待过程中没有成功获取资源(如timeout,或者可中断的情况下被中断了),那么取消结点在队列中的等待。
            cancelAcquire(node);
    }
}

先看看shouldParkAfterFailedAcquire()和parkAndCheckInterrupt()具体干些什么

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;//拿到前驱的状态
    if (ws == Node.SIGNAL)//如果已经告诉前驱拿完号后通知自己一下,那就可以安心休息了
        return true;
    if (ws > 0) {
        /*
         * 如果前驱放弃了,那就一直往前找,直到找到最近一个正常等待的状态,并排在它的后边。
         * 注意:那些放弃的结点,由于被自己“加塞”到它们前边,它们相当于形成一个无引用链,稍后就会被GC回收
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        //如果前驱正常,那就把前驱的状态设置成SIGNAL,告诉它拿完号后通知自己一下。有可能失败,人家说不定刚刚释放完呢
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

park()会让当前线程进入waiting状态。在此状态下,有两种途径可以唤醒该线程:1)被unpark();2)被interrupt()

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);//调用park()使线程进入waiting状态
    return Thread.interrupted();//如果被唤醒,查看自己是不是被中断的
}

看了shouldParkAfterFailedAcquire()和parkAndCheckInterrupt(),现在让我们再回到acquireQueued(),总结下该函数的具体流程:

  1. 结点进入队尾后,检查状态,找到安全休息点;
  2. 调用park()进入waiting状态,等待unpark()或interrupt()唤醒自己;
  3. 被唤醒后,看自己是不是有资格能拿到号。如果拿到,head指向当前结点,并返回从入队到拿到号的整个过程中是否被中断过;如果没拿到,继续流程1。
acquire
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
  1. 调用自定义同步器的tryAcquire()尝试直接去获取资源,如果成功则直接返回;
  2. 没成功,则addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;
  3. acquireQueued()使线程在等待队列中休息,有机会时(轮到自己,会被unpark())会去尝试获取资源。获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
  4. 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上
release
public final boolean release(int arg) {
 //tryRelease这个方法是需要独占模式的自定义同步器去实现的。正常来说,tryRelease()都会成功的,因为这是独占模式,该线程来释放资源,那么它肯定已经拿到独占资源了,直接减掉相应量的资源即(state-=arg),也不需要考虑线程安全的问题。但要注意它的返回值,上面已经提到了,release()是根据tryRelease()的返回值来判断该线程是否已经完成释放掉资源了!所以自义定同步器在实现时,如果已经彻底释放资源(state=0),要返回true,否则返回false。
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

此方法用于唤醒等待队列中下一个线程

private void unparkSuccessor(Node node) {
    //这里,node一般为当前线程所在的结点。
    int ws = node.waitStatus;
    if (ws < 0)//置零当前线程所在的结点状态,允许失败。
        compareAndSetWaitStatus(node, ws, 0);

   
    Node s = node.next;//找到下一个需要唤醒的结点s
    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);//唤醒
}

这个函数并不复杂。一句话概括:用unpark()唤醒等待队列中最前边的那个未放弃线程,这里我们也用s来表示吧

以上就是AbstractQueuedSynchronize常用的方法,同时ReentrantLock的加锁和释放锁都是基于以上方法。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值