AQS(AbstractQueuedSynchronizer)是Java并发包中的一个核心同步器框架,它定义了一套多线程访问共享资源的同步机制。
一、AQS的定义
AQS,全称AbstractQueuedSynchronizer,即抽象队列同步器,是Java中的一个抽象类。它是构建锁或者其他同步组件的基础框架,通过继承AQS,子类可以实现自己的同步逻辑,而无需深入了解底层的同步机制。AQS为Java并发编程提供了极大的便利,使得开发者可以轻松地实现自定义的同步器,满足特定的同步需求。
二、AQS的功能
AQS主要提供了两种方式来实现同步器:独占模式和共享模式。这两种模式分别适用于不同的同步场景。
- 独占模式:在独占模式下,一次只有一个线程能够获取到同步状态。这种模式适用于需要严格保证线程安全性的场景,如ReentrantLock(可重入锁)。在独占模式下,AQS会维护一个同步队列,用于存放等待获取锁的线程。当一个线程成功获取锁后,它会成为持有锁的线程,并将该信息记录在同步队列的头部。其他尝试获取锁的线程会被放入同步队列的尾部,并处于阻塞状态,直到持有锁的线程释放锁为止。
- 共享模式:在共享模式下,多个线程可以同时获取到同步状态。这种模式适用于允许多个线程并发访问共享资源的场景,如Semaphore(信号量)、CountDownLatch(倒计时器)等。在共享模式下,AQS同样会维护一个同步队列,但不同的是,多个线程可以同时处于同步状态,即它们可以同时访问共享资源。当共享资源的可用量发生变化时,AQS会相应地更新同步状态,并唤醒等待队列中的线程。
除了提供独占模式和共享模式外,AQS还具备以下功能:
- 状态管理:AQS使用一个volatile类型的int变量state来表示同步状态。该状态变量可以通过getState、setState、compareAndSetState等方法进行访问和修改。由于state是volatile类型的,因此它保证了变量的可见性和有序性,从而避免了多线程环境下的竞争条件。
- 线程排队与阻塞:当线程无法获取同步状态时,AQS会将其放入同步队列中等待。同步队列是一个基于双向链表的FIFO队列,队列中的每个节点代表一个等待获取同步状态的线程。线程在加入队列时会处于阻塞状态,直到被唤醒为止。AQS使用LockSupport.park()和unpark(Thread thread)方法来实现线程的阻塞与唤醒。
- 公平与非公平策略:AQS支持公平锁和非公平锁两种策略。在公平锁模式下,线程按照加入同步队列的顺序依次获取锁;而在非公平锁模式下,线程可能会插队获取锁。非公平锁的性能通常更高,因为它减少了线程切换和上下文切换的开销。但需要注意的是,非公平锁可能会导致线程饥饿问题,即某些线程可能长时间无法获取到锁。
三、AQS的工作原理
AQS的工作原理可以概括为以下三个步骤:
- 线程尝试获取锁:当一个线程尝试获取锁时,它会首先检查同步状态(state)的值。如果state为0(表示没有线程持有锁),则该线程会尝试使用CAS操作将state设置为1(表示该线程持有锁)。如果CAS操作成功,则该线程成功获取锁;如果失败,则说明有其他线程已经持有锁,该线程需要进入同步队列等待。
- 线程加入同步队列:当线程无法获取锁时,AQS会将其封装成一个节点(Node)并加入同步队列的尾部。节点中包含了线程引用、等待状态等信息。同时,该线程会处于阻塞状态,直到被唤醒为止。
- 线程被唤醒并尝试获取锁:当持有锁的线程释放锁时,它会修改同步状态(state)的值,并唤醒同步队列中的第一个线程(在公平锁模式下)或随机一个线程(在非公平锁模式下)。被唤醒的线程会再次尝试获取锁,如果成功,则继续执行后续操作;如果失败,则继续等待。
四、AQS的应用
AQS在Java并发包中被广泛应用于各种同步组件中,如ReentrantLock、Semaphore、CountDownLatch等。以下是一些基于AQS实现的同步组件的示例:
- ReentrantLock:ReentrantLock是一个可重入的互斥锁,它允许同一个线程多次获得该锁。ReentrantLock是基于AQS的独占模式实现的。当一个线程成功获取锁后,它会将AQS的同步状态(state)设置为1,并持有该锁。其他尝试获取锁的线程会被放入同步队列中等待。当持有锁的线程释放锁时,它会将同步状态(state)重置为0,并唤醒同步队列中的第一个线程。
- Semaphore:Semaphore是一个信号量,它用于控制对共享资源的并发访问数量。Semaphore是基于AQS的共享模式实现的。它维护了一个计数器来表示可用资源的数量。当线程尝试获取资源时,它会检查计数器的值。如果计数器大于0,则线程可以成功获取资源,并将计数器减1;如果计数器等于0,则线程需要进入同步队列等待。当释放资源时,线程会将计数器加1,并唤醒同步队列中的线程。
- CountDownLatch:CountDownLatch是一个倒计时器,它允许一个或多个线程等待其他线程完成一组操作。CountDownLatch也是基于AQS的共享模式实现的。它维护了一个计数器来表示需要等待的线程数量。当线程调用await()方法时,如果计数器不为0,则线程会进入等待状态,并将其封装成节点加入AQS的同步队列中。当其他线程调用countDown()方法时,计数器的值会递减。当计数器的值减至0时,AQS会唤醒同步队列中的所有线程。
五、AQS与synchronized的对比
在Java中,除了AQS外,还可以使用synchronized关键字来实现线程同步。然而,AQS同步和synchronized关键字同步采用的是两种不同的机制。
- synchronized:synchronized关键字经过编译后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令。这两个字节码指令需要关联到一个监视对象(即锁对象)。当线程执行monitorenter指令时,它会尝试获取监视对象的锁。如果成功获取锁,则线程可以进入同步块执行操作;如果失败,则线程会被阻塞,直到锁被释放为止。当线程离开同步块时,它会执行monitorexit指令来释放锁。synchronized的优点是简单易用,但缺点是缺乏灵活性,无法控制释放锁的时机和方式。
- AQS:与synchronized不同,AQS使用了大量的CAS操作和volatile变量来实现状态同步,避免了直接阻塞线程和减少线程切换的开销。AQS内部使用了变种的CLH队列锁,这是一种基于链表的高性能、公平队列锁。在AQS中,每个线程只会监视其前一个节点的状态来判断自己是可以继续争抢锁还是需要阻塞。这种方式相比传统的所有线程都监视读写一个同步变量来说,可以减少多CPU的缓存同步开销。此外,AQS还支持公平锁和非公平锁两种策略,以及可重入性、可中断性、超时等丰富的同步特性。这些特性使得基于AQS实现的同步工具更加灵活、强大。
六、总结
AQS(AbstractQueuedSynchronizer)是Java并发包中的一个核心同步器框架,它定义了一套多线程访问共享资源的同步机制。通过继承AQS并实现其抽象方法,子类可以轻松地实现自定义的同步器,满足特定的同步需求。AQS主要提供了独占模式和共享模式两种同步方式,并具备状态管理、线程排队与阻塞、公平与非公平策略等功能。在Java并发包中,AQS被广泛应用于各种同步组件中,如ReentrantLock、Semaphore、CountDownLatch等。与synchronized关键字相比,AQS具有更高的灵活性和性能优势。因此,在需要实现复杂的同步逻辑时,可以考虑使用AQS来构建自定义的同步器。