06、condition源码分析及基于condition实现阻塞队

基操

一、背景: Condition是在Java 1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用不需要在同步块中使用,Condition的await()、signal()这种方式实现线程间协作更加安全和高效
二、基本使用: await -> 让线程阻塞, 并且释放锁 signal -> 唤醒阻塞的线程
三、注意事项:

  1. Condition必须在同步块中使用,否则会抛出IllegalMonitorStateException异常。
  2. await()方法会使当前线程进入等待状态,直到被其他线程调用signal()方法唤醒。如果在await()方法返回时没有信号被发出,那么当前线程会一直等待下去,直到被其他线程调用signal()方法唤醒。
  3. signal()方法会使当前线程从等待状态中醒来,如果有其他线程已经调用了await()方法并等待在某个Condition对象上,那么这些线程会被唤醒并重新竞争资源。
  4. 如果一个Condition对象已经被锁定,那么再次调用signal()或await()方法会导致死锁。

**四、具体源码分析:**详细见实现原理

五、Condition的原理(猜测)

  • await
    • 可以阻塞N个线程
    • 会释放持有的锁
  • signal、signalAll
  • 如何让线程等待
  • 等待队列来存储等待中的线程
  • 唤醒等待的线程
  • AQS中的同步队列和Condition中的等待队列的线程的转移

六、 技术关联性:
ReentrantLock、ArrayBlockingQueue中有使用
七、记录笔记:
八、问题排查:
九面试题要点:
ReentrantLock Condition条件队列流程

Conditon的实现原理

await

  1. 释放锁
  2. 让释放锁的线程,应该被阻塞。
  3. 被阻塞之后要存储到队列中。
  4. 重新去竞争锁。->AQS的逻辑
  5. 要能够处理interupt()的中断响应。

这段Java代码是一个用于线程等待和唤醒的实现。下面是对代码的注释说明:

public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    // 添加到等待队列
    Node node = addConditionWaiter();
    // 完整的释放锁(考虑重入问题)
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    while (!isOnSyncQueue(node)) {
        // 上下文切换(程序计数器、寄存器) 用户态-内核态的转化(上下文切换)
        LockSupport.park(this); // 阻塞当前线程,当其他线程调用signal()方法时,该线程会从这个位置去执行
        // 要判断当前被阻塞的线程是否是因为interrupt()唤醒
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    // 重新竞争锁,savedState表示的是被释放的锁的重入次数.
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // 清理已取消的等待线程
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

这段代码实现了一个await()方法,用于使当前线程等待并唤醒。以下是代码中各部分的解释:

  1. 首先,检查当前线程是否已经被中断,如果是,则抛出InterruptedException异常。
  2. 将当前线程添加到等待队列中,通过调用addConditionWaiter()方法获取一个节点对象。
  3. 释放当前线程持有的锁,并将释放锁的状态保存在savedState变量中。这里考虑了重入问题。
  4. 进入一个循环,直到当前线程位于同步队列上为止。在循环中,进行以下操作:
    • 使用LockSupport.park(this)方法阻塞当前线程,使其进入等待状态。
    • 检查当前线程是否因为interrupt()方法而被唤醒,如果是,则将interruptMode设置为非零值,并跳出循环。
  5. 在循环结束后,重新竞争锁。如果成功获取到锁并且interruptMode不等于THROW_IE,则将interruptMode设置为REINTERRUPT
  6. 如果等待队列中还有其他线程等待,则清理已取消的等待线程,通过调用unlinkCancelledWaiters()方法实现。
  7. 如果interruptMode不等于零,则在唤醒后报告中断情况,通过调用reportInterruptAfterWait(interruptMode)方法实现。

以下是带有注释的代码:

private Node addConditionWaiter() {
    // 获取最后一个等待节点
    Node t = lastWaiter;
    // 如果最后一个等待节点存在且其状态不是条件状态,则清除所有已取消的等待节点
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
    // 创建一个新的节点,该节点的线程是当前线程,状态是条件状态
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    // 如果最后一个等待节点为空,则将新节点设置为第一个等待节点
    if (t == null)
        firstWaiter = node;
    // 否则,将新节点添加到最后一个等待节点的后面
    else
        t.nextWaiter = node;
    // 更新最后一个等待节点为新节点
    lastWaiter = node;
    // 返回新创建的节点
    return node;
}

上面代码说明:

  1. 获取最后一个等待节点(lastWaiter)。
  2. 如果最后一个等待节点存在且其状态不是条件状态(CONDITION),则调用unlinkCancelledWaiters()方法来清除所有已取消的等待节点。
  3. 创建一个新的节点,该节点的线程是当前线程,状态是条件状态。
  4. 如果最后一个等待节点为空,则将新节点设置为第一个等待节点;否则,将新节点添加到最后一个等待节点的后面。
  5. 更新最后一个等待节点为新节点。
  6. 返回新创建的节点。

signal

  • 要把被阻塞的线程,先唤醒(signal、signalAll)
  • 把等待队列中被唤醒的线程转移到AQS队列中
    这段Java代码是关于线程同步和通信的,具体来说是关于使用信号量来实现线程之间的通信。

以下是对这段代码的注释说明:

// 定义一个公共的无返回值的方法,用于发送信号
public final void signal() {
    // 如果当前线程没有独占地持有锁,则抛出IllegalMonitorStateException异常
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    // 获取等待队列的第一个节点
    Node first = firstWaiter;
    // 如果等待队列不为空,则唤醒队列中的第一个线程
    if (first != null)
        doSignal(first);
}

// 私有方法,用于唤醒等待队列中的一个线程
private void doSignal(Node first) {
    // 循环直到成功唤醒线程或没有更多的等待线程
    do {
        // 将firstWaiter设置为其下一个等待线程
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        // 将first的nextWaiter设置为null,表示该线程已经唤醒
        first.nextWaiter = null;
    } while (!transferForSignal(first) &&
            (first = firstWaiter) != null);
}

主要目的是实现线程间的通信,通过信号量来控制线程的执行顺序。当一个线程需要唤醒其他线程时,它会调用signal()方法,该方法会检查当前线程是否独占地持有锁,如果不是,则抛出异常。如果是,则获取等待队列的第一个节点,并尝试唤醒它。如果唤醒失败或者没有更多的等待线程,那么就会结束循环。

这段Java代码是一个名为transferForSignal的方法,它接受一个Node类型的参数node。下面是对该方法的逐行解释和注释:

final boolean transferForSignal(Node node) {
    // 检查并设置节点的等待状态为0,如果设置失败则返回false
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;

    // 将当前从等待队列中头部节点保存到AQS队列
    Node p = enq(node);

    // 获取节点的等待状态
    int ws = p.waitStatus;

    // 如果节点的等待状态大于0或者无法将节点的等待状态设置为信号状态,则唤醒节点的线程
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread); // 唤醒节点的线程

    // 返回true表示成功转移
    return true;
}

将一个节点从等待状态转移到信号状态,并在转移成功后唤醒该节点的线程。具体步骤如下:

  1. 首先,通过调用compareAndSetWaitStatus方法来检查并设置节点的等待状态为0。如果设置失败(可能是因为其他线程已经设置了相同的等待状态),则返回false。
  2. 然后,将当前从等待队列中头部节点保存到AQS队列中,使用enq方法实现。
  3. 接下来,获取节点的等待状态,并将其保存在变量ws中。
  4. 如果节点的等待状态大于0(表示节点之前是处于等待状态)或者无法将节点的等待状态设置为信号状态(可能是因为其他线程已经设置了相同的等待状态),则调用LockSupport.unpark方法来唤醒节点的线程。
  5. 最后,返回true表示成功转移。

再回到await方法

  • 抢占锁即可.
public final void await() throws InterruptedException {
    // 检查当前线程是否被中断,如果是则抛出InterruptedException异常
    if (Thread.interrupted())
        throw new InterruptedException();
    
    // 调用addConditionWaiter()方法获取一个节点对象
    Node node = addConditionWaiter();
    
    // 释放节点对象的资源并返回保存的状态
    int savedState = fullyRelease(node);
    
    // 初始化中断模式为0
    int interruptMode = 0;
    
    // 循环等待直到节点对象在同步队列上
    while (!isOnSyncQueue(node)) {
        // 使用LockSupport.park()方法将当前线程挂起,等待唤醒
        LockSupport.park(this);
        
        // 检查在等待过程中是否发生中断,如果是则更新中断模式并跳出循环
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    
    // 如果成功获取到节点对象的资源并且中断模式不等于THROW_IE,则设置中断模式为REINTERRUPT
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    
    // 如果节点对象的下一个等待者不为空(即有其他线程在等待),则清除已取消的等待者
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    
    // 如果中断模式不等于0,则报告在等待后的中断情况
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

在多线程环境中协调线程的执行顺序。具体来说,它通过调用addConditionWaiter()方法获取一个条件节点对象,然后进入一个循环,在该循环中,线程会调用LockSupport.park()方法将自己挂起,等待条件满足。同时,它还提供了一些额外的功能,如在等待过程中检查和处理中断、清理已取消的等待者以及报告等待后的中断情况。
这段Java代码是一个方法的实现,名为acquireQueued。下面是对该方法的解释和注释:

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true; // 初始化failed为true,表示获取操作失败
    try {
        boolean interrupted = false; // 初始化interrupted为false,表示是否被中断
        for (;;) { // 无限循环
            final Node p = node.predecessor(); // 获取node的前驱节点p
            if (p == head && tryAcquire(arg)) { // 如果p是头节点并且尝试获取资源成功
                setHead(node); // 将当前节点设置为头节点
                p.next = null; // 帮助垃圾回收器进行回收
                failed = false; // 设置failed为false,表示获取操作成功
                return interrupted; // 返回interrupted,表示是否被中断
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt()) // 如果应该在获取失败后挂起,并且检查是否被中断
                interrupted = true; // 设置interrupted为true,表示被中断
        }
    } finally {
        if (failed) // 如果获取操作失败
            cancelAcquire(node); // 取消获取操作
    }
}

该方法的作用是从等待队列中获取一个资源,如果获取成功则返回true,否则返回false。在方法内部,通过一个无限循环来不断尝试获取资源,直到成功或失败为止。

首先,方法会获取传入的节点node的前驱节点p。然后,判断如果p是头节点并且尝试获取资源成功,就将当前节点设置为头节点,将p.next置为null以帮助垃圾回收器进行回收,并将failed设置为false表示获取操作成功。最后,返回interrupted表示是否被中断。

如果在获取资源的过程中发生异常,或者获取资源失败,就会进入finally块,在其中调用cancelAcquire(node)来取消获取操作。

总结起来,这段代码实现了一个阻塞式的获取资源的方法,通过循环尝试获取资源,并根据不同的情况进行处理。

图解condition

Condition的实际应用

-> 实现阻塞队列(业务组件)
-> 在线程池中会用到阻塞队列
-> 生产者消费者
-> 流量缓冲

什么叫阻塞队列?

队列是一种只允许在一端进行删除操作,在另一端进行插入操作的线性表,允许插入的一端称为队尾、允许删除的一端称为队头。
那么阻塞队列,实际上是在队列的基础上增加了两个操作。

  • 支持阻塞插入:队列满了的情况下,会阻塞继续往队列中添加数据的线程,直到队列元素被释放。
  • 只是阻塞移除:队列为空的情况下,会阻塞从队列中获取元素的线程,直到队列添加了新的元素。

阻塞队列中的方法

  • 添加元素
    针对队列满了之后的不同的处理策略
    - add -> 如果队列满了,抛出异常
    - offer -> true/false , 添加成功返回true,否则返回false
    put -> 如果队列满了,则一直阻塞
    offer(timeout) , 带了一个超时时间。如果添加一个元素,队列满了,此时会阻塞timeout时长,超过阻塞时长,返回false。

  • 移除元素

    • element-> 队列为空,抛异常
    • peek -> true/false , 移除成功返回true,否则返回false
    • take -> 一直阻塞
    • poll(timeout) -> 如果超时了,还没有元素,则返回null

dequeue -> LIFO , FIFO的队列.

J.U.C 中的阻塞队列

- ArrayBlockingQueue 基于数组结构

特征:
基于数组实现,队列容量固定。存/取数据的操作共用一把锁(默认非公平锁),无法实现真正意义上存/取操作并行执行。

分析:
由于基于数组,容量固定所以不容易出现内存占用率过高,但是如果容量太小,取数据比存数据的速度慢,那么会造成过多的线程进入阻塞(也可以使用offer()方法达到不阻塞线程), 此外由于存取共用一把锁,所以有高并发和吞吐量的要求情况下,我们也不建议使用ArrayBlockingQueue。

使用场景:
在上述的情况下,笔者觉得它的使用场景更多应该放在项目的一些次级业务中,比如:
人事系统中员工离职/变更后,其他依赖应用进行数据同步。
适用于生产者-消费者模式的场景
在一些项目中,可能同公司的其他部门的应用服务会要求同步我们人事系统的部分组织架构数据,但是当人事系统数据发生变更后,应用的依赖方需要进行数据的同步, 这种场景下,由于员工离职/变更操作不是非常频繁,所以能有效防止线程阻塞,也基本没有并发和吞吐量的要求,所以可以将数据存放到ArrayBlockingQueue中, 由依赖方应用服务进行获取同步。

ArrayBlockingQueue 适用于生产者-消费者模式的场景示例:

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class ArrayBlockingQueueExample {

    public static void main(String[] args) {
        // 创建一个容量为 5 的 ArrayBlockingQueue
        BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(5);

        // 生产者线程
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    System.out.println("生产者生产:" + i);
                    queue.put(i); // 插入数据到队列,如果队列已满,则阻塞等待
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        // 消费者线程
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    Integer item = queue.take(); // 从队列中取出数据,如果队列为空,则阻塞等待
                    System.out.println("消费者消费:" + item);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

在这个示例中,我们创建了一个容量为 5 的 ArrayBlockingQueue,然后启动了两个线程,一个生产者线程和一个消费者线程。生产者线程向队列中插入数据,消费者线程从队列中取出数据。由于 ArrayBlockingQueue 的特性,当队列满时,生产者线程会阻塞等待;当队列为空时,消费者线程会阻塞等待。这样就实现了生产者-消费者模式。

- LinkedBlockingQueue 基于链表结构
特征:
LinkedBlockingQueue基于链表实现,队列容量默认Integer.MAX_VALUE
存/取数据的操作分别拥有独立的锁,可实现存/取并行执行。

分析:
1.基于链表,数据的新增和移除速度比数组快,但是每次存储/取出数据都会有Node对象的新建和移除,所以也存在由于GC影响性能的可能
2.默认容量非常大,所以存储数据的线程基本不会阻塞,但是如果消费速度过低,内存占用可能会飙升。
3.读/取操作锁分离,所以适合有并发和吞吐量要求的项目中

使用场景:

LinkedBlockingQueue是Java中的一个阻塞队列,它实现了BlockingQueue接口。它的使用场景主要有以下几点:

  1. 生产者-消费者模型:在生产者和消费者模型中,生产者将数据放入队列,消费者从队列中取出数据。如果队列为空,消费者线程会等待,直到有新的数据被放入;如果队列已满,生产者线程会等待,直到有空间可用。

  2. 缓存:LinkedBlockingQueue可以用作缓存,存储最近访问过的数据。当缓存满时,需要替换掉最早进入队列的数据。

  3. 任务调度:LinkedBlockingQueue可以用于任务调度,例如一个线程池中的线程需要执行的任务队列。

  4. 消息队列:在分布式系统中,LinkedBlockingQueue可以用作消息队列,实现不同服务之间的通信。

  5. 订单完成的邮件/短信提醒。

  6. 主要是在多线程环境下,用于存储和管理数据。

以下是一个简单的使用LinkedBlockingQueue的示例代码:

import java.util.concurrent.LinkedBlockingQueue;

public class Test {
    public static void main(String[] args) {
        // 创建一个容量为5的LinkedBlockingQueue
        LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<>(5);

        // 创建并启动生产者线程
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    System.out.println("生产者生产:" + i);
                    queue.put(i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        // 创建并启动消费者线程
        new Thread(() -> {
            while (true) {
                try {
                    Integer num = queue.take();
                    System.out.println("消费者消费:" + num);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

在这个示例中,我们创建了一个容量为5的LinkedBlockingQueue,然后创建了两个线程,一个生产者线程和一个消费者线程。生产者线程将0到9这10个数字放入队列,消费者线程从队列中取出并打印这些数字。
订单系统中当用户下单成功后,将信息放入ArrayBlockingQueue中,由消息推送系统取出数据进行消息推送提示用户下单成功。

如果订单的成交量非常大,那么使用ArrayBlockingQueue就会有一些问题,固定数组很容易被使用完,此时调用的线程会进入阻塞,那么可能无法及时将消息推送出去,所以使用LinkedBlockingQueue比较合适,但是要注意消费速度不能太低,不然很容易内存被使用完(一般而言不会时时刻刻生产消息, 但是需要预防消息大量堆积)比较ArrayBlockingQueue:
实际上对于ArrayBlockingQueue和LinkedBlockingQueue在处理普通的生产者-消费者问题时,两者一般可互相替换使用。

- PriorityBlcokingQueue 基于优先级队列

特征:
基于数组实现,队列容量最大为Integer.MAX_VALUE - 8(减8是因为数组的对象头)。

根据传入的优先级进行排序,保证按优先级来消费

分析
优先级阻塞队列中存在一次排序,根据优先级来将数据放入到头部或者尾部排序带来的损耗因素,由二叉树最小堆排序算法来降低

使用场景:
在项目上存在优先级的业务

  1. 生产者-消费者模型:在生产者-消费者模型中,生产者将数据放入队列,消费者从队列中取出数据进行处理。由于PriorityBlockingQueue是一个线程安全的队列,所以它可以在多线程环境下使用。

  2. 任务调度:PriorityBlockingQueue可以用于任务调度,例如,可以将任务按照优先级放入队列,然后由一个线程池来处理这些任务。

  3. 缓存:PriorityBlockingQueue也可以用于实现简单的缓存,例如,可以将最近访问过的数据放入队列,然后由一个后台线程来处理这些数据。

  4. 工作队列:在多线程编程中,可以使用PriorityBlockingQueue作为工作队列,将任务按照优先级放入队列,然后由一个线程池来处理这些任务。

  5. VIP排队购票

以下是一个简单的PriorityBlockingQueue的使用示例:

import java.util.concurrent.PriorityBlockingQueue;

public class Test {
    public static void main(String[] args) {
        // 创建一个优先队列
        PriorityBlockingQueue<Integer> queue = new PriorityBlockingQueue<>();

        // 创建并启动消费者线程
        new Thread(() -> {
            while (true) {
                try {
                    // 从队列中取出元素,如果队列为空,则等待
                    int element = queue.take();
                    System.out.println("消费者取出:" + element);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        // 创建并启动生产者线程
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    // 向队列中添加元素
                    queue.put(i);
                    System.out.println("生产者放入:" + i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

在这个示例中,我们创建了一个优先队列,并启动了一个消费者线程和一个生产者线程。消费者线程会从队列中取出元素,而生产者线程则会向队列中添加元素。
- DelayQueue 允许延时执行的队列
DelayQueue使用场景
特征:
DelayQueue延迟队列,基于优先级队列来实现
存储元素必须实现Delayed接口(Delayed接口继承了Comparable接口)

分析:
由于是基于优先级队列实现,但是它比较的是时间,我们可以根据需要去倒叙或者正序排列(一般都是倒叙,用于倒计时)

使用场景:
以下是一些 DelayQueue 的使用场景及示例代码:

  1. 缓存系统:使用 DelayQueue 作为缓存系统的底层实现,可以有效地减少缓存穿透和缓存击穿的问题。生产者将数据添加到 DelayQueue 中,消费者从 DelayQueue 中获取数据进行缓存。
import java.util.concurrent.*;

public class CacheSystem {
    private final int capacity;
    private final DelayQueue<String> cacheQueue;
    private final LinkedBlockingQueue<String> producerQueue;
    private final LinkedBlockingQueue<String> consumerQueue;

    public CacheSystem(int capacity) {
        this.capacity = capacity;
        this.cacheQueue = new DelayQueue<>(capacity);
        this.producerQueue = new LinkedBlockingQueue<>();
        this.consumerQueue = new LinkedBlockingQueue<>();
    }

    public void produce(String data) throws InterruptedException {
        if (cacheQueue.remainingCapacity() < 1) {
            // 如果缓存队列已满,生产者线程阻塞等待
            Thread.sleep(1000);
        }
        cacheQueue.put(data);
    }

    public String consume() throws InterruptedException {
        while (true) {
            String data = cacheQueue.take();
            if (data != null) {
                consumerQueue.put(data);
                break;
            } else {
                // 如果缓存队列为空,消费者线程阻塞等待
                Thread.sleep(1000);
            }
        }
        return data;
    }
}
  1. 任务调度:使用 DelayQueue 作为任务调度器,可以将任务按照优先级进行排序,从而实现优先级高的任务优先执行。
import java.util.concurrent.*;

public class TaskScheduler {
    private final int priority;
    private final DelayQueue<Runnable> taskQueue;

    public TaskScheduler(int priority) {
        this.priority = priority;
        this.taskQueue = new DelayQueue<>(priority);
    }

    public void schedule(Runnable task, long delayTime) {
        taskQueue.put(new DelayedTask(task, delayTime));
    }

    public void start() {
        new Thread(() -> {
            while (true) {
                try {
                    Runnable task = taskQueue.take();
                    task.run();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

class DelayedTask implements Comparable<DelayedTask> {
    private final Runnable task;
    private final long endTime;

    public DelayedTask(Runnable task, long delayTime) {
        this.task = task;
        this.endTime = System.currentTimeMillis() + delayTime;
    }

    @Override
    public int compareTo(DelayedTask o) {
        return Long.compare(this.endTime, o.endTime);
    }
}
  1. 限流器:使用 DelayQueue 作为限流器,可以实现对请求的速率限制。当请求达到一定速率时,生产者线程会阻塞等待,直到有足够的空间放入新的请求。
import java.util.concurrent.*;

public class RateLimiter {
    private final int maxRequestPerSecond;
    private final DelayQueue<Runnable> requestQueue;
    private final int capacity;

    public RateLimiter(int maxRequestPerSecond, int capacity) {
        this.maxRequestPerSecond = maxRequestPerSecond;
        this.capacity = capacity;
        this.requestQueue = new DelayQueue<>(capacity);
    }

    public void produce(Runnable request) throws InterruptedException {
        if (requestQueue.remainingCapacity() < 1) {
            // 如果请求队列已满,生产者线程阻塞等待
            Thread.sleep(1000 / maxRequestPerSecond);
        }
        requestQueue.put(request);
    }
}

订单超时取消功能

用户下订单未支付开始倒计时,超时则释放订单中的资源,如果取消或者完成支付,我们再讲队列中的数据移除掉。

网站刷题倒计时 (实现代码在文章末尾)

- SynchronousQueue 没有任何存储结构的的队列
SynchronousQueue使用场景
特征:
采用双栈双队列算法的无空间队列或栈任何一个对SynchronousQueue写需要等到一个对SynchronousQueue的读操作,任何一个个读操作需要等待一个写操作没有容量,是无缓冲等待队列,是一个不存储元素的阻塞队列,会直接将任务交给消费者。

分析:
相当于是交换通道,不存储任何元素,提供者和消费者是需要组队完成工作,缺少一个将会阻塞线程,直到等到配对为止

使用场景:

参考线程池newCachedThreadPool()。

如果我们不确定每一个来自生产者请求数量但是需要很快的处理掉,那么配合SynchronousQueue为每个生产者请求分配一个消费线程是最简洁的办法。Executors.newCachedThreadPool()就使用了SynchronousQueue,这个线程池根据需要(新任务到来时)创建新的线程,如果有空闲线程则会重复使用,线程默认空闲了60秒后会被回收。
轻量级别的任务转交
比如会话转交,通常坐席需要进行会话转交,如果有坐席在线那么会为我们分配一个客服,但是如果没有,那么阻塞请求线程,一段时间后会超时或者提示当前坐席已满。

  • newCachedThreadPool.
    可缓存的线程池。
    可以处理非常大请求的任务。 1000个任务过来,那么线程池需要分配1000个线程来执行。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值