深入理解Java并发核心:AQS

深入解析Java AQS并发核心

引言

在Java并发编程领域,java.util.concurrent包(JUC)提供了丰富且强大的并发工具。而这些工具的基石,便是AbstractQueuedSynchronizer(AQS),抽象队列同步器。AQS不仅是ReentrantLockSemaphoreCountDownLatch等并发组件的底层实现,更是理解Java并发机制的关键。本文将探讨AQS的设计思想、核心原理、内部数据结构以及其在独占模式和共享模式下的应用。

1. AQS概述

AQS,全称AbstractQueuedSynchronizer,意为抽象队列同步器。它是一个用于构建锁和同步器的框架,提供了一种实现依赖于先进先出(FIFO)等待队列的阻塞锁和相关同步器的机制。AQS的核心思想是,如果被请求的共享资源空闲,那么就将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态;如果共享资源被占用,就需要一套线程阻塞等待以及被唤醒时锁分配的机制,AQS就是解决了这个问题。

1.1 AQS的特性

  • 阻塞和唤醒机制:当线程尝试获取资源失败时,AQS会将其封装成一个Node节点加入到同步队列中,并进行阻塞。当资源被释放时,AQS会唤醒同步队列中的下一个等待线程。
  • 状态管理:AQS内部使用一个volatile修饰的int类型的成员变量state来表示同步状态。这个state变量是AQS实现同步机制的核心,通过对它的修改来表示资源的占用和释放。例如,在ReentrantLock中,state表示锁的重入次数;在Semaphore中,state表示可用的许可数量。
  • FIFO同步队列:AQS维护了一个FIFO的双向链表作为同步队列,用于管理等待获取资源的线程。当线程获取资源失败时,会被包装成一个Node节点并加入到这个队列的尾部。当资源被释放时,队列头部的线程会被唤醒并尝试获取资源。
  • 模板方法模式:AQS本身是一个抽象类,它定义了同步器实现的基本骨架,但具体的资源获取和释放逻辑则由子类通过重写其模板方法来实现。这使得AQS具有很高的可扩展性,开发者可以基于AQS轻松实现各种自定义同步器。

1.2 AQS的地位

AQS是JUC包中许多核心并发组件的基石,例如:

  • ReentrantLock:可重入的独占锁,支持公平锁和非公平锁。
  • Semaphore:信号量,用于控制同时访问特定资源的线程数量。
  • CountDownLatch:倒计时门闩,允许一个或多个线程等待,直到在其他线程中执行的一组操作完成。
  • ReentrantReadWriteLock:可重入的读写锁,允许多个读线程同时访问,但只允许一个写线程访问。
  • ThreadPoolExecutor中的Worker线程的启动和关闭也间接依赖于AQS。

理解AQS的原理,对于深入理解这些并发工具的内部工作机制至关重要。

2. AQS核心原理

AQS的核心原理围绕着三个关键要素展开:同步状态(state)、FIFO同步队列(CLH队列)以及对同步状态的原子性操作(CAS)。

2.1 同步状态(state

AQS使用一个volatile int state成员变量来表示同步状态。这个变量是AQS实现所有同步逻辑的基础。不同的同步器对state的含义有不同的解释:

  • 独占锁(如ReentrantLockstate为0表示锁未被占用,为1表示锁已被占用。对于可重入锁,state的值可以大于1,表示当前线程获取锁的次数。
  • 共享锁(如Semaphorestate表示当前可用的资源数量。当state为0时,表示资源已耗尽。

state变量被声明为volatile,确保了其在多线程环境下的可见性。AQS提供了三个protected方法来访问和修改state

  • getState():获取当前同步状态的值。
  • setState(int newState):设置当前同步状态的值。
  • compareAndSetState(int expect, int update):使用CAS(Compare-And-Swap)操作原子性地更新同步状态。这是AQS实现无锁并发的关键。

2.2 FIFO同步队列(CLH队列)

AQS内部维护了一个双向链表,即FIFO(First-In, First-Out)同步队列,也常被称为CLH(Craig, Landin, and Hagersten)队列的变体。这个队列用于管理所有等待获取资源的线程。当一个线程尝试获取资源失败时,它会被封装成一个Node节点,并加入到同步队列的尾部。每个Node节点除了包含线程本身,还包含线程的等待状态(waitStatus)以及指向前驱和后继节点的引用。

Node的结构

Node是AQS的静态内部类,它定义了等待队列中节点的结构,主要包含以下字段:

  • thread:当前节点关联的线程。
  • prev:指向前驱节点。
  • next:指向后继节点。
  • waitStatus:表示当前节点的等待状态,有以下几种:
    • CANCELLED (1):表示当前节点已取消。当线程等待超时或被中断时,会进入此状态。
    • SIGNAL (-1):表示后继节点需要被唤醒。当前节点释放锁或取消时,会唤醒后继节点。
    • CONDITION (-2):表示节点在条件队列中等待。当调用Condition.await()方法时,线程会进入此状态。
    • PROPAGATE (-3):表示共享模式下,当前节点不仅会唤醒后继节点,还会传播唤醒操作。主要用于releaseShared方法。
    • 0:初始状态。

同步队列的工作流程

  1. 入队:当线程尝试获取资源失败时,AQS会创建一个Node节点,将当前线程封装进去,并通过CAS操作将其添加到同步队列的尾部。如果添加失败(例如,在并发环境下其他线程同时尝试入队),会进行自旋重试。
  2. 阻塞:线程入队后,会检查其前驱节点的waitStatus。如果前驱节点的状态是SIGNAL,则当前线程会安全地挂起(park),等待被唤醒。如果不是SIGNAL,则会尝试将前驱节点的状态设置为SIGNAL,并再次检查。
  3. 出队与唤醒:当持有资源的线程释放资源时,它会唤醒同步队列中的头节点(通常是队列中等待时间最长的线程)。被唤醒的线程会再次尝试获取资源。如果获取成功,它将成为新的头节点,并从队列中移除。

2.3 CAS(Compare-And-Swap)操作

CAS是一种乐观锁机制,它包含三个操作数:内存位置(V)、预期原值(A)和新值(B)。如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置的值更新为新值B。否则,处理器不做任何操作。AQS大量使用CAS操作来保证对state变量和同步队列的原子性修改,从而避免了传统锁带来的开销。

例如,在获取独占锁时,AQS会尝试使用CAS将state从0设置为1。如果成功,则表示获取锁成功;如果失败,则说明有其他线程已经获取了锁,当前线程需要进入等待队列。

// 示例:AQS中尝试获取独占锁的核心逻辑(简化版)
protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
        selfInterrupt();
    }
}

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // 尝试快速入队
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 慢速入队(自旋)
    enq(node);
    return node;
}

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

// 示例:AQS中尝试释放独占锁的核心逻辑(简化版)
protected boolean tryRelease(int arg) {
    throw new UnsupportedOperationException();
}

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

上述代码片段展示了AQS中acquirerelease方法的核心逻辑,其中tryAcquiretryRelease是需要子类实现的模板方法,它们定义了具体的资源获取和释放策略。addWaiterenq方法负责将线程添加到同步队列,unparkSuccessor负责唤醒后继线程。

3. AQS独占模式

独占模式(Exclusive Mode)是指在同一时刻,只有一个线程能够成功获取同步状态。其他尝试获取同步状态的线程都将被阻塞,并进入同步队列等待。典型的独占模式同步器有ReentrantLockReentrantReadWriteLock的写锁。

3.1 独占模式的获取(acquire

当线程调用acquire(int arg)方法尝试获取独占锁时,其内部逻辑大致如下:

  1. 尝试获取同步状态:首先调用子类实现的tryAcquire(int arg)方法。这个方法是AQS的模板方法,由具体的同步器实现,用于尝试以独占方式获取同步状态。例如,在ReentrantLock中,tryAcquire会尝试通过CAS操作将state从0设置为1,或者在重入时增加state的值。
  2. 获取成功:如果tryAcquire返回true,表示当前线程成功获取了同步状态,acquire方法直接返回。
  3. 获取失败:如果tryAcquire返回false,表示同步状态已被其他线程占用。此时,当前线程会被封装成一个Node节点,并加入到同步队列的尾部(通过addWaiter方法)。
  4. 阻塞等待:线程进入同步队列后,会通过acquireQueued方法进行自旋,并检查其前驱节点的waitStatus。如果前驱节点是SIGNAL状态,则当前线程会安全地挂起(park),等待被唤醒。这种机制避免了不必要的CPU消耗,只有当前驱节点释放锁时,才会被唤醒。
  5. 中断响应:在阻塞等待过程中,如果线程被中断,acquire方法会根据实现(acquireacquireInterruptibly)选择是否响应中断。如果响应中断,则会抛出InterruptedException

3.2 独占模式的释放(release

当线程调用release(int arg)方法释放独占锁时,其内部逻辑大致如下:

  1. 尝试释放同步状态:首先调用子类实现的tryRelease(int arg)方法。这个方法由具体的同步器实现,用于尝试以独占方式释放同步状态。例如,在ReentrantLock中,tryRelease会尝试减少state的值,当state减到0时,表示锁完全释放。
  2. 释放成功:如果tryRelease返回true,表示当前线程成功释放了同步状态。此时,AQS会检查同步队列的头节点。如果头节点存在且其waitStatus不是0(通常是SIGNAL),则会唤醒头节点中的线程(通过unparkSuccessor方法)。
  3. 释放失败:如果tryRelease返回false,表示同步状态尚未完全释放(例如,在可重入锁中,state尚未减到0),release方法直接返回。

3.3 独占模式示例:ReentrantLock

ReentrantLock是Java中最常用的独占锁之一,其内部实现正是基于AQS。它支持可重入性,即同一个线程可以多次获取同一把锁,并且需要释放相同次数的锁才能真正释放。

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {
    private static ReentrantLock lock = new ReentrantLock();
    private static int count = 0;

    public static void increment() {
        lock.lock(); // 获取锁
        try {
            for (int i = 0; i < 10000; i++) {
                count++;
            }
        } finally {
            lock.unlock(); // 释放锁
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> increment());
        Thread t2 = new Thread(() -> increment());

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("Final count: " + count);
    }
}

在上述示例中,lock.lock()方法会调用AQS的acquire方法,而lock.unlock()方法会调用AQS的release方法。ReentrantLock的内部类Sync(公平或非公平)实现了tryAcquiretryRelease方法,从而实现了独占锁的逻辑。

4. AQS共享模式

共享模式(Shared Mode)是指在同一时刻,多个线程可以同时获取同步状态。当一个线程获取同步状态后,其他线程仍然可以尝试获取,只要同步状态允许。典型的共享模式同步器有SemaphoreCountDownLatch,以及ReentrantReadWriteLock的读锁。

4.1 共享模式的获取(acquireShared

当线程调用acquireShared(int arg)方法尝试获取共享锁时,其内部逻辑大致如下:

  1. 尝试获取同步状态:首先调用子类实现的tryAcquireShared(int arg)方法。这个方法是AQS的模板方法,由具体的同步器实现,用于尝试以共享方式获取同步状态。该方法返回一个int值,表示剩余的同步状态。如果返回负数,表示获取失败;如果返回0,表示获取成功但没有剩余同步状态;如果返回正数,表示获取成功且有剩余同步状态。
  2. 获取成功:如果tryAcquireShared返回非负数,表示当前线程成功获取了同步状态。此时,AQS会进一步判断是否需要传播唤醒操作(setHeadAndPropagate),以唤醒同步队列中的其他等待线程,特别是那些也想以共享模式获取资源的线程。
  3. 获取失败:如果tryAcquireShared返回负数,表示同步状态不足。此时,当前线程会被封装成一个Node节点,并加入到同步队列的尾部。
  4. 阻塞等待:线程进入同步队列后,会通过doAcquireShared方法进行自旋,并检查其前驱节点的waitStatus。如果前驱节点是SIGNAL状态,则当前线程会安全地挂起(park),等待被唤醒。
  5. 中断响应:与独占模式类似,在阻塞等待过程中,如果线程被中断,acquireShared方法会根据实现选择是否响应中断。

4.2 共享模式的释放(releaseShared

当线程调用releaseShared(int arg)方法释放共享锁时,其内部逻辑大致如下:

  1. 尝试释放同步状态:首先调用子类实现的tryReleaseShared(int arg)方法。这个方法由具体的同步器实现,用于尝试以共享方式释放同步状态。该方法返回true表示同步状态完全释放,可以唤醒后续等待线程;返回false表示同步状态尚未完全释放。
  2. 释放成功并传播唤醒:如果tryReleaseShared返回true,表示同步状态已被释放。此时,AQS会检查同步队列的头节点。如果头节点存在且其waitStatus不是0,则会唤醒头节点中的线程(通过doReleaseShared方法)。与独占模式不同的是,共享模式下的释放可能会导致连锁唤醒,即一个线程释放资源后,可能会唤醒多个等待线程,这些线程又可能继续唤醒后续线程,直到没有可用的资源或者队列为空。

4.3 共享模式示例:Semaphore

Semaphore(信号量)是AQS共享模式的一个典型应用。它维护了一个许可集,线程可以通过acquire()方法获取许可,通过release()方法释放许可。如果许可数量不足,线程将被阻塞。

import java.util.concurrent.Semaphore;

public class SemaphoreExample {
    private static Semaphore semaphore = new Semaphore(3); // 允许3个线程同时访问

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    semaphore.acquire(); // 获取许可
                    System.out.println(Thread.currentThread().getName() + " 正在执行");
                    Thread.sleep(2000); // 模拟业务逻辑
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    System.out.println(Thread.currentThread().getName() + " 执行完毕");
                    semaphore.release(); // 释放许可
                }
            }, "Thread-" + i).start();
        }
    }
}

在上述示例中,semaphore.acquire()方法会调用AQS的acquireShared方法,而semaphore.release()方法会调用AQS的releaseShared方法。Semaphore的内部类Sync实现了tryAcquireSharedtryReleaseShared方法,从而实现了信号量的逻辑。

5. 如何基于AQS实现自定义同步器

AQS之所以被称为“抽象队列同步器”,是因为它提供了一个灵活的框架,允许开发者通过继承AQS并实现其抽象方法来构建自己的同步器。这正是AQS的强大之处,它将同步器的通用逻辑(如队列管理、线程阻塞/唤醒)与具体同步逻辑(如资源获取/释放条件)分离。

实现自定义同步器通常需要重写以下一个或多个AQS的protected方法:

5.1 独占模式下需要重写的方法

  • protected boolean tryAcquire(int arg):尝试以独占模式获取同步状态。如果成功,返回true;否则返回false。实现时需要考虑同步状态的获取条件和CAS操作。
  • protected boolean tryRelease(int arg):尝试以独占模式释放同步状态。如果成功,返回true;否则返回false。实现时需要考虑同步状态的释放条件。
  • protected boolean isHeldExclusively():查询当前线程是否独占同步状态。通常用于调试和内部检查。

5.2 共享模式下需要重写的方法

  • protected int tryAcquireShared(int arg):尝试以共享模式获取同步状态。如果成功,返回一个非负数,表示剩余的同步状态;如果失败,返回一个负数。实现时需要考虑同步状态的获取条件和CAS操作。
  • protected boolean tryReleaseShared(int arg):尝试以共享模式释放同步状态。如果成功,返回true,表示可以唤醒后续等待线程;否则返回false。实现时需要考虑同步状态的释放条件。

5.3 条件队列(ConditionObject

AQS还提供了一个内部类ConditionObject,用于实现条件队列。条件队列与同步队列是相互独立的,它允许线程在满足特定条件时才被唤醒。每个ConditionObject实例都与一个AQS同步器关联,并且可以有多个条件队列。这使得开发者可以实现更复杂的同步逻辑,例如生产者-消费者模式中的等待/通知机制。

使用ConditionObject通常涉及以下步骤:

  1. 创建Condition实例:通过AQS的newCondition()方法创建Condition实例。
  2. 等待条件:当线程需要等待某个条件时,调用condition.await()方法。此时,线程会释放持有的锁,并进入条件队列等待。当条件满足时,其他线程会通过condition.signal()condition.signalAll()方法唤醒等待线程。
  3. 通知条件:当某个条件满足时,调用condition.signal()(唤醒一个等待线程)或condition.signalAll()(唤醒所有等待线程)方法。被唤醒的线程会重新尝试获取锁,并在获取成功后从await()方法返回。

5.4 自定义同步器示例:一个简单的独占锁

下面是一个基于AQS实现的一个简单独占锁的示例,它不具备可重入性,仅用于演示AQS的基本用法:

import java.util.concurrent.locks.AbstractQueuedSynchronizer;

public class SimpleLock {

    // 静态内部类,继承AQS
    private static class Sync extends AbstractQueuedSynchronizer {
        // 判断是否处于锁定状态
        @Override
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }

        // 尝试获取锁
        @Override
        protected boolean tryAcquire(int acquires) {
            if (compareAndSetState(0, 1)) { // 尝试将state从0设置为1
                setExclusiveOwnerThread(Thread.currentThread()); // 设置当前线程为独占所有者
                return true;
            }
            return false;
        }

        // 尝试释放锁
        @Override
        protected boolean tryRelease(int releases) {
            if (getState() == 0) {
                throw new IllegalMonitorStateException();
            }
            setExclusiveOwnerThread(null); // 清除独占所有者
            setState(0); // 将state设置为0
            return true;
        }
    }

    private final Sync sync = new Sync();

    // 获取锁
    public void lock() {
        sync.acquire(1);
    }

    // 释放锁
    public void unlock() {
        sync.release(1);
    }

    // 判断是否被锁定
    public boolean isLocked() {
        return sync.isHeldExclusively();
    }

    public static void main(String[] args) throws InterruptedException {
        SimpleLock lock = new SimpleLock();
        Thread t1 = new Thread(() -> {
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + " acquired the lock.");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                lock.unlock();
                System.out.println(Thread.currentThread().getName() + " released the lock.");
            }
        }, "Thread-1");

        Thread t2 = new Thread(() -> {
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + " acquired the lock.");
            } finally {
                lock.unlock();
                System.out.println(Thread.currentThread().getName() + " released the lock.");
            }
        }, "Thread-2");

        t1.start();
        t2.start();

        t1.join();
        t2.join();
    }
}

在这个示例中,SimpleLock通过内部类Sync继承了AQS,并重写了tryAcquiretryRelease方法来定义锁的获取和释放逻辑。tryAcquire通过CAS操作尝试将state从0设置为1,表示获取锁;tryRelease则将state设置为0,表示释放锁。acquirerelease方法则直接调用了AQS提供的模板方法。

6. 总结与展望

AQS作为Java并发包的核心组件,其设计精妙且功能强大。它通过volatile int state、FIFO同步队列和CAS操作,为各种高级同步器提供了统一且高效的实现框架。无论是独占模式下的ReentrantLock,还是共享模式下的Semaphore,都离不开AQS的底层支持。

深入理解AQS,不仅能够帮助我们更好地使用JUC包中的并发工具,还能够为我们设计和实现高性能、高可用的自定义同步器提供坚实的基础。未来,随着Java平台和并发编程技术的发展,AQS及其衍生出的同步器将继续在构建健壮、高效的并发应用中发挥核心作用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值