校招面试准备——AQS

 

先给大家分享一个我觉得写的很好的博客:https://blog.csdn.net/jiankunking/article/details/79431767

文章中还有一个哦

 

 

AQS是AbatractQueuedSynchronizer,是一个抽象类,在Locks包下,在ReentrantLock以及线程池中都用到了它

AQS维护了一个volatile修饰的资源state,以及一个用于存储竞争该资源的线程队列。这个队列是FIFO的,没有竞争到state的线程会被放置在这个队列中。

这个共享资源state 是int类型的变量,共有三个方法可以操作state

    private volatile int state;

    /*
      return current state value
    */
    protected final int getState() {
        return state;
    }

    /*
       Sets the value of synchronization state.
    */
    protected final void setState(int newState) {
        state = newState;
    }

    /**
     判断现在的值是否为期待的值,如果是,对state的值进行修改
     * @return true if successful. return false value was not equal to the expected value.
     */
    protected final boolean compareAndSetState(int expect, int update) {
        return STATE.compareAndSet(this, expect, update);
    }

那这个资源每次能被多少个线程使用呢?

一共有两种情况:

1. 独占式  Exclusive

只有单个线程可以获取资源,比如 ReentrantLock

2. 共享式  Shared

多给线程可以同时获取资源

因为AQS是抽象类,它之中有一些抽象函数需要被子类实现。但其实AQS已经实现好了大多数的同步逻辑,子类只需要实现对state的获取和释放函数就可以了。主要包括以下方法(需要被子类实现):但这些方法并没有被abstract修饰,目的是避免子类不得不覆盖多个方法,因为子类一般要么是独占式 要么是 共享式的,不需要实现所有的方法。当然 AQS也支持子类同时实现独占式和共享式,如ReentrantReadWriteLock。【这些方法都是protected的,int类型的参数代表什么是由程序员自己决定的】

bool tryAcquire( int )   当为独占式时的获取资源的方法

bool tryRelease( int )   当为独占式时的释放资源的方法

bool tryAcquireShared( int )   当为共享式时的获取资源的方法

bool tryReleaseShared( int )   当为共享式时的释放资源的方法

isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它

 

内部类Node

刚刚提到AQS 维护了一个队列,队列中的每个元素 都是AQS内部类Node的对象。它是一个双向队列,所以每个元素需要记录它之前和之后的元素(都是volatile的): 

volatile Node prev;

volatile Node next;

因为这个队列是用于存储被阻塞的线程的,所以每个节点最基本的就是存储线程。因此Node类中有成员变量:

volatile Thread thread;

此外,每个节点都有一个 volatile int waitState 变量;这个变量共有5种值:

SIGNAL(-1): 当前节点释放state或者取消后,将通知后续节点竞争state。

CANCELLED(1): 线程因timeout和interrupt而放弃竞争state

CONDITION(-2): 表征当前节点处于条件队列中,即它在等待某个条件

PROPAGATE(-3): 表征下一个acquireShared应无条件传播 【没明白】

0:  None of the above ,默认值

再然后,节点还要记录这个线程是处于什么模式的(共享 or 独占)

首先,我们会发现,Node类中有两个静态变量:

    // 表明节点在共享模式下等待的标记
    static final Node SHARED = new Node();
    // 表明节点在独占模式下等待的标记
    static final Node EXCLUSIVE = null;

这两个变量代表了这个线程是在什么模式下。可能说的不太清楚,接着解释

Node中有一个成员变量:volatil int waitStatus

这个变量 是用来表示当前的线程处于什么模式。如果 waitStatus == Node.SHARED ,则说明这个线程是共享模式

 

AQS中的Node

AQS中记录着队列的头结点和尾节点,从第一个图可以看出。头结点是现在正在占用资源的节点

private transient volatile Node head;

private transient volatile Node tail;

 

再接着看AQS中的方法:

之前提到了用户需要自己实现 tryAcquire 和 tryRelease等方法,但实际上,这些方法是protected修饰的,也就是当用户想要为某个现场要state时,不需要直接调用这些方法,而是调用AQS封装好的这四个方法。

对于参数arg,源代码中如此解释:

arg the acquire argument.  This value is conveyed to tryAcquire() but is otherwise uninterpreted and can represent anything you like.   这个参数会被传递给tryAcquire,但对它没有解释,您可以让它代表任何你喜欢的值。

public final void acquire (int arg) 

public final void acquireShared (int arg) 

public final boolean release(int arg)

public final void releaseShared (int arg) 

独占模式下的获取资源过程:

public final void acquire(int arg) {
    // Node.EXCLUSIVE就是刚刚提到的 Node中表示独占式的静态变量
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

这个方法首先调用用户定义的tryAcquire()方法,如果获取资源成功,则 ! tryAcquire()为false,后面的操作会被短路(就是不执行了);如果获取失败,则需要继续执行 "&&"后面的方法 final boolean acquireQueued ( final Node node, int arg)。

acquireQueued方法的第一个参数 addWaiter()方法,这个方法 Creates and enqueues node for current thread and given mode.【为这个线程以及模式(独占、共享)创建Node并且放入队列(队尾),它返回为这个线程新建的Node对象。

addWaiter() 方法具体的工作是:如果队尾被初始化过了,且线程在加入队尾时成功加进去了(没有竞争或者竞争到了)就直接插进去。如果队尾没有初始化过或者加入失败了,则调用 enq() 方法,一直循环尝试插入,直到插入成功。

    private Node addWaiter(Node mode) {
        //把当前线程包装为node,设为独占模式
        Node node = new Node(Thread.currentThread(), mode);
        // 尝试快速入队,即无竞争条件下肯定成功。如果失败,则进入enq自旋重试入队
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            //CAS替换当前尾部。成功则返回
            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) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

在把新节点插入到队列之后,继续通过acquireQueued方法努力争取一下锁。方法是通过循环,不断判断它的前一个节点是否为头结点。之前说过头结点是正在占用资源的节点,如果它的前一个节点是头结点,说明它很有希望,就用tryRequire尝试获取一下资源。如果获取到了就返回中断标记。如果没有,判断一下是否要阻塞该线程。如果发生了异常,则放弃获取资源疑问:什么情况下会发生异常啊】

因此 acquireQueued 方法第一个参数是为被新阻塞的线程创建的Node,之后它做了两件事情:

  • 使线程在等待队列中获取资源,直到获取到资源返回,若整个等待过程被中断过,则返回True,否则返回False。
  • 如果线程在等待过程中被中断过,则先标记上,待获取到资源后再进行自我中断selfInterrupt(),将中断响应掉。

【代码和注释引用自:https://www.jianshu.com/p/0f876ead2846

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);
                // 说明前继节点已经释放掉资源了,将其next置空,以方便虚拟机回收掉该前继节点
                p.next = null; // help GC
                // 标识获取资源成功
                failed = false;
                // 返回中断标记
                return interrupted;
            }
            // 若前继节点不是头结点,或者获取资源失败,
            // 则需要通过shouldParkAfterFailedAcquire函数
            // 判断是否需要阻塞该节点持有的线程
            // 若shouldParkAfterFailedAcquire函数返回true,
            // 则继续执行parkAndCheckInterrupt()函数,
            // 将该线程阻塞并检查是否可以被中断,若返回true,则将interrupted标志置于true
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        // 最终获取资源失败,则当前节点放弃获取资源
        if (failed)
            cancelAcquire(node);
    }
}

至此,独占模式下,线程获取资源acquire的代码就跟完了,总结一下过程:

  1. 首先线程通过tryAcquire(arg)尝试获取共享资源,若获取成功则直接返回,若不成功,则将该线程以独占模式添加到等待队列尾部,tryAcquire(arg)由继承AQS的自定义同步器来具体实现;
  2. 当前线程加入等待队列后,会通过acquireQueued方法基于CAS自旋不断尝试获取资源,直至获取到资源;
  3. 若在自旋过程中,线程被中断过,acquireQueued方法会标记此次中断,并返回true。
  4. 若acquireQueued方法获取到资源后,返回true,则执行线程自我中断操作selfInterrupt()。【https://www.jianshu.com/p/0f876ead2846】

 

独占模式释放资源的方法:

当程序员要释放资源的时候,一定是释放现在占用着资源的线程。之前提到过,队列头结点存着的线程就是占用资源的线程。所以当调用release方法时,要先获取头结点。获取之后,通过他的waitStatus判断一下是否要唤醒之后的线程

(回忆一下waitstatus: -1 signal 要唤醒后续的线程   -2 condition 在等待某个条件    -3 propagate 下一个acquireShared应无条件传播   1 线程放弃竞争   0以上都不是0 )

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        // 获取到等待队列的头结点h
        Node h = head;
        // 若头结点不为空且其ws值非0,则唤醒h的后继节点
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h); //该方法主要用于唤醒等待队列中的下一个阻塞线程。
            // 如果后续节点不为空且没作废(waitStatus不为1),就唤醒他,否则一直找下去
        return true;
    }
    return false;
}

 

共享模式下获取资源:

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

tryAcquireShared() 方法返回值如果为0,则代表获取资源成功,但没有剩下的资源;如果为正数 表示成功获取且有剩余的资源;如果为负数,则表示获取失败。

如果获取资源成功则直接返回,否则调用doAcquireShared方法使线程进入队列,执行自旋获取资源 ;在自旋过程中,如果获取到了资源,则判断是否有多余的资源,如果有,就唤醒后续的线程(体现了共享,独占状态下,只有在释放资源成功后才会唤醒之后的线程);此外我们发现在acquireShared中没有判断中断,那是因为在doAcquireShared方法中对中断进行了处理。

 

共享模式下释放资源: 

public final boolean releaseShared(int arg) {
    // 尝试释放资源
    if (tryReleaseShared(arg)) {
        // 唤醒后继节点的线程
        doReleaseShared();
        return true;
    }
    return false;
}

当 一个线程成功释放了资源,会通过doReleaseShared()方法唤醒其他等待资源的线程。

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值