程序员老鬼

面试官:说说你对AQS的理解。。。

今天跟大家聊聊AQS,也就是AbstractQueuedSynchronizer,这个名字可能有点吓人,但其实就是Java并发包里的一个“同步神器”。如果你做Java开发,总会听到各种“锁、同步器、并发工具”啥的,AQS就是这些工具的幕后功臣。

它的目标很简单,就是提供一种机制来管理多个线程对共享资源的访问,保证同一时刻只有一个线程能操作这些资源,而其他线程要排队等候——可以理解成是线程界的“排队窗口”。

AQS的核心思想其实很简单,如果有资源可用,那就让当前线程上去操作;如果没资源了,那就排队等着,直到资源释放。

这里的“排队”可不是大家随便站一排,而是有秩序的,因为AQS采用的是CLH(Craig, Landin, Hagersten)队列的一个变体,即一个双向链表结构,来保证线程是FIFO(先进先出)地排队。这种队列设计保证了先到的线程优先操作资源,后到的就乖乖排队去。

那到底怎么实现这个排队机制的呢?AQS的原理图可能会让人眼花缭乱,但其实它的底层设计并不复杂。首先AQS内部有一个volatile修饰的int类型变量state,这个变量代表了同步状态。

AQS通过这个状态变量和CAS(Compare and Swap)操作来控制线程对资源的访问状态,比如一个“状态变量”可以理解成“资源的信号”,表示当前资源有没有被占用。

AQS的工作原理就是当线程请求资源时,它会先检查state值,如果资源空闲,那就通过CAS操作将state改成已占用的状态;如果资源被占用了,那这个线程就得被挂起并加入到CLH队列中排队。

再来说说这个CLH队列,AQS将每一个请求资源的线程封装成一个节点,这样排队的线程就是一个一个节点地链接成队列(实际上是一个虚拟的双向队列)。

每个节点表示一个线程对资源的请求状态,同时保证了线程会按顺序拿到资源,而不会出现“插队”的情况。这种设计对保证线程安全至关重要,因为Java多线程环境里,线程之间的资源抢占随时会发生,而AQS这个“排队员”就是来负责公平分配资源的。

除了管理排队和分配资源,AQS还需要负责线程的阻塞和唤醒。对于那些没有拿到资源的线程,AQS会将其阻塞,直到资源释放;而当资源释放时,AQS会唤醒队列中下一个等待的线程,继续这个“接力跑”。所以AQS不仅仅是一个锁,而是一个可以用来实现各种并发控制工具的“同步框架”。

举个例子,大家熟悉的Java锁ReentrantLock和倒计数器CountDownLatch都是基于AQS实现的。ReentrantLock是一种互斥锁,它使用state变量来记录当前锁的持有情况,当state为0时,表示锁是空闲状态;如果有线程获得锁,就将state加1,释放锁则减1;支持锁的重入时,state会随着重入次数递增。

而CountDownLatch则是一个用来控制线程等待的工具,state在这里表示倒计数的数值,当倒计数为0时,所有等待的线程都会被唤醒,继续往下执行。

AQS的状态变量state其实就是整个同步机制的“灵魂”。不同的并发工具会以不同的方式去使用这个变量,比如在Semaphore中,state代表许可证的数量;在ReentrantLock中,state代表锁的占有情况;在CountDownLatch里,state表示倒数的计数值。

AQS通过state变量的变化来实现对资源的控制,具体实现中会用到unsafe类来保证state的操作是线程安全的。

AQS还有两个关键的方法:acquire和release,这两个方法就是负责管理资源的获取和释放。acquire方法会检查state状态,如果资源空闲,那就将state值改为已占用状态;如果资源被占用,那当前线程就会被加入到CLH队列中等待。

当有资源释放时,release方法会将state值改回空闲状态,然后唤醒等待的线程。为了实现这些功能,AQS为每个实现类提供了tryAcquire和tryRelease方法,让这些类自己去定义具体的获取和释放行为。

举个小例子,用ReentrantLock来看AQS的工作方式:

import java.util.concurrent.locks.ReentrantLock;

public class AQSDemo {
    private static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        Runnable task = () -> {
            try {
                lock.lock();
                System.out.println(Thread.currentThread().getName() + " acquired the lock.");
                Thread.sleep(1000); // 模拟任务执行
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println(Thread.currentThread().getName() + " releasing the lock.");
                lock.unlock();
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        
        t1.start();
        t2.start();
    }
}

在上面的代码中,两个线程t1和t2都尝试获取锁,由于ReentrantLock基于AQS实现,所以当t1获取锁后,t2会进入阻塞状态并被加入到等待队列中,直到t1释放锁。这就是AQS通过状态和队列实现的“排队”原理。

好了,现在聊完了AQS的原理,接下来说说如果你在面试中被问到AQS应该怎么回答。

最优的回答应该从AQS的核心功能出发,简洁地说明它是如何管理同步状态(state变量)和排队机制(CLH队列),可以适当提到它在ReentrantLock、Semaphore等工具中的应用。此外,还可以补充一下AQS的三个主要职责:1)同步状态的原子性管理;2)线程的阻塞和解除;3)排队管理。

面试官如果继续深入,比如问AQS的优点,可以提到它的设计模式比较通用,适用于各种并发场景。同时,它的CAS操作保证了高效的原子性操作,而CLH队列保证了线程安全的排队,避免了传统锁的“自旋”浪费,适用于高并发场景。

-END - ok,今天先说到这,老规矩,看完文章记得右下角给何老师点赞。 最后送给大家一个福利,我这里有一份搞副业的教程,这份教程里有 100+个搞钱小项目 : 网盘拉新核心玩法、公众号运营变现、 小红书虚拟资料引流 等,现在扫码加我微信,即可领取这份副业教程。 添加时备注: 副业