Java教程

AQS源码解读

本文主要是介绍AQS源码解读,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

  抽象队列同步器(Abstract Queued Synchronizer,AQS)作为并发包JUL中一个基础组件,用来实现各种锁和同步组件,AQS主要由状态state变量、加锁线程和等待队列组成。AQS定义了多线程访问共享资源的框架,AQS定义了Exclusive(Reentrantlock)和share(Semaphore和CountDownLacth)两种资源共享方式,在不同的组件中都有基于AQS的自定义同步器,不同的自定义同步器主要区别在获取state的方式不同。

  AQS内通过一个FIFO队列来管理资源获取线程的排队工作,并通过一个int型的变量来表示线程持有锁的状态,通过CAS锁对int变量进行修改。int变量实现线程之间互斥,state等于1表示共享资源已经被线程加锁;state等于0,表示共享资源没有被人加锁;state大于1表示共享资源已经被同一个线程多次加锁。下图为AQS和其它类之间的一个集成依赖关系。

在这里插入图片描述
在这里插入图片描述

任务队列
java并发包JUC不同的锁内部都对AQS进行了具体实现,下面以ReentrantLock为例,具体说明ReentrantLokc如何通过AQS实现加锁和释放锁。

1.Reentranlock整体加锁过程

  第一个过程是ReentranLock整体的加锁过程,这个过程主要描述了一个新任务的执行时机和新任务入队时机。

在这里插入图片描述

ReentrantLock的实现过程,在ReentrantLock通过集成AQS事项了公平了非公平的同步器,无参方法创建ReentrantLock时创建,内部调用的是非公平同步器。
在这里插入图片描述

public void lock() {
//调用非公平阻塞的lock方法
        sync.lock();
}

final void lock() {
//state初试值为0,表示第一个线程
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);//	不是第一个线程
}

2.任务入队过程

第二个过程是任务阻塞入队的过程,在真正阻塞之前,进入到任务等候队列,会两次尝试去获得资源锁,两次都获取不到,任务线程才会进入阻塞状态,等待前面的线程执行完毕。线程使用了LockSupport的自旋锁进行线程阻塞。

在这里插入图片描述
无法获得共享资源锁的线程会被封装在一个Node中,被阻塞后放在任务队列中等待被唤醒。Node中有一个表示线程状态的标志位waitStatus和记录阻塞线程的Thread变量。
Node中waitStatus的值为:
0:创建一个node的默认值。
CANCELLED(1):表示线程获取锁的请求已经被取消。
CONDITION(-2):表示节点在等待队列中,节点线程等待被唤醒。
PROPAGATE(-3):当先线程处于SHARED情况下,该字段才会被使用。
SIGNAL(-1):表示线程已经准备好了,等待资源被释放了。
最终任务队列中节点状态为:
1) 除了第一个节点,所有节点都处于阻塞状态;
2) 除了尾节点,所有节点waitStatus==Node.SIGNAL。

3.Reentranlock释放锁过程

第三个过程是等待队列中的线程被唤醒的过程,主要描述了等待线程唤醒时机,唤醒线程从等待队列到执行所做的一些动作。
在这里插入图片描述

Q1:为什么在释放锁的过程中从队列尾往前找第一个WaitStatus不为CANCELLED的NODE节点进行释放。

 Node s = node.next;//node为head节点
 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);

因为Node节点在入队的时候,保证了prev的强一致性,node的next的是弱一致性。维护一致性通常需要牺牲部分性能,为了进一步的提升性能,使用了弱一致性。

node.prev = pred;//无条件执行
if (compareAndSetTail(pred, node)) {
    pred.next = node;//if满足才能执行
    return node;
}

Q2:AQS为什么使用双向队列作为任务等待队列?

AQS使用单向队列也能实现任务队列,为什么还要使用到双向队列,通过head指针和tail指针就可以方便节点的移除和增加,但是在很多场合,对当前节点的操作,需要知道前一个节点的状态,为了节省访问上一个节点的时间复杂度,使用了双向队列,查询当前节点上一个节点的时间复杂度为O(1)。

例如:
 final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
           final Node p = node.predecessor();//需要获取前驱节点信息
           ...
 }

Reference

[1] https://www.bilibili.com/video/BV1Hy4y1B78T
[2]https://monkeysayhi.github.io/2017/12/05/%E6%BA%90%E7%A0%81%7C%E5%B9%B6%E5%8F%91%E4%B8%80%E6%9E%9D%E8%8A%B1%E4%B9%8BReentrantLock%E4%B8%8EAQS%EF%BC%881%EF%BC%89%EF%BC%9Alock%E3%80%81unlock/.

这篇关于AQS源码解读的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!