并发编程原理与实战(三十九)并发基石ArrayBlockingQueue与LinkedBlockingQueue的底层实现与API设计解析

本文继续学习另外两个并发工具类ArrayBlockingQueue和LinkedBlockingQueue。

ArrayBlockingQueue核心特性与原理

ArrayBlockingQueue是Java并发编程中一个重要的有界阻塞队列,基于数组实现,采用单个ReentrantLock控制线程安全,非常适合生产者-消费者场景。具有有界队列、线程安全、先进先出等特性。

有界队列‌

ArrayBlockingQueue的三个构造函数都必须传入队列容量参数,创建对比时必须指定容量,无法动态扩容。

线程安全‌

java 复制代码
/*
 * Concurrency control uses the classic two-condition algorithm
 * found in any textbook.
 */

/** Main lock guarding all access */
final ReentrantLock lock;

/** Condition for waiting takes */
@SuppressWarnings("serial")  // Classes implementing Condition may be serializable.
private final Condition notEmpty;

/** Condition for waiting puts */
@SuppressWarnings("serial")  // Classes implementing Condition may be serializable.
private final Condition notFull;

......

    /**
 * Creates an {@code ArrayBlockingQueue} with the given (fixed)
 * capacity and the specified access policy.
 *
 * @param capacity the capacity of this queue
 * @param fair if {@code true} then queue accesses for threads blocked
 *        on insertion or removal, are processed in FIFO order;
 *        if {@code false} the access order is unspecified.
 * @throws IllegalArgumentException if {@code capacity < 1}
 */
public ArrayBlockingQueue(int capacity, boolean fair) {
    if (capacity <= 0)
        throw new IllegalArgumentException();
    this.items = new Object[capacity];
    lock = new ReentrantLock(fair);
    notEmpty = lock.newCondition();
    notFull =  lock.newCondition();
}

从源码可以看出,ArrayBlockingQueue采用了经典的两条件算法,通过ReentrantLock和Condition实现线程间的阻塞与唤醒,队列满时阻塞生产者,队列空时阻塞消费者。notFull‌条件控制生产者线程的等待和唤醒,notEmpty‌条件控制消费者线程的等待和唤醒。

FIFO原则‌

遵循先进先出的队列顺序。 FIFO 特性是通过一个‌循环数组‌来实现的,其核心在于两个指针:takeIndex 和 putIndex。

java 复制代码
/** The queued items */
@SuppressWarnings("serial") // Conditionally serializable
final Object[] items;

/** items index for next take, poll, peek or remove */
int takeIndex;

/** items index for next put, offer, or add */
int putIndex;

/** Number of elements in the queue */
int count;

item 是存储队列元素的数组,takeIndex是下一个被取出元素的索引,putIndex是下一个被放入元素的索引,count为队列中的元素数量。

ArrayBlockingQueue核心API方法

入队方法

ArrayBlockingQueue提供了多种入队的方法,其主要区别在队列满时行为、返回值。

(1)add(E e)方法

java 复制代码
/**
 * Inserts the specified element at the tail of this queue if it is
 * possible to do so immediately without exceeding the queue's capacity,
 * returning {@code true} upon success and throwing an
 * {@code IllegalStateException} if this queue is full.
 *
 * @param e the element to add
 * @return {@code true} (as specified by {@link Collection#add})
 * @throws IllegalStateException if this queue is full
 * @throws NullPointerException if the specified element is null
 */
public boolean add(E e) {
    return super.add(e);
}

add(E e)方法往队列尾部插入一个元素,如果队列满时将抛出IllegalStateException异常,插入的元素为空则抛出NullPointerException,插入成功返回true,否则返回false,立即返回。其内部最终调用offer(E e)实现。

(2)offer(E e)方法

java 复制代码
/**
 * Inserts the specified element at the tail of this queue if it is
 * possible to do so immediately without exceeding the queue's capacity,
 * returning {@code true} upon success and {@code false} if this queue
 * is full.  This method is generally preferable to method {@link #add},
 * which can fail to insert an element only by throwing an exception.
 *
 * @throws NullPointerException if the specified element is null
 */
public boolean offer(E e) {
    Objects.requireNonNull(e);
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        if (count == items.length)
            return false;
        else {
            enqueue(e);
            return true;
        }
    } finally {
        lock.unlock();
    }
}

offer(E e)采用了ReentrantLock对插入元素的过程进行加锁。插入的元素为空则抛出NullPointerException,插入成功返回true,否则返回false,立即返回。该方法通常优于方法add,因为add方法在无法插入元素时只能通过抛出异常来反馈失败。

(3)offer(E e, long timeout, TimeUnit unit)方法

java 复制代码
/**
 * Inserts the specified element at the tail of this queue, waiting
 * up to the specified wait time for space to become available if
 * the queue is full.
 *
 * @throws InterruptedException {@inheritDoc}
 * @throws NullPointerException {@inheritDoc}
 */
public boolean offer(E e, long timeout, TimeUnit unit)
    throws InterruptedException {

    Objects.requireNonNull(e);
    long nanos = unit.toNanos(timeout);
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == items.length) {
            if (nanos <= 0L)
                return false;
            nanos = notFull.awaitNanos(nanos);
        }
        enqueue(e);
        return true;
    } finally {
        lock.unlock();
    }
}

offer(E e, long timeout, TimeUnit unit)方法则在队列满时等待指定的时间,直到时间消耗完后才返回,插入成功返回true,否则返回false。

(4)put(E e)方法

java 复制代码
/**
 * Inserts the specified element at the tail of this queue, waiting
 * for space to become available if the queue is full.
 *
 * @throws InterruptedException {@inheritDoc}
 * @throws NullPointerException {@inheritDoc}
 */
public void put(E e) throws InterruptedException {
    Objects.requireNonNull(e);
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == items.length)
            notFull.await();
        enqueue(e);
    } finally {
        lock.unlock();
    }
}

put(E e)方法插入时,判断队列是否已满,满了就一直阻塞等待直到能插入元素,该方法没有返回值。从上述源码可以看出,入队操作的这几个方法最终都是调用私有的enqueue(E x)方法。

java 复制代码
private void enqueue(E x) {
    final Object[] items = this.items;
    items[putIndex] = x; // 将元素放入 putIndex 位置
    if (++putIndex == items.length) // putIndex 指针后移,如果到达数组末尾...
        putIndex = 0; // ...则绕回数组开头(循环)
    count++; // 元素数量增加
    notEmpty.signal(); // 唤醒可能正在等待获取元素的消费者线程
}

出队方法

ArrayBlockingQueue同样提供了多个元素出队方法,主要区别在是否立即返回、阻塞等待、超时控制。

java 复制代码
// 有元素立即返回,否则返回null
public E poll() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        return (count == 0) ? null : dequeue();
    } finally {
        lock.unlock();
    }
}
// 有元素立即返回,否则等待指定长的时间
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
    long nanos = unit.toNanos(timeout);
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == 0) {
            if (nanos <= 0L)
                return null;
            nanos = notEmpty.awaitNanos(nanos);
        }
        return dequeue();
    } finally {
        lock.unlock();
    }
}
//阻塞等待直到有元素可以返回
public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == 0)
            notEmpty.await();
        return dequeue();
    } finally {
        lock.unlock();
    }
}

这四个方法最终都是调用私有的核心出队方法dequeue()。

java 复制代码
private E dequeue() {
    final Object[] items = this.items;
    @SuppressWarnings("unchecked")
    E x = (E) items[takeIndex]; // 从 takeIndex 位置取出元素
    items[takeIndex] = null; // 清空该位置,帮助GC
    if (++takeIndex == items.length) // takeIndex 指针后移,如果到达数组末尾...
        takeIndex = 0; // ...则绕回数组开头(循环)
    count--; // 元素数量减少
    if (itrs != null)
        itrs.elementDequeued(); // 更新迭代器状态
    notFull.signal(); // 唤醒可能正在等待放入元素的生产者线程
    return x;
}

ArrayBlockingQueue总结

入队方法这个多个,究竟用哪个?通过上面的分析,不难发现,add()方法在队列满时抛出异常,对于日常的排队需求,很明显不符合要求;offer()方法相对add()方法友好一点,通过返回true或false表示插入结果,但是不会等待;通常很多排队场景是需要等待的,一直等待直到队列能插入元素为止,所以put()方法最符合日常需求,这也是为什么叫阻塞队列原因。

方法 队列满时行为 返回值
add(E e) 抛出IllegalStateException boolean
offer(E e) 立即返回false boolean
put(E e) 阻塞直到队列有空位 void
offer(E e, long timeout, TimeUnit unit) 阻塞指定时间后返回false boolean

同理,出队方法中具有阻塞特性的是take()方法,取元素时等待直到有元素可以取为止。

方法 队列空时行为 返回值
poll() 立即返回null E
take() 阻塞直到队列有元素 E
poll(long timeout, TimeUnit unit) 阻塞指定时间后返回null E

LinkedBlockingQueue核心特性与原理

LinkedBlockingQueue 是 Java 并发包中基于链表的阻塞队列实现,同样采用生产者-消费者模式,是处理多线程数据交换的重要工具。具有链表结构、容量灵活、线程安全与双锁设计、生产者-消费者模型等特性。

底层数据结构

java 复制代码
/**
 * Linked list node class.
 */
static class Node<E> {
    E item;

    /**
     * One of:
     * - the real successor Node
     * - this Node, meaning the successor is head.next
     * - null, meaning there is no successor (this is the last node)
     */
    Node<E> next;

    Node(E x) { item = x; }
}

/** The capacity bound, or Integer.MAX_VALUE if none */
private final int capacity;

/** Current number of elements */
private final AtomicInteger count = new AtomicInteger();

/**
 * Head of linked list.
 * Invariant: head.item == null
 */
transient Node<E> head;

/**
 * Tail of linked list.
 * Invariant: last.next == null
 */
private transient Node<E> last;

Node类是单向链表‌的节点,存储队列元素‌;Node类型的head 和 last节点 分别指向链表首尾‌

原子计数器‌count 记录当前队列元素个数‌。

容量灵活

java 复制代码
/**
 * Creates a {@code LinkedBlockingQueue} with a capacity of
 * {@link Integer#MAX_VALUE}.
 */
public LinkedBlockingQueue() {
    this(Integer.MAX_VALUE);
}

/**
 * Creates a {@code LinkedBlockingQueue} with the given (fixed) capacity.
 *
 * @param capacity the capacity of this queue
 * @throws IllegalArgumentException if {@code capacity} is not greater
 *         than zero
 */
public LinkedBlockingQueue(int capacity) {
    if (capacity <= 0) throw new IllegalArgumentException();
    this.capacity = capacity;
    last = head = new Node<E>(null);
}

创建LinkedBlockingQueue时,可以指定容量,不指定容量时默认为 Integer.MAX_VALUE,可视为无界队列‌。

线程安全与双锁设计

java 复制代码
/** Lock held by take, poll, etc */
private final ReentrantLock takeLock = new ReentrantLock();

/** Wait queue for waiting takes */
@SuppressWarnings("serial") // Classes implementing Condition may be serializable.
private final Condition notEmpty = takeLock.newCondition();

/** Lock held by put, offer, etc */
private final ReentrantLock putLock = new ReentrantLock();

/** Wait queue for waiting puts */
@SuppressWarnings("serial") // Classes implementing Condition may be serializable.
private final Condition notFull = putLock.newCondition();

takeLock是取元素用的锁,putLock是插入元素用的锁。notEmpty:队列为空时,出队线程在此条件变量上等待‌;notFull:队列满时,入队线程在此条件变量上等待‌。

核心API方法

入队方法

和ArrayBlockingQueue一样,LinkedBlockingQueue同样提供了add/offer/put这几个方法,功能同ArrayBlockingQueue。我们截取其中的阻塞方法分析下:

java 复制代码
/**
 * 将指定元素插入此队列的尾部,必要时等待空间可用。
 * 这是阻塞队列的核心方法之一,当队列满时会阻塞当前线程。
 *
 * @throws InterruptedException 如果在线程等待时被中断
 * @throws NullPointerException 如果指定的元素为 null
 */
public void put(E e) throws InterruptedException {
    // 参数校验:不允许插入 null 元素
    if (e == null) throw new NullPointerException();
    
    // 定义局部变量 c,用于记录插入前的队列元素数量
    final int c;
    
    // 创建新的节点包装要插入的元素
    final Node<E> node = new Node<E>(e);
    
    // 获取入队锁和原子计数器的本地引用,避免后续访问 this 指针
    final ReentrantLock putLock = this.putLock;
    final AtomicInteger count = this.count;
    
    // 获取入队锁,支持中断响应
    putLock.lockInterruptibly();
    try {
        /*
         * 注意:这里使用 count 作为等待条件判断,即使它没有被当前锁保护。
         * 这样是可行的,因为在持有 putLock 期间,count 只能减少(其他入队操作被锁阻塞),
         * 当 count 从容量值改变时,我们(或其他等待的入队线程)会被通知唤醒。
         * 同样适用于其他等待条件中使用 count 的情况。
         */
        
        // 循环检查队列是否已满(使用循环是为了防止虚假唤醒)
        while (count.get() == capacity) {
            // 队列满,当前线程在 notFull 条件上等待
            notFull.await();
        }
        
        // 队列有空闲空间,将新节点加入链表尾部
        enqueue(node);
        
        // 原子性地增加计数器,并返回增加前的值
        c = count.getAndIncrement();
        
        // 如果插入后队列仍未满,唤醒其他可能正在等待的入队线程
        if (c + 1 < capacity)
            notFull.signal();
    } finally {
        // 无论是否成功,最终都要释放锁
        putLock.unlock();
    }
    
    // 如果插入前队列为空(c == 0),需要唤醒可能正在等待的消费线程
    if (c == 0)
        signalNotEmpty();
}

/**
 * 将节点添加到队列尾部。
 * 该方法由持有putLock的生产者线程调用,确保线程安全。
 * 通过修改last指针实现O(1)时间复杂度操作。
 *
 * @param node 待添加的节点
 */
private void enqueue(Node<E> node) {
    // 调试断言:当前线程必须持有putLock
    // assert putLock.isHeldByCurrentThread();
    // 调试断言:队列原为空(last.next == null)
    // assert last.next == null;
    // 原子操作:将新节点链接到队列尾部
    last = last.next = node;
}

出队方法

和ArrayBlockingQueue一样,LinkedBlockingQueue同样提供了poll/remove/take这几个方法,功能同ArrayBlockingQueue。我们截取其中的阻塞方法分析下:

java 复制代码
public E take() throws InterruptedException {
    // 定义局部变量:x用于存储出队元素,c用于记录出队前的队列元素数量
    final E x;
    final int c;
    
    // 获取原子计数器和出队锁的本地引用,优化性能
    final AtomicInteger count = this.count;
    final ReentrantLock takeLock = this.takeLock;
    
    // 获取出队锁,支持中断响应
    takeLock.lockInterruptibly();
    try {
        // 循环检查队列是否为空(使用循环防止虚假唤醒)
        while (count.get() == 0) {
            // 队列为空,当前线程在notEmpty条件上等待
            notEmpty.await();
        }
        
        // 队列非空,从链表头部移除元素
        x = dequeue();
        
        // 原子性地减少计数器,并返回减少前的值
        c = count.getAndDecrement();
        
        // 如果出队前队列中还有多于1个元素,唤醒其他可能正在等待的消费线程
        if (c > 1)
            notEmpty.signal();
    } finally {
        // 确保锁被释放,避免死锁
        takeLock.unlock();
    }
    
    // 如果出队前队列是满的(c == capacity),需要唤醒可能正在等待的生产线程
    if (c == capacity)
        signalNotFull();
    
    // 返回出队的元素
    return x;
}

ArrayBlockingQueue与LinkedBlockingQueue性能对比

ArrayBlockingQueue和LinkedBlockingQueue在并发性能上的差异主要源于它们的锁机制设计,这直接影响了生产者和消费者能否同时工作。

ArrayBlockingQueue‌ 使用‌单锁设计‌,生产者和消费者共用一把锁。这意味着在任何时刻,只能有一个线程执行入队或出队操作,两者无法并行。

LinkedBlockingQueue‌ 采用‌双锁设计‌,拥有独立的入队锁(putLock)和出队锁(takeLock)。这使得生产者和消费者可以‌真正并发执行‌,大幅提升吞吐量。

下面通过一个例子来对比两者的并发性能:创建相同数量的生产者和消费者线程,分别使用ArrayBlockingQueue和LinkedBlockingQueue,统计相同时间内的任务处理数量。

java 复制代码
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicLong;

public class QueueBenchmark {
    private static final int PRODUCER_COUNT = 4;
    private static final int CONSUMER_COUNT = 4;
    private static final int TEST_DURATION_SECONDS = 10;
    private static final int QUEUE_CAPACITY = 1000;

    private static class Producer implements Runnable {
        private final BlockingQueue<Integer> queue;
        private final AtomicLong counter;
        private volatile boolean running = true;

        public Producer(BlockingQueue<Integer> queue, AtomicLong counter) {
            this.queue = queue;
            this.counter = counter;
        }

        @Override
        public void run() {
            try {
                while (running) {
                    queue.put(1);
                    counter.incrementAndGet();
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }

        public void stop() {
            running = false;
        }
    }

    private static class Consumer implements Runnable {
        private final BlockingQueue<Integer> queue;
        private final AtomicLong counter;
        private volatile boolean running = true;

        public Consumer(BlockingQueue<Integer> queue, AtomicLong counter) {
            this.queue = queue;
            this.counter = counter;
        }

        @Override
        public void run() {
            try {
                while (running) {
                    queue.take();
                    counter.incrementAndGet();
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }

        public void stop() {
            running = false;
        }
    }

    public static long benchmarkQueue(BlockingQueue<Integer> queue, String queueType)
            throws InterruptedException {

        AtomicLong produceCounter = new AtomicLong(0);
        AtomicLong consumeCounter = new AtomicLong(0);

        Producer[] producers = new Producer[PRODUCER_COUNT];
        Consumer[] consumers = new Consumer[CONSUMER_COUNT];

        Thread[] producerThreads = new Thread[PRODUCER_COUNT];
        Thread[] consumerThreads = new Thread[CONSUMER_COUNT];

        // 创建并启动生产者线程
        for (int i = 0; i < PRODUCER_COUNT; i++) {
            producers[i] = new Producer(queue, produceCounter);
            producerThreads[i] = new Thread(producers[i], "Producer-" + queueType + "-" + i);
            producerThreads[i].start();
        }

        // 创建并启动消费者线程
        for (int i = 0; i < CONSUMER_COUNT; i++) {
            consumers[i] = new Consumer(queue, consumeCounter);
            consumerThreads[i] = new Thread(consumers[i], "Consumer-" + queueType + "-" + i);
            consumerThreads[i].start();
        }

        // 运行测试指定时间
        Thread.sleep(TEST_DURATION_SECONDS * 1000);

        // 停止所有线程
        for (Producer producer : producers) {
            producer.stop();
        }
        for (Consumer consumer : consumers) {
            consumer.stop();
        }

        // 中断线程以确保快速停止
        for (Thread thread : producerThreads) {
            thread.interrupt();
        }
        for (Thread thread : consumerThreads) {
            thread.interrupt();
        }

        // 等待所有线程结束
        for (Thread thread : producerThreads) {
            thread.join(1000);
        }
        for (Thread thread : consumerThreads) {
            thread.join(1000);
        }

        long totalOperations = produceCounter.get() + consumeCounter.get();
        System.out.printf("%s 性能结果:%n", queueType);
        System.out.printf("  生产操作数: %,d%n", produceCounter.get());
        System.out.printf("  消费操作数: %,d%n", consumeCounter.get());
        System.out.printf("  总操作数: %,d%n", totalOperations);
        System.out.printf("  吞吐量: %,d 操作/秒%n", totalOperations / TEST_DURATION_SECONDS);
        System.out.println();

        return totalOperations;
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println("开始队列性能对比测试...");
        System.out.printf("测试配置: %d 生产者, %d 消费者, %d 秒测试时间, 队列容量 %d%n%n",
                PRODUCER_COUNT, CONSUMER_COUNT, TEST_DURATION_SECONDS, QUEUE_CAPACITY);

        // 测试 ArrayBlockingQueue
        BlockingQueue<Integer> arrayQueue = new ArrayBlockingQueue<>(QUEUE_CAPACITY);
        long arrayOps = benchmarkQueue(arrayQueue, "ArrayBlockingQueue");

        // 测试 LinkedBlockingQueue
        BlockingQueue<Integer> linkedQueue = new LinkedBlockingQueue<>(QUEUE_CAPACITY);
        long linkedOps = benchmarkQueue(linkedQueue, "LinkedBlockingQueue");

        // 性能对比分析
        double performanceRatio = (double) linkedOps / arrayOps;
        System.out.println("性能对比分析:");
        System.out.printf("LinkedBlockingQueue 比 ArrayBlockingQueue 快 %.2f 倍%n", performanceRatio);

        if (performanceRatio > 1.0) {
            System.out.println("结论: LinkedBlockingQueue 在并发场景下展现出明显的吞吐量优势");
        } else {
            System.out.println("结论: 在当前测试条件下,两种队列性能相近");
        }
    }
}

运行结果:

java 复制代码
开始队列性能对比测试...
测试配置: 4 生产者, 4 消费者, 10 秒测试时间, 队列容量 1000

ArrayBlockingQueue 性能结果:
  生产操作数: 104,607,461
  消费操作数: 104,607,461
  总操作数: 209,214,922
  吞吐量: 20,921,492 操作/秒

LinkedBlockingQueue 性能结果:
  生产操作数: 60,179,525
  消费操作数: 60,179,524
  总操作数: 120,359,049
  吞吐量: 12,035,904 操作/秒

性能对比分析:
LinkedBlockingQueue 比 ArrayBlockingQueue 快 0.58 倍
结论: 在当前测试条件下,两种队列性能相近

从运行结果看,线程数量比较少的情况下,ArrayBlockingQueue的性能反而比LinkedBlockingQueue的好。加大生产消费线程的数量到10,再次运行程序:

java 复制代码
开始队列性能对比测试...
测试配置: 10 生产者, 10 消费者, 10 秒测试时间, 队列容量 1000

ArrayBlockingQueue 性能结果:
  生产操作数: 56,185,094
  消费操作数: 56,185,094
  总操作数: 112,370,188
  吞吐量: 11,237,018 操作/秒

LinkedBlockingQueue 性能结果:
  生产操作数: 56,596,683
  消费操作数: 56,595,693
  总操作数: 113,192,376
  吞吐量: 11,319,237 操作/秒

性能对比分析:
LinkedBlockingQueue 比 ArrayBlockingQueue 快 1.01 倍
结论: LinkedBlockingQueue 在并发场景下展现出明显的吞吐量优势

加大生产消费线程的数量到20,再次运行程序:

java 复制代码
开始队列性能对比测试...
测试配置: 20 生产者, 20 消费者, 10 秒测试时间, 队列容量 1000

ArrayBlockingQueue 性能结果:
  生产操作数: 33,684,938
  消费操作数: 33,684,938
  总操作数: 67,369,876
  吞吐量: 6,736,987 操作/秒

LinkedBlockingQueue 性能结果:
  生产操作数: 57,188,358
  消费操作数: 57,187,361
  总操作数: 114,375,719
  吞吐量: 11,437,571 操作/秒

性能对比分析:
LinkedBlockingQueue 比 ArrayBlockingQueue 快 1.70 倍
结论: LinkedBlockingQueue 在并发场景下展现出明显的吞吐量优势

从运行结果可以看出,随着并发线程数的增多,LinkedBlockingQueue的性能优势越明显。

相关推荐
num_killer5 小时前
小白的Langchain学习
java·python·学习·langchain
期待のcode6 小时前
Java虚拟机的运行模式
java·开发语言·jvm
程序员老徐6 小时前
Tomcat源码分析三(Tomcat请求源码分析)
java·tomcat
a程序小傲6 小时前
京东Java面试被问:动态规划的状态压缩和优化技巧
java·开发语言·mysql·算法·adb·postgresql·深度优先
仙俊红6 小时前
spring的IoC(控制反转)面试题
java·后端·spring
阿湯哥6 小时前
AgentScope Java 集成 Spring AI Alibaba Workflow 完整指南
java·人工智能·spring
小楼v7 小时前
说说常见的限流算法及如何使用Redisson实现多机限流
java·后端·redisson·限流算法
与遨游于天地7 小时前
NIO的三个组件解决三个问题
java·后端·nio
czlczl200209257 小时前
Guava Cache 原理与实战
java·后端·spring
yangminlei7 小时前
Spring 事务探秘:核心机制与应用场景解析
java·spring boot