前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JUC中的AQS

JUC中的AQS

作者头像
热心的大肚皮
发布2023-02-28 13:40:44
2120
发布2023-02-28 13:40:44
举报
文章被收录于专栏:程序猿日常笔记

AQS简介

  • java.util.concurrent中有许多可阻塞的类,如ReentrantLock、Semaphore、ReentrantReadWriteLock、CountDownLatch、SynchronousQueue和FutureTask等,这些阻塞类有一个共同点就是都是基于AQS构建的。
  • AQS(AbstractQueuedSynchronizer)即队列同步器。是用来构建锁或者其他同步组件的基础框架,是JUC并发包中的核心基础组件。
  • AQS解决了实现同步器是涉及到的大量细节问题,如:获取同步状态、FIFO同步队列等。在基于AQS构建的同步器中,只能在一个时刻发生阻塞,从而降低上下文切换的开销,提供了吞吐量。
  • AQS的主要使用方式是继承,子类通过继承AQS并实现它的抽象方法来管理同步状态。
  • AQS使用一个int类型的成员变量state来表示同步状态,当state>0时表示已经获取锁,当state=0时表示释放了锁。AQS提供了三个方法来对同步状态state进行操作。
  • AQS通过内置的FIFO同步队列来完成资源获取线程的排队工作,如果当前线程获取同步状态失败时,AQS会将当前线程以及等待状态等信息构造成一个Node并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,则会把节点中的线程唤醒,使其再次尝试获取同步状态。

AQS提供的主要方法

  • getState():返回同步状态的当前值
  • setState(int newState):设置当前同步状态
  • compareAndSetState(int expect, int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性
  • isHeldExclusively():当前同步器是否在独占式模式下被线程占用,一般该方法表示是否被当前线程独占
  • acquire(int arg):独占式获取同步状态,如果当前线程获取同步状态成功,则由改方法返回,否则,将会进入同步队列等待,该方法将会调用可重写的tryAcquire(int arg)方法
  • acquireInterruptibly(int arg):与acquire(int arg)相同,但是该方法响应中断,当前线程为获取到同步状态而进入到同步队列中,如果当前线程被中断,则该方法抛出InterruptedException异常并返回
  • acquireShared(int arg):共享式获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待,与独占式的主要区别是在同一时刻可以有多个线程获取到同步状态
  • acquireSharedInterruptibly(int arg):共享式获取同步状态,响应中断
  • release(int arg):独占式释放同步状态,该方法会在释放同步状态之后,将同步队列中第一个节点包含的线程唤醒
  • releaseShared(int arg):共享式释放同步状态

  • tryAcquire(int arg):独占式获取同步状态,获取同步状态成功后,其他线程需要等待该线程释放同步状态才能获取同步状态
  • tryRelease(int arg):独占式释放同步状态
  • tryAcquireNanos(int arg, long nanos):超时获取同步状态,如果当前线程在nanos时间内没有获取到同步状态,那么将会返回false,已经获取则返回true
  • tryAcquireShared(int arg):共享式获取同步状态,返回值大于等于0表示获取成功,否则获取失败
  • tryReleaseShared(int arg):共享式释放同步状态
  • tryAcquireSharedNanos(int arg, long nanosTimeout):共享式获取同步状态,增加超时限制

CLH同步队列

  • CLH同步队列是一个FIFO双向队列,AQS依赖它来完成同步状态的管理,当前线程如果获取同步状态失败时,AQS则会将当前线程已经等待状态等信息构造成一个节点(Node)并将其加入到CLH同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态。
  • 在CLH同步队列中,一个节点表示一个线程,它保存着线程的引用(thread)、状态(waitStatus)、前驱节点(prev)、后继节点(next) CLH同步队列结构

同步状态的获取与释放

  • AQS的设计模式采用了模板方法,子类通过继承的方式,实现AQS的抽象方法来管理同步状态,AQS提供了大量的模板方法来事项同步,主要分三类:独占式获取和释放同步状态、共享式获取和释放同步状态、查询同步队列中的等待线程情况。

ReentrantLock结构

独占式

  • 独占式指同一时刻仅有一个线程持有同步状态
  • 独占式同步状态获取:acquire(int arg)方法为AQS提供的模板方法,改方法为独占式获取同步状态,但是该方法对中断不敏感,也就是说由于线程获取同步状态失败加入到CLH同步队列中,后续对线程进行中断操作时,线程不会从同步队列中移除。
  • 各个方法的定义:
  • tryAcquire:去尝试获取锁,获取成功则设置锁状态并返回true,否则返回false。该方法自定义同步组件自己实现,该方法必须要保证线程安全的获取同步状态。
  • addWaiter:如果tryAcquire返回FALSE(获取同步状态失败),则调用该方法将当前线程加入到CLH同步队列尾部。
  • acquireQueued:当前线程会根据公平性原则来进行阻塞等待(自旋),直到获取锁为止;并且返回当前线程在等待过程中有没有中断过。
  • selfInterrupt:产生一个中断。
代码语言:javascript
复制
  //ReentrantLock的lock代码
  public void lock() {
        sync.lock();
    }
  //NonfairSync的lock代码
  final void lock() {
    if (compareAndSetState(0, 1))
      setExclusiveOwnerThread(Thread.currentThread());
    else
      acquire(1);
  }
  //AQS中的acquire
  public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
  改写为
  public final void acquire(int arg) {
        if (tryAcquire(arg)) {
            return;
        }
        Node newNode = addWaiter(Node.EXCLUSIVE);
        boolean interrupted = acquireQueued(newNode, arg);
        if (interrupted) {
            selfInterrupt();
        }
    }

acquire(int arg)方法流程图

  • 独占式获取响应中断:AQS提供了acquire(int arg)方法以供独占式获取同步状态,但是该方法对中断不响应,对线程进行中断操作后,该线程会依然位于CLH同步队列中等待获取同步状态,为了响应中断,AQS提供了acquireInterruptibly(int arg)方法,该方法在等待获取同步状态时,如果当前线程被中断了,会立即响应中断并抛出InterruptedException。

首先校验该线程是否已经中断了,如果是则抛出InterruptedException,否则执行tryAcquire(int arg)方法获取同步状态,如果获取成功,则直接返回,否则执行doAcquireInterruptibly(int arg)。

doAcquireInterruptibly(int arg)方法与acquire(int arg)方法仅有两个差别。

1.方法声明抛出InterruptedException异常。

]2.在中断方法处不再是使用interrupted标志,而是直接抛出InterruptedException异常。

  • 独占式超时获取

AQS除了提供上面两个方法外,还提供了一个增强版的方法:tryAcquireNanos(int arg,long nanos)。该方法为acquireInterruptibly方法的进一步增强,它除了响应中断外,还有超时控制。即如果当前线程没有在指定时间内获取同步状态,则会返回false,否则返回true。

  • 独占式同步状态释放

当线程获取同步状态后,执行完相应逻辑后就需要释放同步状态。AQS提供了release(int arg)方法释放同步状态。该方法同样是先调用自定义同步器自定义的tryRelease(int arg)方法来释放同步状态,释放成功后,会调用unparkSuccessor(Node node)方法唤醒后继节点。** 在AQS中维护着一个FIFO的同步队列,当线程获取同步状态失败后,则会加入到这个CLH同步队列的对尾并一直保持着自旋。在CLH同步队列中的线程在自旋时会判断其前驱节点是否为首节点,如果为首节点则不断尝试获取同步状态,获取成功则退出CLH同步队列。当线程执行完逻辑后,会释放同步状态,释放后会唤醒其后继节点。**

共享式

  • 共享式与独占式的最主要区别在于同一时刻独占式只能有一个线程获取同步状态,而共享式在同一时刻可以有多个线程获取同步状态。例如读操作可以有多个线程同时进行,而写操作同一时刻只能有一个线程进行写操作,其他操作都会被阻塞。
  • 共享式同步状态获取 AQS提供acquireShared(int arg)方法共享式获取同步状态:
  • 方法首先是调用tryAcquireShared(int arg)方法尝试获取同步状态,如果获取失败则调用doAcquireShared(int arg)自旋方式获取同步状态,共享式获取同步状态的标志是返回 >= 0 的值表示获取成功。
  • tryAcquireShared(int arg)方法尝试获取同步状态,返回值为int,当其 >= 0 时,表示能够获取到同步状态,这个时候就可以从自旋过程中退出。
  • acquireShared(int arg)方法不响应中断,与独占式相似,AQS也提供了响应中断、超时的方法,分别是:acquireSharedInterruptibly(int arg)、tryAcquireSharedNanos(int arg,long nanos)。
  • 共享式同步状态释放 获取同步状态后,需要调用release(int arg)方法释放同步状态
  • 因为可能会存在多个线程同时行释放同步状态资源,所以需要确保同步状态安全地成功释放,一般都是通过CAS和循环来完成的。

阻塞和唤醒线程

  • 在线程获取同步状态时如果获取失败,则加入CLH同步队列,通过通过自旋的方式不断获取同步状态,但是在自旋的过程中则需要判断当前线程是否需要阻塞,其主要方法在acquireQueued()
  • 在获取同步状态失败后,线程并不是立马进行阻塞,需要检查该线程的状态,检查状态的方法为 shouldParkAfterFailedAcquire(Node pred, Node node) 方法,该方法主要靠前驱节点判断当前线程是否应该被阻塞。

扩展------为何大神们写代码的时候,喜欢用for(;;) 而不用 while(1)?

不多说 直接上C的源码

代码语言:javascript
复制
    编译前               编译后 
    while (1);          mov eax,1  
                        test eax,eax 
                        je foo+23h
                        jmp foo+18h


    编译前               编译后 
    for (;;);         jmp foo+23h

一目了然,for (;;)指令少,不占用寄存器,而且没有判断跳转,在空间和时间上,比while (1)性能都要高很多。

也就是说两者在在宏观上完全一样的逻辑,但是底层完全不一样,for相对于来说更加简洁明了。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-12-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序猿日常笔记 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • AQS提供的主要方法
  • CLH同步队列
  • 同步状态的获取与释放
  • ReentrantLock结构
    • 独占式
      • 共享式
      • 阻塞和唤醒线程
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档