JDK源码阅读计划(Day14&15) AQS

JDK11

AQS

JDK并发包中的CountDownLatch,ReentrantLock,ThreadPoolExecutor,Semaphore,ReentrantReadWriteLock 都是继承自AbstractQueuedSynchronizer这个抽象类,其本质使用一个双向链表维护一个FIFO队列,用state来维护资源的状态以此用于实现上面那些子类的锁语义。

若被请求的共享资源空闲,则会把当前请求资源的线程设置为有效的工作线程,并且把共享资源设置为锁定状态。如果被请求的共享资源被占用,就把线程封装为Node加入到一个虚拟队列中,具体是通过CLH队列锁的方式实现。CLH本质上是用前一结点某一属性表示当前结点的状态
在这里插入图片描述
那么在这些子类中资源分别表示什么意思呢?这里参考ref给出一个表格
在这里插入图片描述

AQS有两种队列,一种是同步队列,一种是条件队列
在这里插入图片描述
在这里插入图片描述
同步队列是一个双向链表,而条件队列只有在使用Condition的时候才会创建,是一个单向队列。至于两者怎么用,得要等到后面才介绍了。队列中的结点本质上是对线程的包装,分为独享型和共享型两种:

继承关系

在这里插入图片描述

public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements Serializable

可以看见继承关系并不复杂,我们首先来看其抽象父类AbstractOwnableSynchronizer的代码

AbstractOwnableSynchronizer

提供了设置独占线程核获取独占线程的方法

public abstract class AbstractOwnableSynchronizer
    implements java.io.Serializable {

    /** Use serial ID even though all fields transient. */
    private static final long serialVersionUID = 3737899427754241961L;

    /**
     * Empty constructor for use by subclasses.
     */
    protected AbstractOwnableSynchronizer() { }

    /**
     * The current owner of exclusive mode synchronization.
     */
    private transient Thread exclusiveOwnerThread;

    /**
     * Sets the thread that currently owns exclusive access.
     * A {@code null} argument indicates that no thread owns access.
     * This method does not otherwise impose any synchronization or
     * {@code volatile} field accesses.
     * @param thread the owner thread
     */
    protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }

    /**
     * Returns the thread last set by {@code setExclusiveOwnerThread},
     * or {@code null} if never set.  This method does not otherwise
     * impose any synchronization or {@code volatile} field accesses.
     * @return the owner thread
     */
    protected final Thread getExclusiveOwnerThread() {
        return exclusiveOwnerThread;
    }
}

内部类

Node

static final class Node

作为同步队列与条件队列的结点。每一个等待锁的线程会封装成一个Node放入队列,每个Node有一个对Thread的引用,所以实际上是Thread在排队,然后每个Node还有一个waitStatus,用来表示线程的状态。

  • Marker用于标志模式
/** Marker to indicate a node is waiting in shared mode */
static final Node SHARED = new Node();  // 【共享】模式
/** Marker to indicate a node is waiting in exclusive mode */
static final Node EXCLUSIVE = null;     // 【独占】模式
  • 四种状态码
/*
* 该标记指示结点应当被取消,不再参与排队
* 如果在线程阻塞期间发生异常的话,会为其所在的结点设置此标记
*/
static final int CANCELLED = 1;

// SIGNAL,值为-1,表示当前节点的后继节点包含的线程需要运行,也就是unpark
static final int SIGNAL = -1;

// CONDITION,值为-2,表示当前节点在条件队列中
static final int CONDITION = -2;

/* 当一个共享结点没有经过阻塞就直接获取锁时,
* 会将头结点更新为该结点,并且为该结点设置PROPAGATE标记,
* 接下来,如果其后续结点也不需要经过阻塞,那么该结点保持PROPAGATE标记,
* 反之,如果其后续结点需要经过阻塞,那么该标记会被修改为SIGNAL
* /
* //只会在队列的头节点设置
static final int PROPAGATE = -3;

状态默认为0,表示结点在同步队列中

  • 线程可见的状态码

volatile保证线程可见

volatile int waitStatus;
  • Thread引用
 // node内存储的线程引用,表面上是node在排队,实际上是thread在排队
volatile Thread thread;
  • 前驱和后继
// 用于【|同步队列|】,表示排队结点的后继,顺着后继遍历可以找到陷入阻塞的node线程
volatile Node next;
volatile Node prev;
  • 下一个结点
/*
* 用于【|同步队列|】时,该引用仅作为标记使用
 * 它表示参与排队的node是[独占模式node],或者是[共享模式node]
 *
 * 用于【|条件队列|】时,该引用表示参与排队的下一个结点
 */
Node nextWaiter;
  • 构造函数
 /** Establishes initial head or SHARED marker. */
// 创建一个空的Node,用作头结点或共享标记
Node() {
}

/** Constructor used by addWaiter. */
// 创建一个独占/共享模式的node
Node(Node nextWaiter) {
    this.nextWaiter = nextWaiter;   // 记录当前node的模式
    THREAD.set(this, Thread.currentThread());   // 继续当前线程
}

/** Constructor used by addConditionWaiter. */
// 创建一个状态为waitStatus的node
Node(int waitStatus) {
    WAITSTATUS.set(this, waitStatus);
    THREAD.set(this, Thread.currentThread());
}
ConditionObject
public class ConditionObject implements Condition, Serializable

这个类方法比较多,具体实现等会再分析,不过我们可以从继承关系推断,这个类相当于AQS内部的Condition,提供wait,signal等线程通信的操作。

// {同步条件}对象,用于更精细地指导线程的同步行为
public class ConditionObject implements Condition, Serializable {
    private static final long serialVersionUID = 1173984872572414699L;
    
    /** Mode meaning to reinterrupt on exit from wait */
    // 刚刚唤醒的线程带有中断标记,且该线程node仍在【|条件队列|】,此时需要为线程恢复中断标记
    private static final int REINTERRUPT = 1;
    
    /** Mode meaning to throw InterruptedException on exit from wait */
    // 刚刚唤醒的线程带有中断标记,且该线程node已进入了【|同步队列|】,此时需要抛出异常
    private static final int THROW_IE = -1;
    
    // 【|条件队列|】以单链表形式组织,firstWaiter和lastWaiter是首尾结点,不存在头结点
    private transient Node firstWaiter, lastWaiter;

重要方法

tryAcquire

在这里插入图片描述

// 申请独占锁,允许阻塞带有中断标记的线程(会先将其标记清除)
public final void acquire(int arg) {
    // 尝试申请独占锁
    if(!tryAcquire(arg)){
        /*
         * 如果当前线程没有申请到独占锁,则需要去排队
         * 注:线程被封装到Node中去排队
         */
        
        // 向【|同步队列|】添加一个[独占模式Node](持有争锁线程)作为排队者
        Node node = addWaiter(Node.EXCLUSIVE);
        
        // 当node进入排队后再次尝试申请锁,如果还是失败,则可能进入阻塞
        if(acquireQueued(node, arg)){
            // 如果线程解除阻塞时拥有中断标记,此处要进行设置
            selfInterrupt();
        }
    }
}
  • 调用tryAcquire函数,调用此方法的线程会尝试申请独占锁。在AQS中该方法会抛异常,并没有实现,需要由子类实现。

  • 如果失败,说明线程没有申请到独占锁,需要以结点形式加入到同步队列末尾

  • 排队之后再次尝试申请锁,如果还是失败线程就阻塞

我们重点来看其中涉及到的方法:

addWaiter

尾插加入同步队列末尾

/*
* 向【|同步队列|】添加一个排队者(线程)
*
* mode有两种可能:
* 1.独占模式:Node.EXCLUSIVE
* 2.共享模式:Node.SHARED
* 由此,可创建[独占模式Node]或[共享模式Node]
* 创建的[模式Node]会记下当前线程的引用,并进入同步队列进行排队
*/
private Node addWaiter(Node mode) {
   // 创建一个独占/共享模式的node,该node存储了当前线程的引用
   Node node = new Node(mode);
   
   // 使用尾插法将node添加到【|同步队列|】
   enq(node);
   
   // 返回刚加入【|同步队列|】的[模式Node]
   return node;
}
enq
// 使用尾插法将node添加到【|同步队列|】,并返回旧的队尾
private Node enq(Node node) {
     for(; ; ) {
         Node oldTail = tail;
         if(oldTail != null) {
             // 设置node的前驱为oldTail
             node.setPrevRelaxed(oldTail);
             
             // 更新队尾游标指向node
             // oldTail为tail的引用,这里的CAS是把oldTail地址对应的值设置为node,相当于tail = node;
             if(compareAndSetTail(oldTail, node)) {
                 // 链接旧的队尾与node,形成一个双向链表
                 oldTail.next = node;
                 return oldTail;
             }
         } else {
             // 【|同步队列|】不存在时,需要初始化一个头结点
             initializeSyncQueue();
         }
     }
 }
acquireQueued

进入同步队列后的结点尝试抢锁,只有当前结点是头节点才有抢锁资格。
如果失败,则会调用shouldParkAfterFailedAcquire 判断能否挂起线程,然后调用parkAndCheckInterrupt挂起当前线程

// 当node进入排队后再次尝试申请锁,如果还是失败,则可能进入阻塞
    final boolean acquireQueued(final Node node, int arg) {
        
        // 记录当前线程从阻塞中醒来时的中断标记(阻塞(park)期间也可设置中断标记)
        boolean interrupted = false;
        
        try {
            /*
             * 死循环,成功申请到锁后退出
             *
             * 每个陷入阻塞的线程醒来后,需要重新申请锁
             * 只有当自身排在队首时,才有权利申请锁
             * 申请成功后,需要丢弃原来的头结点,并将自身作为头结点,然后返回
             */
            for(; ; ) {
                // 获取node结点的前驱
                final Node p = node.predecessor();//返回AQS的prev
                
                // 如果node结点目前排在了队首,则node线程有权利申请锁
                if(p == head) {
                    // 再次尝试申请锁
                    if(tryAcquire(arg)){
                        // 设置node为头结点(即丢掉了原来的头结点)
                        setHead(node);
                        
                        // 切断旧的头结点与后一个结点的联系,以便GC
                        p.next = null;
                        
                        // 返回线程当前的中断标记(如果线程在阻塞期间被标记为中断,这里会返回true)
                        return interrupted;
                    }
                }
                
                // 抢锁失败时,尝试为node的前驱设置阻塞标记(每个结点的阻塞标记设置在其前驱上)
                if(shouldParkAfterFailedAcquire(p, node)) {
                    /*
                     * 使线程陷入阻塞
                     *
                     * 如果首次到达这里时线程被标记为中断,则此步只是简单地清除中断标记,并返回true
                     * 接下来,通过死循环,线程再次来到这里,然后进入阻塞(park)...
                     *
                     * 如果首次到达这里时线程没有被标记为中断,则直接进入阻塞(park)
                     *
                     * 当线程被唤醒后,返回线程当前的中断标记(阻塞(park)期间也可设置中断标记)
                     */
                    interrupted |= parkAndCheckInterrupt();
                }
            }
        } catch(Throwable t) {
            // 如果中途有异常发生,应当撤销当前线程对锁的申请
            cancelAcquire(node);
            
            // 如果发生异常时拥有中断标记,此处要进行设置
            if(interrupted) {
                selfInterrupt();
            }
            
            throw t;
        }
    }
shouldParkAfterFailedAcquire

当入队的结点抢锁失败之后,调用该函数判断线程是否应该挂起

/*
     * 抢锁失败时,尝试为node的前驱设置阻塞标记
     *
     * 每个结点的阻塞标记设置在其前驱上,原因是:
     * 每个正在活动的结点都将成为头结点,当活动的结点(头结点)执行完之后,
     * 需要根据自身上面的阻塞标记,以确定要不要唤醒后续的结点
     *
     */
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        // node前驱的状态
        int ws = pred.waitStatus;
        
        // 如果node前驱的状态为Node.SIGNAL,则表示node需要进入阻塞状态
        if(ws == Node.SIGNAL){
            /* This node has already set status asking a release to signal it, so it can safely park. */
            
            // 只有前驱节点为SIGNAL才能调用park阻塞线程
            return true;
        }
        
        // 如果node前驱的状态被标记为取消,则顺着其前驱向前遍历,将紧邻的待取消结点连成一片
        if(ws>0) {
            /* Predecessor was cancelled. Skip over predecessors and indicate retry. */
            do {
                node.prev = pred = pred.prev;
            } while(pred.waitStatus>0);
            
            pred.next = node;
        } else {
            /*
             * waitStatus must be 0 or PROPAGATE.
             * Indicate that we need a signal, but don't park yet.
             * Caller will need to retry to make sure it cannot acquire before parking.
             */
            // 更新node前驱的状态为Node.SIGNAL,即使node陷入阻塞
            pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
        }
        
        return false;
    }
parkAndCheckInterrupt

阻塞线程的方法封装在这个函数中
首先执行park操作,即禁用当前线程,然后返回该线程是否已经被中断

// 设置线程进入阻塞状态,并清除线程的中断状态。返回值代表之前线程是否处于阻塞状态
private final boolean parkAndCheckInterrupt() {
    // 设置线程阻塞(对标记为中断的线程无效)
    LockSupport.park(this);
    return Thread.interrupted();//会清除中断标志位
}
cancelAcquire

如果acquireQueued抛异常,就要调用这个方法设置当前Node为CANCELLED,使其丧失获得资源的资格

 // 标记node结点为Node.CANCELLED(取消)状态
    private void cancelAcquire(Node node) {
        // Ignore if node doesn't exist
        if(node == null) {
            return;
        }
        
        // 删除对线程的引用,方便GC
        node.thread = null;
        
        // Skip cancelled predecessors
        Node pred = node.prev;
        // 顺着node的前驱向前遍历,将标记为取消的node结点连成一片
        while(pred.waitStatus>0) { //CANCELLED  = 1
            node.prev = pred = pred.prev;
        }
        
        // predNext is the apparent node to unsplice.
        // CASes below will fail if not, in which case, we lost race vs another cancel or signal,
        // so no further action is necessary, although with a possibility that a cancelled node may transiently remain reachable.
        Node predNext = pred.next;
        
        // Can use unconditional write instead of CAS here.
        // After this atomic step, other Nodes can skip past us.
        // Before, we are free of interference from other threads.
        // 标记node线程进入取消状态
        node.waitStatus = Node.CANCELLED;
        
        // If we are the tail, remove ourselves.
        // 如果当前节点是尾节点,把pred设置成tail
        if(node == tail && compareAndSetTail(node, pred)) {
            // 把predNext即当前Node设为null
            pred.compareAndSetNext(predNext, null);
        } else {
            // If successor needs signal, try to set pred's next-link so it will get one.
            // Otherwise wake it up to propagate.
            int ws;
            
            if(pred != head
                && ((ws = pred.waitStatus) == Node.SIGNAL
                    || (ws<=0 && pred.compareAndSetWaitStatus(ws, Node.SIGNAL)))
                && pred.thread != null) {
                Node next = node.next;
                // 将处于阻塞状态的node连成一片,通过后继向后遍历即可获得
                if(next != null && next.waitStatus<=0) {
                    //pred.next = next 相当于把node忽略掉
                    pred.compareAndSetNext(predNext, next);
                }
            } else {
                // 唤醒node后面陷入阻塞的“后继”
                unparkSuccessor(node);
            }
            
            node.next = node; // node后继指向自身,目的是为了便于GC,因为再也没有别的引用指向Node了
        }
    }

结合下面这张图来理解,cancelAcquire以Node为参数,把其状态设置为CANCELLED,并且向前遍历把直到找到SIGNAL的结点,中间CANCELLED的所有节点都会被移除这个同步队列。然后调用unparkSuccessor尝试唤醒node的下一个正在阻塞的结点。

/*
 * 唤醒node后面陷入阻塞的“后继”
 *
 * 注:
 * 由于有些结点可能会被标记为Node.CANCELLED(取消),
 * 所以这里的后继可能不是node.next,需要进一步搜索后才能确定
 */
private void unparkSuccessor(Node node) {
    /*
     * If status is negative (i.e., possibly needing signal) try to clear in anticipation of signalling.
     * It is OK if this fails or if status is changed by waiting thread.
     */
    int ws = node.waitStatus;
    if(ws<0) {
        // 如果node状态码为负,则将其重置为0
        node.compareAndSetWaitStatus(ws, 0);
    }
    
    /*
     * Thread to unpark is held in successor, which is normally just the next node.
     * But if cancelled or apparently null, traverse backwards from tail to find the actual non-cancelled successor.
     */
    Node s = node.next;
    /*
     * 如果s==null,说明node已经是尾结点,后面没有需要唤醒的结点了
     *
     * 如果s!=null,且s.waitStatus>0,说明node被标记为Node.CANCELLED(取消)
     * 此时,需要从尾端向前遍历,找到离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;
            }
        }
    }
    
    // 唤醒node的后继
    if(s != null) {
        LockSupport.unpark(s.thread);
    }
}

一个不允许中断带有中断标志的线程的acquire方法都那么复杂了…源码还有几个允许中断的方法,还有几个带超时参数的方法,以后再说吧,但是大概思路和上面是一样的

Release

// 释放锁,如果锁已被完全释放,则唤醒后续的阻塞线程。返回值表示本次操作后锁是否自由
public final boolean release(int arg) {
    // 释放一次锁,返回值表示同步锁是否处于自由状态(无线程持有)
    if(tryRelease(arg)) {
        /* 如果锁已经处于自由状态,则可以唤醒下一个阻塞的线程了 */
        
        Node h = head;
        if(h != null && h.waitStatus != 0) {
            // 唤醒h后面陷入阻塞的“后继”
            unparkSuccessor(h);
        }
        
        return true;
    }
    
    return false;
}

tryRelease在AQS中没有实现,需要依赖子类来实现

acquire和release方法相当于实现了锁中lock和unlock的操作

acquire总结起来就是当前线程尝试去抢占锁,如果抢不了就以Node形式加入到队列中,再尝试抢一次,抢不到就挂起阻塞

然后之后AQS具体怎么用,就要看其子类的源码的时候再给出相关源码了。至于contentObject方法得具体实现,可能要到看ReentrantLock和ReentrantReadWriteLock的时候再介绍了

ref

https://juejin.im/post/5ae754af518825672a02b498
https://www.cnblogs.com/leesf456/p/5350186.html
https://editor.csdn.net/md/?articleId=106038954

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值