AQS(AbstractQueuedSynchronizer)是Java中提供的一个同步器框架,可以用来实现各种同步工具,如ReentrantLock、Semaphore、CountDownLatch等。本文将从原理、实现方式、应用场景以及代码实现等方面来深入理解AQS。
1. AQS原理
AQS通过一个FIFO队列来管理线程的竞争,队列中的每个节点都代表一个等待线程,当某个线程请求共享资源时,如果发现有其他线程已经占用了这个资源,它就会被放到队列的尾部等待;当占用资源的线程释放资源时,它会通知队列的头部节点,使它可以重新竞争资源。整个过程就是一个典型的“先进先出”的同步机制。
AQS的核心是一个volatile变量state,通过对state的操作来判断当前线程是否可以获得共享资源。当state为0时,表示当前没有线程占用资源,其他线程可以通过compareAndSetState()方法来竞争资源;当state为1时,表示当前已有线程占用资源,其他线程必须进入队列等待。
此外,AQS还提供了两个方法acquire()和release()来实现单线程对共享资源的获取和释放。
2. AQS实现方式
AQS的实现方式主要包括三个部分:同步状态的管理、等待队列的管理以及线程的阻塞与唤醒。
2.1 同步状态的管理
AQS通过一个volatile变量state来管理同步状态。在AQS中,state代表的是资源的数量,一般情况下,state等于0表示资源没有被占用,大于0表示资源已经被占用。在使用AQS实现同步器时,我们需要根据具体使用场景来赋予state合适的含义。
AQS提供了compareAndSetState()方法来对state进行原子操作,保证并发下的操作的正确性。compareAndSetState()方法的实现代码如下:
protected final boolean compareAndSetState(int expect, int update) { // see below for intrinsics setup to support this return unsafe.compareAndSwapInt(this, stateOffset, expect, update); } private static final Unsafe unsafe = Unsafe.getUnsafe(); private static final long stateOffset; static { try { stateOffset = unsafe.objectFieldOffset (AbstractQueuedSynchronizer.class.getDeclaredField("state")); } catch (Exception ex) { throw new Error(ex); } }
在上面的代码中,我们可以看到,compareAndSetState()方法实际上是通过调用sun.misc.Unsafe类的compareAndSwapInt()方法来实现的。
2.2 等待队列的管理
AQS中的等待队列是一个FIFO队列,可以通过同步器提供的acquire()方法向队列中添加一个节点,也可以通过release()方法将头节点出队,唤醒等待队列中的下一个节点。AQS中等待队列的实现是通过一个双向链表来实现的,每个节点都有前驱节点和后继节点。
在AQS中,队列中的每个节点都是一个ConditionObject对象,它是AQS的内部类,继承了Condition接口。ConditionObject实现了Condition接口的所有方法,它能够使线程以安全的方式等待特定的条件。
2.3 线程的阻塞与唤醒
当线程请求共享资源时,如果发现资源已被占用,线程就会被阻塞,并被加入到等待队列中。线程被阻塞后,它就会进入WAITING状态。
在AQS中,线程的阻塞与唤醒是通过LockSupport类来实现的。LockSupport是Java中用于线程阻塞与唤醒的工具类,它提供了park()和unpark()方法来控制线程的阻塞和唤醒。
3. AQS应用场景
AQS广泛应用于Java并发包中的各种同步器,如ReentrantLock、Semaphore、CountDownLatch等。它为我们提供了一种可扩展、可重用的同步机制,使我们能够方便地实现各种同步工具。
4. AQS代码实现
下面我们以ReentrantLock为例,演示如何使用AQS实现一个可重入的互斥锁。
Lock接口是Java并发包中提供的一个用于互斥同步的接口,它定义了lock()、unlock()、tryLock()等方法。在AQS中,我们可以通过继承AbstractQueuedSynchronizer类来实现Lock接口的所有方法。
public class ReentrantLock implements Lock, java.io.Serializable { private final Sync sync; abstract static class Sync extends AbstractQueuedSynchronizer { abstract void lock(); // 其他方法略 } static final class NonfairSync extends Sync { // 具体实现略 } static final class FairSync extends Sync { // 具体实现略 } }
在上面的代码中,我们可以看到,ReentrantLock中定义了一个Sync类,它继承了AbstractQueuedSynchronizer,Sync类中定义了lock()、unlock()等方法,这些方法实际上是通过调用AQS的acquire()、release()等方法来实现的。
为了提高性能,ReentrantLock还提供了两种Sync的实现方式:NonfairSync和FairSync。NonfairSync采用非公平锁的方式,即线程可以随时争夺锁,无需排队;而FairSync则采用公平锁的方式,即线程按照先来先服务的原则进行排队争夺锁。在ReentrantLock的构造函数中,我们可以根据需要来选择使用哪种Sync的实现方式。
最后,我们来看一下ReentrantLock的lock()方法的实现:
public void lock() { sync.acquire(1); } public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
在lock()方法中,我们调用了Sync的acquire()方法来获取锁。在acquire()方法中,我们首先调用tryAcquire()方法尝试获取锁,如果获取成功,直接返回;否则,将当前线程加入到等待队列中,并阻塞当前线程。当其他线程释放锁时,acquireQueued()方法会对等待队列中的下一个节点进行唤醒,让它重新尝试获取锁。
总结
通过上述分析,我们可以发现,AQS是Java中用于实现同步器的一个重要框架。借助AQS,我们可以方便地实现各种同步工具,如ReentrantLock、Semaphore、CountDownLatch等,为并发编程提供了强有力的支持。
小故事
从前有个村子,村子里的人们每天都要从家里到集市上买卖东西。为了让大家更有规律地出门,村子里决定修建一座门,每天早上门口一打开,大家就可以出门了。但是,每天出门的人太多了,门口经常会拥堵,导致一些人出门需要等待很长时间。于是,村子里的人又决定在门口添加一个能够控制出门的机制,这个机制能够很好地控制出门的人数,让大家更有规律地出门。
这个门口的机制就像是同步器AQS的底层工作原理。AQS其实就是一种锁的机制,它可以让代码块在并发环境中互斥地执行,避免竞争问题,并提高并发性能。
在同步器AQS的底层实现中,比如ReentrantLock,就是通过“门”的模式进行控制。首先,AQS会维护一个state状态值,它代表着当前的锁状态。当一个线程想要进入同步代码块时,它就需要先尝试去获取锁。如果此时state值为0,那么线程就可以获取到锁,并将state值设置为1,表示它已经拿到了锁。
但是,如果此时state值不为0,那么线程就不能获取到锁,它就需要进入等待状态,等待其他线程释放锁。等待的线程会被放到同步队列中,等待队列中的线程都会被阻塞,直到某个线程释放了锁,才会有一个线程从等待队列中被唤醒并进入同步代码块执行。
这个“门”的机制就是同步器AQS的底层工作原理,通过控制进入同步代码块的线程数,避免竞争问题,让代码块在并发环境中互斥地执行,提高并发性能。