面试官最爱问的AQS,一文带你吃透!

一、从实际问题引入 AQS

在多线程的编程世界里,我们常常会遇到这样的场景:多个线程同时访问和修改共享资源。就好比多个顾客同时抢购商店里仅剩的几件商品,谁能抢到,什么时候抢到,都充满了不确定性。在代码中,如果不加以控制,就会引发数据不一致、竞态条件等问题,严重影响程序的正确性和稳定性。​

举个简单的例子,假设我们有一个银行账户类,多个线程同时进行取款操作。如果没有合理的同步机制,就可能出现账户余额被错误扣除,甚至出现负数余额的情况。代码示例如下:

public class BankAccount {
    private int balance;

    public BankAccount(int initialBalance) {
        this.balance = initialBalance;
    }

    public void withdraw(int amount) {
        if (balance >= amount) {
            try {
                Thread.sleep(100); // 模拟一些业务操作时间
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            balance -= amount;
            System.out.println(Thread.currentThread().getName() + " 成功取款 " + amount + ",余额为 " + balance);
        } else {
            System.out.println(Thread.currentThread().getName() + " 取款失败,余额不足");
        }
    }
}

在这个例子中,由于withdraw方法没有进行同步控制,当两个线程同时调用时,就可能出现问题。比如线程 1 判断余额足够后,还没来得及扣除余额,线程 2 也判断余额足够,然后两个线程都进行了扣除操作,导致余额错误。​

面对这些多线程资源竞争问题,我们通常会想到使用synchronized关键字、Lock接口等方式来实现线程同步。但是,当并发场景变得复杂,线程数量众多时,这些传统的同步方式可能会面临性能瓶颈和代码复杂度增加的问题。那么,有没有一种更高效、更通用的解决方案呢?这时候,AQS(AbstractQueuedSynchronizer)就登场了,它为我们提供了一种统一的框架来处理多线程同步问题 ,接下来,让我们一起深入了解 AQS 的奥秘。

二、AQS 是什么​

AQS,即 AbstractQueuedSynchronizer,抽象队列同步器 ,是 Java 并发包中构建锁和同步器的基础框架,它为实现依赖于先进先出(FIFO)等待队列的阻塞锁和同步器提供了一个通用框架。在 Java 并发编程的大厦中,AQS 就如同坚实的地基,许多重要的同步工具类都基于它构建而成。​

AQS 在 Java 并发编程中占据着举足轻重的地位。像我们常用的ReentrantLock(可重入锁)、CountDownLatch(倒计时器)、Semaphore(信号量)、ReentrantReadWriteLock(可重入读写锁)等,它们的底层实现都离不开 AQS。可以说,理解了 AQS,就相当于掌握了 Java 并发包的核心精髓。​

以ReentrantLock为例,它是一种可重入的互斥锁,允许同一个线程多次获取同一把锁。当我们使用ReentrantLock时,线程在获取锁的过程中,AQS 会负责管理线程的排队、阻塞与唤醒等操作,确保线程安全地访问共享资源。而CountDownLatch则允许一个或多个线程等待,直到其他线程完成一组操作。这里的等待和唤醒机制,同样是基于 AQS 实现的。再看Semaphore,它通过 AQS 来维护许可数,控制同时访问特定资源的线程数量。在这些工具类中,AQS 就像一个幕后英雄,默默地处理着复杂的线程同步逻辑,让我们能够更便捷、高效地编写多线程程序 。

三、AQS 的核心构成​

1. 状态变量 state​

AQS 使用一个volatile修饰的整型变量state来表示同步状态 ,这个变量在不同的同步器中有着不同的含义,就像一把万能钥匙,虽然形状固定,但在不同的锁孔里能开启不同的功能。​

以ReentrantLock为例,state表示锁的重入次数。当一个线程首次获取到锁时,state会被设置为 1。如果该线程再次获取同一把锁,state就会递增,变为 2,以此类推,表示线程对锁的重入。而当线程释放锁时,state会相应递减,直到state为 0 时,锁被完全释放 。​

再看Semaphore(信号量),state表示剩余的许可数。假设我们创建一个信号量对象并设置许可数为 5,那么初始时state就是 5。当一个线程调用acquire方法获取许可时,如果state大于 0,该线程就能获取到许可,同时state减 1;当state为 0 时,其他线程再调用acquire方法就会被阻塞,直到有其他线程调用release方法释放许可,state增加后,才有可能获取到许可 。​

state被volatile修饰,这确保了它在多线程环境下的可见性,即一个线程对state的修改,其他线程能够立即看到。并且,对state的操作大多基于CAS(Compare and Swap)算法,这保证了在多线程并发修改state时的原子性 ,避免了数据竞争和不一致的问题,就像给state穿上了一层坚固的铠甲,使其在复杂的多线程战场中能够稳定地发挥作用。​

2. 同步队列(CLH 队列)​

同步队列,也被称为 CLH 队列(Craig, Landin, and Hagersten 队列的变体),是 AQS 实现线程同步的关键数据结构,它就像是一个有序的排队等候区,所有等待获取锁的线程都在这里按顺序排队。​

从结构上看,同步队列是一个双向链表 ,每个节点(Node)代表一个等待线程,节点包含了线程的引用(thread)、前驱节点(prev)、后继节点(next)以及等待状态(waitStatus)等信息。​

当一个线程尝试获取锁失败时,它会被封装成一个节点加入到同步队列的尾部。例如,线程 A、B、C 依次尝试获取锁但都失败,那么它们会依次被加入到同步队列中,形成一个 A -> B -> C 的链表结构 。​

节点的状态waitStatus有多种取值,每种取值都有着特定的含义和作用:​

  • CANCELLED(值为 1):表示该节点的线程由于超时或被中断等原因,已经被取消等待,处于作废状态,这样的节点会被从等待队列中移除。比如线程在等待获取锁的过程中,被其他线程中断了,它对应的节点就可能会被标记为CANCELLED 。​
  • SIGNAL(值为 -1):意味着后继节点的线程需要被唤醒。当一个节点的状态被设置为SIGNAL时,它的后继节点的线程处于挂起或者即将挂起的状态。当前节点的线程如果释放了锁或者放弃获取锁并且状态为SIGNAL,那么将会尝试唤醒后继节点的线程,让其有机会去竞争锁 。​
  • CONDITION(值为 -2):表示线程在条件队列里面等待。当线程调用Condition的await方法时,会进入条件队列等待,此时节点的状态会被设置为CONDITION。直到其他线程调用Condition的signal方法,该节点才会从条件队列转移到同步队列中,重新参与锁的竞争 。​
  • PROPAGATE(值为 -3):主要用于共享模式下,释放共享资源时需要通知其它节点,保证下一个acquireShared操作能够得以执行 。​

在同步队列中,节点的入队、出队和取消操作都有着严格的流程:​

  • 入队操作:新节点入队时,首先会尝试快速插入到队列尾部。如果队列已经存在(即tail不为空),新节点会将当前tail作为前驱节点,然后通过CAS操作尝试更新tail为自己,如果更新成功,再将原来的tail的后继节点指向自己,完成快速入队。如果快速入队失败,就会进入完整的入队流程,通过循环不断尝试,直到成功入队 。​
  • 出队操作:当持有锁的线程释放锁时,会唤醒同步队列的头节点的后继节点(如果存在)。后继节点被唤醒后,会尝试获取锁。如果获取成功,该节点就会成为新的头节点,原来的头节点因为不再有线程关联,会被垃圾回收机制回收,实现了节点的出队 。​
  • 取消操作:当一个节点的线程被取消等待(例如超时或被中断),它会将自己的状态设置为CANCELLED。然后,它的前驱节点会跳过它,直接将后继节点指向它的后继节点(如果存在),将其从队列中移除,完成取消操作 。

四、AQS 的工作机制​

1. 独占模式​

独占模式下,同一时刻只有一个线程能够获取到同步状态,从而访问被保护的资源。以ReentrantLock为例,我们来深入剖析其在独占模式下的工作流程。​

获取锁:当一个线程调用ReentrantLock的lock方法获取锁时,实际上调用的是AQS的acquire方法,这个方法会尝试获取锁。在ReentrantLock中,tryAcquire方法是获取锁的核心逻辑 :

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) 
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}
  1. 首先检查当前状态state是否为 0,若为 0,表示锁未被持有 。此时,对于公平锁,会调用hasQueuedPredecessors方法检查同步队列中是否有前驱节点(即是否有其他线程在等待获取锁),如果没有前驱节点,说明当前线程是等待队列中最早的请求者,再通过CAS操作尝试将state从 0 设置为 1 ,若设置成功,将当前线程设置为锁的持有者,返回true,表示获取锁成功 。​
  2. 如果state不为 0,说明锁已被持有,此时判断当前线程是否是锁的持有者(即current == getExclusiveOwnerThread())。如果是,说明是可重入锁的重入操作,将state增加acquires(通常为 1),并返回true,表示获取锁成功 。​
  3. 如果以上条件都不满足,说明获取锁失败,返回false。​

当获取锁失败时,线程会通过addWaiter方法被封装成一个节点(Node)加入到同步队列的尾部,然后进入acquireQueued方法,在这个方法中,线程会在一个循环中不断尝试获取锁 :

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; 
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
  1. 首先获取当前节点的前驱节点p,如果p是头节点,说明当前节点是等待队列中最靠前的节点,有机会获取锁,再次调用tryAcquire方法尝试获取锁 。若获取成功,将当前节点设置为头节点(setHead(node)),并将原头节点的后继指针置空(p.next = null),以便垃圾回收 ,返回是否被中断的标志interrupted 。​
  2. 如果获取锁失败,调用shouldParkAfterFailedAcquire方法判断是否应该阻塞当前线程 。如果应该阻塞,调用parkAndCheckInterrupt方法阻塞当前线程,并返回是否被中断的状态 。如果线程在阻塞过程中被中断,interrupted会被设置为true 。

 释放锁:当线程调用ReentrantLock的unlock方法释放锁时,实际上调用的是AQS的release方法 :

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

首先调用tryRelease方法尝试释放锁,在ReentrantLock中,tryRelease方法的实现如下: 

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

该方法首先将state减去releases(通常为 1),如果当前线程不是锁的持有者,抛出IllegalMonitorStateException异常 。如果state减为 0,说明锁已被完全释放,将锁的持有者设置为null,并返回true,表示释放锁成功 。否则,返回false,表示锁还未被完全释放 。​

其次如果tryRelease返回true,说明锁已成功释放,获取头节点h。如果头节点不为空且头节点的等待状态不为 0,调用unparkSuccessor方法唤醒头节点的后继节点对应的线程 ,使其有机会竞争锁 。unparkSuccessor方法会从尾节点开始向前查找,找到距离头节点最近的处于等待状态(waitStatus <= 0)的节点,并调用LockSupport.unpark方法唤醒该节点对应的线程 。​

公平锁和非公平锁的区别及实现原理:ReentrantLock可以创建公平锁和非公平锁 。公平锁保证线程按照请求锁的顺序获取锁,即先到先得 ;非公平锁则不保证获取顺序,线程可以在锁可用时直接竞争获取,可能会出现后到的线程先获取到锁的情况 。​

公平锁和非公平锁的主要区别在于获取锁时的tryAcquire方法实现。公平锁在获取锁时,会先调用hasQueuedPredecessors方法检查同步队列中是否有前驱节点 ,只有在没有前驱节点时才尝试获取锁 ,以此保证先到的线程先获取锁 。而非公平锁在获取锁时,直接尝试通过CAS操作获取锁,不会检查同步队列 ,如果获取失败,才会和公平锁一样,将线程加入同步队列等待 。例如,非公平锁的tryAcquire方法实现如下:

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) 
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

可以看到,非公平锁在state为 0 时,直接通过CAS尝试获取锁,没有检查同步队列 。这种实现方式使得非公平锁在高并发场景下,由于减少了检查同步队列的开销,性能通常比公平锁更高 。但同时,由于可能导致一些线程长时间等待,出现线程饥饿的问题 。

2. 共享模式​

共享模式下,允许多个线程同时获取同步状态,访问共享资源 。以Semaphore(信号量)和CountDownLatch(倒计时器)为例,讲解共享模式下线程获取和释放共享资源的过程 。​

获取共享资源:当一个线程调用Semaphore的acquire方法获取许可(共享资源)时,实际上调用的是AQS的acquireShared方法 ,这个方法会尝试获取共享资源 。在Semaphore中,tryAcquireShared方法是获取共享资源的核心逻辑 :

protected int tryAcquireShared(int acquires) {
    for (;;) {
        int available = getState();
        int remaining = available - acquires;
        if (remaining < 0 || compareAndSetState(available, remaining))
            return remaining;
    }
}
  1. 首先进入一个无限循环,获取当前的状态state(在Semaphore中,state表示剩余的许可数) 。​
  2. 计算获取acquires个许可后剩余的许可数remaining 。​
  3. 如果remaining小于 0,说明当前剩余许可数不足,返回remaining,表示获取失败 。​
  4. 否则,通过CAS操作尝试将state从available更新为remaining 。如果更新成功,返回remaining,表示获取成功 ;如果更新失败,说明有其他线程同时在修改state,继续循环尝试 。

当获取共享资源失败时,线程会通过doAcquireShared方法被封装成一个节点(Node)加入到同步队列的尾部,然后进入一个循环,不断尝试获取共享资源 : 

private void doAcquireShared(int arg) {
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null; 
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
  1. 首先通过addWaiter方法将当前线程封装成一个共享模式的节点加入到同步队列的尾部 。​
  2. 然后进入一个无限循环,获取当前节点的前驱节点p 。如果p是头节点,说明当前节点是等待队列中最靠前的节点,有机会获取共享资源,调用tryAcquireShared方法尝试获取共享资源 。​
  3. 如果获取成功(r >= 0),调用setHeadAndPropagate方法将当前节点设置为头节点,并根据情况唤醒后续节点 ,将原头节点的后继指针置空(p.next = null),以便垃圾回收 。如果线程在等待过程中被中断,调用selfInterrupt方法重新设置中断标志 ,返回 。​
  4. 如果获取失败,调用shouldParkAfterFailedAcquire方法判断是否应该阻塞当前线程 。如果应该阻塞,调用parkAndCheckInterrupt方法阻塞当前线程,并返回是否被中断的状态 。如果线程在阻塞过程中被中断,interrupted会被设置为true 。

 释放共享资源:当线程调用Semaphore的release方法释放许可(共享资源)时,实际上调用的是AQS的releaseShared方法 :

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

首先调用tryReleaseShared方法尝试释放共享资源,在Semaphore中,tryReleaseShared方法的实现如下:

protected boolean tryReleaseShared(int releases) {
    for (;;) {
        int current = getState();
        int next = current + releases;
        if (next < current) 
            throw new Error("Maximum permit count exceeded");
        if (compareAndSetState(current, next))
            return true;
    }
}

该方法首先进入一个无限循环,获取当前的状态state 。计算释放releases个许可后的状态next 。如果next小于current,说明释放的许可数超过了最大许可数,抛出Error异常 。否则,通过CAS操作尝试将state从current更新为next 。如果更新成功,返回true,表示释放成功 ;如果更新失败,说明有其他线程同时在修改state,继续循环尝试 。​

如果tryReleaseShared返回true,说明释放共享资源成功,调用doReleaseShared方法唤醒同步队列中等待的线程 。doReleaseShared方法会从同步队列的头节点开始,找到第一个等待状态小于 0 的节点(即有效的等待节点),调用LockSupport.unpark方法唤醒该节点对应的线程 。如果唤醒后发现该节点的后继节点也处于等待状态,继续唤醒后继节点,直到没有可唤醒的节点为止 。

共享模式与独占模式的差异:​

  1. 获取资源的方式:独占模式下,同一时刻只有一个线程能获取到同步状态,其他线程只能等待 ;共享模式下,允许多个线程同时获取同步状态,只要剩余的共享资源满足线程的需求 。​
  2. 同步队列的处理:在独占模式中,获取锁失败的线程会被加入同步队列,当持有锁的线程释放锁时,只会唤醒同步队列中头节点的后继节点对应的线程 ;在共享模式中,获取共享资源失败的线程同样会被加入同步队列,当有线程释放共享资源时,会唤醒同步队列中多个等待的线程(只要剩余的共享资源足够) 。​
  3. 应用场景:独占模式适用于需要严格控制资源访问,保证同一时刻只有一个线程访问资源的场景,如文件写入操作 ;共享模式适用于读多写少的场景,或者需要多个线程同时访问共享资源的场景,如数据库连接池、信号量控制并发访问数量等 。

五、AQS 在实际场景中的应用​

AQS 作为 Java 并发包的核心框架,在众多实际场景中发挥着重要作用 。许多基于 AQS 实现的并发工具类,为我们解决多线程并发问题提供了强大的支持 。下面我们来介绍一些常见的基于 AQS 实现的并发工具类及其使用场景 。​

1. ReentrantLock​

ReentrantLock是一种可重入的互斥锁 ,它允许同一个线程多次获取同一把锁 。在需要保证同一时刻只有一个线程能够访问共享资源的场景中,ReentrantLock是一个很好的选择 。​

例如,在一个多线程的银行转账系统中,为了保证账户余额的一致性,在进行转账操作时需要对账户进行加锁 。使用ReentrantLock可以实现如下:

public class BankAccount {
    private double balance;
    private final ReentrantLock lock = new ReentrantLock();

    public BankAccount(double initialBalance) {
        this.balance = initialBalance;
    }

    public void transfer(BankAccount target, double amount) {
        lock.lock();
        try {
            if (this.balance >= amount) {
                this.balance -= amount;
                target.balance += amount;
                System.out.println(Thread.currentThread().getName() + " 成功转账 " + amount + " 到目标账户");
            } else {
                System.out.println(Thread.currentThread().getName() + " 转账失败,余额不足");
            }
        } finally {
            lock.unlock();
        }
    }
}

在这个例子中,transfer方法使用ReentrantLock来保证在同一时刻只有一个线程能够执行转账操作,避免了多线程同时操作账户余额导致的数据不一致问题 。​

2. Semaphore​

Semaphore是一个计数信号量,它可以用来控制同时访问特定资源的线程数量 。在资源有限的情况下,Semaphore非常有用 。​

比如,在一个数据库连接池的实现中,我们需要限制同时获取数据库连接的线程数量,以避免数据库连接被耗尽 。使用Semaphore可以实现如下:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.concurrent.Semaphore;

public class DatabaseConnectionPool {
    private static final String URL = "jdbc:mysql://localhost:3306/mydb";
    private static final String USER = "root";
    private static final String PASSWORD = "password";
    private static final int MAX_CONNECTIONS = 5;

    private final Semaphore semaphore;

    public DatabaseConnectionPool() {
        this.semaphore = new Semaphore(MAX_CONNECTIONS);
    }

    public Connection getConnection() throws InterruptedException, SQLException {
        semaphore.acquire();
        try {
            return DriverManager.getConnection(URL, USER, PASSWORD);
        } catch (SQLException e) {
            semaphore.release();
            throw e;
        }
    }

    public void releaseConnection(Connection connection) {
        if (connection != null) {
            try {
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            } finally {
                semaphore.release();
            }
        }
    }
}

在这个例子中,DatabaseConnectionPool使用Semaphore来控制同时获取数据库连接的线程数量,当有线程获取连接时,调用acquire方法获取许可,当线程释放连接时,调用release方法释放许可 ,这样可以确保不会有过多的线程同时获取数据库连接,避免了数据库连接资源的耗尽 。​

3. CountDownLatch​

CountDownLatch是一个同步辅助类,它允许一个或多个线程等待,直到其他线程完成一组操作 。在需要等待多个线程完成任务后再继续执行的场景中,CountDownLatch非常适用 。​

比如,在一个分布式系统中,有多个子任务需要并发执行,当所有子任务完成后,再进行汇总操作 。使用CountDownLatch可以实现如下:

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class DistributedTask {
    private static final int TASK_COUNT = 5;

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(TASK_COUNT);
        CountDownLatch countDownLatch = new CountDownLatch(TASK_COUNT);

        for (int i = 0; i < TASK_COUNT; i++) {
            final int taskIndex = i;
            executorService.submit(() -> {
                try {
                    System.out.println("任务 " + taskIndex + " 开始执行");
                    // 模拟任务执行
                    Thread.sleep((long) (Math.random() * 1000));
                    System.out.println("任务 " + taskIndex + " 执行完成");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    countDownLatch.countDown();
                }
            });
        }

        countDownLatch.await();
        System.out.println("所有任务执行完成,进行汇总操作");
        executorService.shutdown();
    }
}

在这个例子中,主线程创建了一个CountDownLatch对象,并将计数器初始化为任务数量 。每个子任务执行完毕后,调用countDown方法将计数器减 1 。主线程调用await方法等待,直到计数器变为 0,即所有子任务都执行完毕后,再继续执行后续的汇总操作 。

七、面试题解析

1. 经典面试题​

  • AQS 是什么?​
  • AQS 的核心原理是什么?​
  • AQS 中的同步队列和条件队列有什么区别?​
  • ReentrantLock 的公平锁和非公平锁有什么区别?​
  • Semaphore 是如何基于 AQS 实现的?​
  • CountDownLatch 的实现原理与 AQS 有什么关系?​
  • AQS 中独占模式和共享模式的区别是什么?​
  • AQS 中的状态变量 state 有什么作用?​
  • 如何自定义一个基于 AQS 的同步器?​

2. 解题思路和答案​

AQS 是什么?​

  • 解题思路:从 AQS 的定义、作用和在 Java 并发包中的地位进行阐述。​
  • 答案:AQS 即抽象队列同步器(AbstractQueuedSynchronizer) ,是 Java 并发包中构建锁和同步器的基础框架 。它提供了一种基于 FIFO 队列的机制来管理线程的竞争和等待状态 ,许多重要的同步工具类,如ReentrantLock、CountDownLatch、Semaphore等,都是基于 AQS 实现的 。AQS 就像是 Java 并发编程大厦的基石,为我们实现各种复杂的同步逻辑提供了强大的支持 。​

3. AQS 的核心原理是什么?​

  • 解题思路:围绕 AQS 的核心构成(状态变量state和同步队列),以及在独占模式和共享模式下线程获取和释放资源的过程进行分析。​
  • 答案:AQS 的核心原理基于一个 FIFO 等待队列和一个同步状态(state) 。state是一个volatile修饰的整型变量,用于表示同步状态 ,不同的同步器中state有着不同的含义 ,如在ReentrantLock中表示锁的重入次数,在Semaphore中表示剩余的许可数 。同步队列是一个双向链表,当线程无法获取同步状态时,会被封装成一个节点加入到同步队列中等待 。在独占模式下,同一时刻只有一个线程能获取到同步状态,其他线程获取失败则进入同步队列等待;在共享模式下,允许多个线程同时获取同步状态,只要剩余的共享资源满足线程的需求 。线程通过CAS操作来竞争获取或释放资源,获取失败则进入同步队列等待唤醒 。​

4. AQS 中的同步队列和条件队列有什么区别?​

解题思路:分别介绍同步队列和条件队列的结构、作用以及节点状态的含义,对比它们在功能和使用场景上的差异。​

答案:同步队列是 AQS 实现线程同步的关键数据结构,是一个双向链表 ,用于存储等待获取同步状态的线程 。当线程获取同步状态失败时,会被封装成一个节点加入到同步队列的尾部 。节点的状态有CANCELLED(线程已取消等待)、SIGNAL(后继节点的线程需要被唤醒)等 。在独占模式下,持有锁的线程释放锁时,会唤醒同步队列中头节点的后继节点对应的线程 ;在共享模式下,释放共享资源时,会唤醒多个等待的线程 。​

条件队列与Condition接口相关联 ,每个Condition对象都有一个单独的条件队列 。线程调用Condition的await方法时,会从同步队列转移到条件队列并释放锁进入等待状态 ,此时节点的状态会被设置为CONDITION 。当其他线程调用Condition的signal方法时,条件队列中第一个节点被转移回同步队列,等待获取锁 。​

总的来说,同步队列主要用于线程获取同步状态失败时的等待和唤醒,而条件队列用于线程在满足特定条件时的等待和唤醒 ,两者相互配合,共同实现了 AQS 强大的同步功能 。​

5. ReentrantLock 的公平锁和非公平锁有什么区别?​

  • 解题思路:从获取锁的过程、是否保证线程获取锁的顺序以及性能等方面进行对比分析。​
  • 答案:公平锁保证线程按照请求锁的顺序获取锁,即先到先得 。在获取锁时,公平锁会先调用hasQueuedPredecessors方法检查同步队列中是否有前驱节点 ,只有在没有前驱节点时才尝试获取锁 ,以此保证先到的线程先获取锁 。​

非公平锁则不保证获取顺序,线程可以在锁可用时直接竞争获取,可能会出现后到的线程先获取到锁的情况 。非公平锁在获取锁时,直接尝试通过CAS操作获取锁,不会检查同步队列 ,如果获取失败,才会和公平锁一样,将线程加入同步队列等待 。​

在性能方面,非公平锁由于减少了检查同步队列的开销,在高并发场景下性能通常比公平锁更高 。但同时,由于可能导致一些线程长时间等待,出现线程饥饿的问题 。而公平锁虽然保证了公平性,但由于频繁的线程上下文切换,性能相对较低 。​

6. Semaphore 是如何基于 AQS 实现的?​

  • 解题思路:分析Semaphore中与 AQS 相关的核心方法,如tryAcquireShared和tryReleaseShared,阐述它们如何利用 AQS 的同步队列和状态变量来实现信号量的功能。​
  • 答案:Semaphore是基于 AQS 的共享模式实现的 。在Semaphore中,AQS 的状态变量state表示剩余的许可数 。当一个线程调用acquire方法获取许可时,实际上调用的是 AQS 的acquireShared方法 ,tryAcquireShared方法是获取共享资源的核心逻辑 。它会进入一个无限循环,获取当前的state,计算获取acquires个许可后剩余的许可数remaining 。如果remaining小于 0,说明当前剩余许可数不足,返回remaining,表示获取失败 ;否则,通过CAS操作尝试将state从当前值更新为remaining 。如果更新成功,返回remaining,表示获取成功 ;如果更新失败,说明有其他线程同时在修改state,继续循环尝试 。​

当线程调用release方法释放许可时,实际上调用的是 AQS 的releaseShared方法 ,tryReleaseShared方法会进入一个无限循环,获取当前的state ,计算释放releases个许可后的状态next 。如果next小于当前state,说明释放的许可数超过了最大许可数,抛出Error异常 ;否则,通过CAS操作尝试将state从当前值更新为next 。如果更新成功,返回true,表示释放成功 ;如果更新失败,说明有其他线程同时在修改state,继续循环尝试 。如果释放成功,会调用doReleaseShared方法唤醒同步队列中等待的线程 。​

7. CountDownLatch 的实现原理与 AQS 有什么关系?​

  • 解题思路:说明CountDownLatch如何利用 AQS 的同步队列和状态变量来实现线程的等待和计数功能,重点分析await和countDown方法的实现逻辑。​
  • 答案:CountDownLatch也是基于 AQS 实现的 。它利用 AQS 的状态变量state来表示计数的初始值 。当一个或多个线程调用await方法时,实际上调用的是 AQS 的acquireSharedInterruptibly方法 ,线程会尝试获取共享资源 。由于CountDownLatch的设计,只有当state减为 0 时,线程才能获取到共享资源,否则线程会被加入到同步队列中等待 。​

当其他线程调用countDown方法时,实际上调用的是 AQS 的releaseShared方法 ,会将state减 1 。如果state减为 0,说明所有的计数都已完成,会唤醒同步队列中等待的线程 ,让它们继续执行 。​

8. AQS 中独占模式和共享模式的区别是什么?​

  • 解题思路:从获取资源的方式、同步队列的处理以及应用场景等方面进行详细对比。​
  • 答案:在获取资源的方式上,独占模式下,同一时刻只有一个线程能获取到同步状态,其他线程只能等待 ;共享模式下,允许多个线程同时获取同步状态,只要剩余的共享资源满足线程的需求 。​

在同步队列的处理上,独占模式中,获取锁失败的线程会被加入同步队列,当持有锁的线程释放锁时,只会唤醒同步队列中头节点的后继节点对应的线程 ;共享模式中,获取共享资源失败的线程同样会被加入同步队列,当有线程释放共享资源时,会唤醒同步队列中多个等待的线程(只要剩余的共享资源足够) 。​

在应用场景方面,独占模式适用于需要严格控制资源访问,保证同一时刻只有一个线程访问资源的场景,如文件写入操作 ;共享模式适用于读多写少的场景,或者需要多个线程同时访问共享资源的场景,如数据库连接池、信号量控制并发访问数量等 。​

9. AQS 中的状态变量 state 有什么作用?​

  • 解题思路:结合不同的同步器,阐述state在表示同步状态、控制资源访问以及实现锁的重入等方面的作用。​
  • 答案:AQS 中的状态变量state是实现同步的关键 ,它用一个volatile修饰的整型变量来表示同步状态 。在不同的同步器中,state有着不同的含义和作用 。在ReentrantLock中,state表示锁的重入次数 。当一个线程首次获取到锁时,state会被设置为 1 。如果该线程再次获取同一把锁,state就会递增,变为 2,以此类推,表示线程对锁的重入 。而当线程释放锁时,state会相应递减,直到state为 0 时,锁被完全释放 。在Semaphore中,state表示剩余的许可数 。假设我们创建一个信号量对象并设置许可数为 5,那么初始时state就是 5 。当一个线程调用acquire方法获取许可时,如果state大于 0,该线程就能获取到许可,同时state减 1 ;当state为 0 时,其他线程再调用acquire方法就会被阻塞,直到有其他线程调用release方法释放许可,state增加后,才有可能获取到许可 。​

9. 如何自定义一个基于 AQS 的同步器?​

  • 解题思路:介绍自定义同步器的步骤,包括继承 AQS 类、重写必要的抽象方法(如tryAcquire、tryRelease等),以及如何使用自定义同步器。​
  • 答案:自定义一个基于 AQS 的同步器,首先需要继承AbstractQueuedSynchronizer类 。然后,根据同步器的类型(独占模式或共享模式),重写相应的抽象方法 。​

对于独占模式的同步器,通常需要重写tryAcquire和tryRelease方法 。tryAcquire方法用于尝试获取同步状态,实现该方法须查询并判断当前状态是否符合预期,然后再进行CAS设置状态 。例如,实现一个简单的独占锁:

import java.util.concurrent.locks.AbstractQueuedSynchronizer;

public class CustomLock {
    private static class Sync extends AbstractQueuedSynchronizer {
        @Override
        protected boolean tryAcquire(int arg) {
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        @Override
        protected boolean tryRelease(int arg) {
            if (getState() == 0)
                throw new IllegalMonitorStateException();
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        protected boolean isHeldExclusively() {
            return getState() == 1;
        }
    }

    private final Sync sync = new Sync();

    public void lock() {
        sync.acquire(1);
    }

    public void unlock() {
        sync.release(1);
    }

    public boolean isLocked() {
        return sync.isHeldExclusively();
    }
}

在这个例子中,tryAcquire方法通过CAS操作尝试将state从 0 设置为 1 ,如果设置成功,将当前线程设置为锁的持有者,返回true,表示获取锁成功 ;tryRelease方法将state设置为 0,并将锁的持有者设置为null,返回true,表示释放锁成功 。​

对于共享模式的同步器,需要重写tryAcquireShared和tryReleaseShared方法 。使用自定义同步器时,创建同步器的实例,然后调用其提供的方法来实现线程同步 。 

八、总结

AQS 作为 Java 并发编程的核心框架,为我们解决多线程同步问题提供了强大的支持。通过本文的介绍,我们从实际问题出发,深入探讨了 AQS 的概念、核心构成、工作机制、实际应用以及面试相关内容。​

AQS 的核心构成包括状态变量state和同步队列 ,它们相互配合,实现了线程的同步控制 。在独占模式和共享模式下,AQS 通过不同的逻辑来管理线程对资源的获取和释放 ,确保了多线程环境下资源访问的安全性和高效性 。​

基于 AQS 实现的并发工具类,如ReentrantLock、Semaphore、CountDownLatch等,在实际开发中有着广泛的应用 ,为我们处理各种并发场景提供了便捷的解决方案 。​

在面试中,AQS 相关的问题也是高频考点 ,掌握 AQS 的原理和应用,能够帮助我们更好地应对面试,展示自己在并发编程方面的能力 。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值