解密JUC——构建锁和同步器的AQS

AQS,本名:AbstractQueuedSynchronizer,是Java 5引入的一个并发工具类。

它提供了一个基于FIFO(先进先出)队列,可以用于构建锁或者其他相关同步装置的基础框架。

它的名字翻译为抽象队列同步器,可以分为三个词:抽象、队列、同步器。

正好不知道怎么开始,那么现在我们就以名字的三个词作为切入点。但是为了逻辑讲得清晰,我调了一下顺序:同步器->抽象->队列。

1、同步器

这个类里面有一个最关键的属性:state,int类型。我觉得可以理解成两种意思,首先名称是state,直接翻译为状态(比如ReentrantLock里面的state用法),然后类型是int,可以理解为资源数量(比如Semaphore里面的state用法)。

它提供了getState和setState方法,还有一个线程安全的compareAndSetState方法。重点是后面这个,利用Unsafe进行CAS操作(如果当前状态值等于预期值,则以原子方式将同步状态设置为给定的更新值,相关资料可以参考上一篇博客:https://blog.csdn.net/qq_31142553/article/details/94407361)。

正是因为可以做到在并发场景下对state的修改是原子性的并且可以获取修改结果,所以基于这个特性可以将它做成一些同步器(类比Redis的setNx命令和Zookeeper的创建节点)。

2、抽象

抽象,说明它是可以被子类继承并且重写其中的一些方法。官方也是这么说的:Subclasses must define the protected methods that change this state, and which define what that state means in terms of this object being acquired or released。用我不太擅长的英语翻译过来就是:子类必须明确更改此状态的受保护方法,并定义哪种状态对于此对象意味着被获取或被释放。因此这个类提供了以下方法

  • tryAcquire(int):试图在独占模式下获取对象状态。
  • tryRelease(int):试图设置状态来反映独占模式下的一个释放。
  • tryAcquireShared(int):试图在共享模式下获取对象状态。
  • tryReleaseShared(int):试图设置状态来反映共享模式下的一个释放。
  • isHeldExclusively():如果对于当前(正调用的)线程,同步是以独占方式进行的,则返回 true。

它们的默认实现都是throw new UnsupportedOperationException();要求我们覆盖这些方法,定义哪种状态对于此对象意味着被获取或被释放。

这就用到了设计模式里面的模板方法模式:抽象父类定义了一个算法的所有步骤,而将其中一些实现交给子类,以满足不同的场景需要。

比如用作锁的话,state可以定义两个值:0表示未锁定状态,1表示锁定状态。

那么加锁就可以这样实现

// Acquire the lock if state is zero
public boolean tryAcquire(int acquires) {
    assert acquires == 1; // Otherwise unused
    if (compareAndSetState(0, 1)) {
        return true;
    }
    return false;
}

释放锁可以这样实现

// Release the lock by setting state to zero
protected boolean tryRelease(int releases) {
    assert releases == 1; // Otherwise unused
    if (getState() == 0) throw new IllegalMonitorStateException();
    setState(0);
    return true;
}

比如用作信号量(许可数量)的话,state可以表示剩余的数量。

那么获取所需资源就可以这样实现

protected int tryAcquireShared(int acquires) {
    for (;;) {
        int available = getState();
        int remaining = available - acquires;
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            return remaining;
    }
}

释放资源就可以这样实现

protected final boolean tryReleaseShared(int releases) {
    for (;;) {
        int current = getState();
        int next = current + releases;
        if (next < current) // overflow
            throw new Error("Maximum permit count exceeded");
        if (compareAndSetState(current, next))
            return true;
    }
}

3、队列

线程抢夺资源失败的时候,需要将其放进等待队列的末尾,然后将其挂起。等到资源释放的时候,拿出等待队列里面的第一个线程,让其继续去争抢资源。

因为这种首尾都会修改的使用特点,采用链表的实现方式远优于数组。定义了一个Note的内部类表示链表的节点,除了有用于维护链表连接关系的prev(前节点)和next(后节点)属性外,还有thread用于保存线程信息、waitStatus表示节点状态等。

下面是waitStatus的取值,注意默认值是没有意义的0。

waitStatus表示节点的状态

AbstractQueuedSynchronizer中使用head和tail两个Note类型的属性存储链表的头结点和尾节点,从而可以修改或者遍历那个所谓的队列(或者说链表)。

        
如果把三者结合起来,就成了下面这个算法。


Acquire:
     while (!tryAcquire(arg)) {
        enqueue thread if it is not already queued;
        possibly block current thread;
     }

 Release:
     if (tryRelease(arg))
        unblock the first queued thread;


我们看下ReentrantLock的非公平锁是怎么实现的

(1)lock方法

下面看下是怎么加入等待队列的(跟着上图中的acquireQueued(addWaiter(Node.EXCLUSIVE), arg)部分)

(2)unlock方法

文章暂时讲到这里了,还有一些功能暂时没讲到,比如ConditionObject、超时、共享模式等,后续看情况补上吧。

AQS其实蛮复杂的,特别是队列里线程等待和唤醒的那部分,如果有讲错的地方,麻烦一定要在评论区里留言哦,万分感谢🙏。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章