AQS概述
AQS(AbstractQueuedSynchronizer)是Java中并发编程的关键类之一,它提供了一种用于构建锁和其他同步器的框架。AQS内部维护了一个FIFO双向队列,用来管理获取同步状态失败的线程,并且通过内置的状态来控制线程的阻塞和唤醒。
AQS(AbstractQueuedSynchronizer)是Java并发包java.util.concurrent.locks中的一个核心类,它提供了一种基于FIFO(先进先出)队列的阻塞锁和相关的同步器(如ReentrantLock、Semaphore、CountDownLatch等)的实现框架。AQS被设计为一个依赖状态的同步器,它使用一个整数来表示同步状态,这个状态可以通过原子操作进行更新。它是一个 Java 提高的底层同步工具类,用一个 int 类型的变量表示同步状态,并提供了一系列的 CAS 操作来管理这个同步状态。AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLock,Semaphore,其他的诸如 ReentrantReadWriteLock,SynchronousQueue,FutureTask 等等皆是基于 AQS的
AQS 是多线程同步器,它是 J.U.C 包中多个组件的底层实现,如 Lock、CountDownLatch、Semaphore 等都用到了 AQS.从本质上来说,AQS 提供了两种锁机制,分别是排它锁,和共享锁。
排它锁,就是存在多线程竞争同一共享资源时,同一时刻只允许一个线程访问该共享资源,也就是多个线程中只能有一个线程获得锁资源,比如 Lock 中的ReentrantLock 重入锁实现就是用到了 AQS 中的排它锁功能。
共享锁也称为读锁,就是在同一时刻允许多个线程同时获得锁资源,比如CountDownLatch 和 Semaphore 都是用到了 AQS 中的共享锁功能
AQS的核心思想是使用一个整型的state变量来表示同步状态,当state为0时表示没有线程获取到同步状态,大于0表示有线程获取到同步状态,小于0通常表示有线程在等待获取同步状态。具体来说,AQS提供了以下几个主要方法:
- acquire(int arg):尝试获取同步状态,如果获取失败则会进入队列进行阻塞等待。
- release(int arg):释放同步状态,可能会唤醒队列中等待的线程。
- acquireShared(int arg):尝试获取共享同步状态,如果获取失败则会进入队列进行阻塞等待。
- releaseShared(int arg):释放共享同步状态,可能会唤醒队列中等待的线程。
AQS为我们提供了一个非常灵活的同步状态管理框架,使得我们可以相对容易地实现各种同步器,比如ReentrantLock、CountDownLatch、Semaphore等。通过扩展AQS并实现相应的获取和释放同步状态的方法,我们可以定制符合自己需求的同步器。
总的来说,AQS为并发编程提供了强大的基础设施,能够帮助我们更好地理解和实现各种复杂的同步机制。
AQS的主要功能包括: - 同步状态管理:AQS使用一个int类型的变量来保存同步状态,该状态可以被获取、设置或者原子地比较并设置。
- FIFO队列:AQS维护了一个FIFO队列,用于管理等待获取同步状态的线程。当一个线程尝试获取同步状态失败时,它会被放入队列中等待。
- 可重入锁:AQS支持可重入锁的特性,即同一个线程可以多次获取同步状态。
- 条件变量:AQS提供了与Object.wait()和Object.notify()类似的功能,但更加灵活和高效。通过继承AQS的子类,可以定义自己的条件变量。
AQS的实现非常精巧,它利用了Java的Unsafe类提供的原子操作来确保对同步状态的安全访问。同时,AQS还提供了大量的模板方法,使得开发者可以方便地扩展和定制自己的同步器。
AQS作为Java并发包中的一个核心组件,被广泛应用于各种并发同步组件的实现中,如ReentrantLock、CountDownLatch、Semaphore等。
AQS 在实际应用中的使用
AQS 是构建锁和同步器的基础,许多常用的并发工具都是基于 AQS 实现的:
ReentrantLock:可重入锁,支持公平锁和非公平锁,通过 AQS 的独占模式实现。
Semaphore:信号量,通过 AQS 的共享模式实现。它用于控制同时访问某个特定资源的线程数量
CountDownLatch:倒计数锁,通过 AQS 的共享模式实现,一个同步辅助类,它允许一个或多个线程等待其他线程完成操作
ReentrantReadWriteLock:读写锁,使用 AQS 实现读锁的共享模式和写锁的独占模式。一个可重入的读写锁,它允许多个读线程同时访问共享资源,但写线程会阻塞读线程和其他写线程。
CyclicBarrier:一个可以让一组线程互相等待,直到所有线程都到达某个公共屏障点的同步辅助类。
这些同步器都通过继承AQS并实现其模板方法来实现各自的功能。
AQS 支持两种同步方式
(1)独占式
(2)共享式
这样方便使用者实现不同类型的同步组件,独占式如 ReentrantLock,共享式如 Semaphore,
CountDownLatch,组 合 式 的 如 ReentrantReadWriteLock。总之,AQS 为使用提供了底层支撑,如何组装实现,使用者可以自由发挥
AQS中有个状态维护字段state,volatile修饰,保证可见性,代表线程是否抢占到锁,或者锁的重入次数
AQS中有一个CLH队列,是一个FIFO的双向链表,存着需要抢占锁的线程、等待状态等信息。
AQS的典型的设计模式是模板方法模式,提供tryAcquire等钩子方法,给予不同子类不同的实现。
AQS中唤醒线程源码在release方法,唤醒的是head的next节点的线程,而不是随机
AQS为什么要用双向链表
首先,双向链表有两个指针,一个指针指向前置节点,一个指针指向后继节点。所以,双向链表可以支持常量 O(1) 时间复杂度的情况下找到前驱节点。因此,双向链表在插入和删除操作的时候,要比单向链表简单、高效。从双向链表的特性来看,我认为 AQS 使用双向链表有三个方面的原因
第1个原因,没有竞争到锁的线程加入到阻塞队列,并且阻塞等待的前提是,当前线程所在节点的前置节点是正常状态,这样设计是为了避免链表中存在异常线程导致无法唤醒后续线程的问题。
所以,线程阻塞之前需要判断前置节点的状态,如果没有指针指向前置节点,就需要从 Head节点开始遍历,性能非常低
第2个原因,在 Lock 接口里面有一个,lockInterruptibly()方法,这个方法表示处于锁阻塞的线程允许被中断。
也就是说,没有竞争到锁的线程加入到同步队列等待以后,是允许外部线程通过interrupt()方法触发唤醒并中断的。这个时候,被中断的线程的状态会修改成 CANCELLED。而被标记为CANCELLED 状态的线程,是不需要去竞争锁的,但是它仍然存在于双向链表里面。
这就意味着在后续的锁竞争中,需要把这个节点从链表里面移除,否则会导致锁阻塞的线程无法被正常唤醒。在这种情况下,如果是单向链表,就需要从 Head 节点开始往下逐个遍历,找到并移除异常状态的节点。同样效率也比较低,还会导致锁唤醒的操作和遍历操作之间的竞争
第3个原因,是为了避免线程阻塞和唤醒的开销,所以刚加入到链表的线程,首先会通过自旋的方式尝试去竞争锁。但是实际上按照公平锁的设计,只有头节点的下一个节点才有必要去竞争锁,后续的节点竞争锁的意义不大。否则,就会造成羊群效应,也就是大量的线程在阻塞之前尝试去竞争锁带来比较大的性能开销。
所以,为了避免这个问题,加入到链表中的节点在尝试竞争锁之前,需要判断前置节点是不是头节点,如果不是头节点,就没必要再去触发锁竞争的动作。所以这里会涉及到前置节点的查找,如果是单向链表,那么这个功能的实现会非常复杂。
aqs底层原理
AQS(AbstractQueuedSynchronizer)是Java并发包java.util.concurrent.locks中的一个抽象类,用于构建锁或其他同步组件的基础框架。AQS的底层原理涉及到同步状态管理、线程排队机制、线程阻塞与唤醒、模板方法模式、节点状态管理、锁的类型以及公平锁与非公平锁等多个方面。这些机制共同构成了AQS这一强大的同步器框架,为Java并发编程提供了坚实的支持
AQS 是 Java 并发包中一个强大而灵活的同步框架,通过 FIFO 队列、CAS 操作、自旋锁和条件队列等机制,实现了高效的线程同步和锁管理。理解 AQS 的底层原理有助于深入掌握 Java 并发编程的核心技术,并能够更好地使用和实现高性能的并发工具。
AQS 的基本结构
AQS 主要由以下几个部分组成:
同步状态 (state):
AQS 通过一个 volatile int state 变量来表示同步状态,子类通过实现 tryAcquire、tryRelease、tryAcquireShared 和 tryReleaseShared 方法来定义具体的同步逻辑。
FIFO 同步队列:
AQS 使用一个 FIFO 队列来管理线程的阻塞与唤醒。每个等待获取同步状态的线程都会被封装成一个 Node 节点,加入到队列中。
独占模式和共享模式:
AQS 支持独占模式 (exclusive mode) 和共享模式 (shared mode),分别对应独占锁(如 ReentrantLock)和共享锁(如 Semaphore)。
AQS 的核心类和方法
Node 类:
Node 是 AQS 内部的静态类,用于表示等待队列中的节点。每个节点包含线程的引用、前驱节点和后继节点的引用,以及状态字段。
acquire 方法:
acquire(int arg):独占模式下获取同步状态,如果获取失败,线程会被加入到等待队列中。
acquireShared(int arg):共享模式下获取同步状态,如果获取失败,线程会被加入到等待队列中。
release 方法:
release(int arg):独占模式下释放同步状态,成功释放后会唤醒后继节点。
releaseShared(int arg):共享模式下释放同步状态,成功释放后会唤醒后继节点。
AQS 的工作流程
独占模式 (exclusive mode)
acquire:
尝试获取同步状态,如果成功,直接返回;如果失败,线程会被封装成 Node 节点,加入到等待队列中。
线程在队列中自旋等待状态变化,如果前驱节点是头节点且再次尝试获取同步状态成功,线程被唤醒并出队。
release:
尝试释放同步状态,如果成功且有等待的节点,将头节点的后继节点唤醒。
共享模式 (shared mode)
acquireShared:
尝试获取同步状态,如果成功且允许共享,直接返回;如果失败,线程会被封装成 Node 节点,加入到等待队列中。
线程在队列中自旋等待状态变化,如果前驱节点是头节点且再次尝试获取同步状态成功,线程被唤醒并出队。
releaseShared:
尝试释放同步状态,唤醒所有等待的共享节点。
AQS 的核心原理
CAS 操作:
AQS 大量使用 CAS 操作(Compare-And-Swap)来实现线程安全的状态更新。CAS 操作是一种无锁算法,能确保高效地更新同步状态。
自旋锁:
线程在等待队列中不会立即阻塞,而是先自旋等待一段时间,通过不断尝试获取锁来减少阻塞开销。
条件队列:
AQS 还支持条件队列(Condition),用于实现条件变量。条件队列和同步队列类似,也是基于 FIFO 的队列,用于管理等待条件的线程。
底层原理可以概括
- 同步状态管理
AQS内部使用一个volatile int state变量来表示同步状态。这个状态变量是共享的,通过CAS(Compare-And-Swap)操作来保证对其修改的原子性。state的值可以表示锁是否被占用、重入次数、信号量数量等,具体含义由子类决定。 - 线程排队机制
AQS内部维护了一个虚拟的FIFO(先进先出)双向队列(通常称为CLH队列),用于存储等待获取锁的线程。
当线程尝试获取锁失败时,AQS会将该线程封装成一个Node对象,并加入到同步队列的尾部。同步队列的头部是虚拟的,不存储任何线程,仅用于标记队列的开始。 - 线程阻塞与唤醒
当线程被添加到同步队列后,会进入一个自旋等待状态,不断检查前置节点是否为头节点且已释放锁。
如果条件不满足,线程会通过LockSupport.park()方法被阻塞。当锁被释放时,AQS会唤醒同步队列中等待时间最长的线程(即头节点的后继节点),让其尝试获取锁。 - 模板方法模式
AQS采用了模板方法模式,定义了获取锁(acquire)、释放锁(release)、共享获取锁(acquireShared)、共享释放锁(releaseShared)等模板方法。这些模板方法内部调用了由子类实现的抽象方法,如tryAcquire、tryRelease等,以允许子类自定义同步状态的管理逻辑。 - 节点状态管理
同步队列中的每个Node对象都包含了一系列状态信息,如线程引用、前置节点引用、后置节点引用、等待状态等。等待状态(waitStatus)用于表示节点的状态,如是否被取消、是否需要被唤醒等。 - 锁的类型
AQS支持两种类型的锁:独占锁和共享锁。
独占锁:每次只允许一个线程持有锁,如ReentrantLock。
共享锁:允许多个线程同时获取锁,并发访问共享资源,如ReentrantReadWriteLock的读锁部分。 - 公平锁与非公平锁
AQS还支持公平锁和非公平锁的实现。
公平锁:按照线程在队列中的排队顺序,先到者先拿到锁。
非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的。
170万+

被折叠的 条评论
为什么被折叠?



