JAVA面试题分享五百四十四:实现一个简单实用的并发同步模型

目录

写在文章开头

设计思路

实践

任务封装

队列声明

实现生产者

实现消费者

基于线程管理消费者

测试


写在文章开头

日常开发后端接口时,总是会遇到一些和业务关联性不是很大却又很耗时的操作,由于功能的重要性和体量远达不到要上消息中间件的情况,这时候我们就可以实现一个简单的生产者消费者模型来实现异步消费。就以笔者这篇文章为例,通过「JUC」包下的阻塞队列实现了一个简单的并发同步模型。

本文整体结构如下,通过笔者的代码示例,你会对生产者消费者这种并发同步的设计模式的开发模型和使用场景有着更进一步的理解。

图片

设计思路

简单来说生产者消费者模型就是让多线程去异步消费生产者的任务,对于「web开发而言」,我们的生产者可以是任意的「HTTP」请求,这些「HTTP」请求会将一些耗时操作提交到队列中让消费者进行消费(这里消费者可以是一个异步的线程或者线程池,具体看读者的业务场景) 所以我们的实现思路如下:

  1. 封装一个任务,将用户的耗时操作封装到该任务中。

  2. 声明一个队列,存储Web请求中的耗时操作。

  3. 将web接口中的耗时操作提交到队列中。

  4. 创建一个线程池,异步消费队列中的任务。

图片

实践

任务封装

这里笔者假设耗时的操作是对一个第三方接口的请求,所以笔者在封装任务时,只需在任务中声明调第三方接口的参数即可:

/**
 * 要被执行的任务
 */
@Data
public class Task {

    /**
     * 任务id
     */
    private Long id;
    /**
     * 任务名称
     */
    private String taskName;
    /**
     * 请求参数
     */
    private JSONObject params;
    /**
     * 创建时间
     */
    private DateTime createTime;
    /**
     * 结束时间
     */
    private DateTime finishTime;
}

队列声明

为了方便管理,我们将阻塞队列以聚合的方式封装一个「QueueBean」交给「Spring」进行管理,注意笔者这里声明的阻塞队列的容量为「2000」仅仅是示例,具体数值读者需要结合压测进行调整,参考「StackOverflow」的回答一般建议设置为可分配的堆内存大小除以对象平均字节数:

Make it "as large as is reasonable". For example, if you are OK with it consuming up to 1Gb of memory, then allocate its size to be 1Gb divided by the average number of bytes of the objects in the queue.

笔者这里就简单设置为2000:

@Component
@Slf4j
public class QueueBean {

    private BlockingQueue<Task> blockingQueue = new ArrayBlockingQueue<>(2000);


    @SneakyThrows
    public void put(Task task) {
        blockingQueue.put(task);
    }

    @SneakyThrows
    public Task take() {
        return blockingQueue.take();
    }


}

实现生产者

我们的HTTP请求就是一个生产者,所以在业务执行过程中,笔者将耗时的三方请求封装为「Task」提交到阻塞队列中交由消费者异步消费:

@Autowired
    private QueueBean queueBean;


@PostMapping("/submitTask")
    public String submitTask() {
        Task task = new Task();
        long id = snowflake.nextId();
        task.setId(id);
        task.setTaskName("任务-" + id);
        task.setParams(new JSONObject().putOnce("userName", RandomUtil.randomString(5)));
        task.setCreateTime(new DateTime());
        task.setFinishTime(new DateTime());

        log.info("提交任务:{}", JSONUtil.toJsonStr(task));
        queueBean.put(task);
        return "success";
    }

实现消费者

因为消费者的执行逻辑需要提交到线程池中让池中的线程进行处理,所以我们这里封装了一个消费的「Runnable」 ,因为这个「Runnable」不受「Spring」容器管理,所以获取「Spring」容器中的队列可以采用「hutool」封装的「SpringUtil」上下文,当然如果了解「Spring」扩展点的读者也可以采用「ApplicationContext」获取阻塞队列,而笔者对于任务消费逻辑比较简单,仅仅打印一下任务信息:

@Slf4j
public class ConsumerTask implements Runnable {


    @Override
    public void run() {
        QueueBean queueBean = SpringUtil.getBean(QueueBean.class);
        while (true) {
            //从阻塞队列中获取任务
            Task task = queueBean.take();
            log.info("消费者消费任务,任务详情:{}", JSONUtil.toJsonStr(task));

        }
    }
}

基于线程管理消费者

完成消费者的封装之后,我们采用线程池的方式创建线程来执行消费者的逻辑,可以看到笔者采用「Spring」后置的扩展点,确保在线程池的「Bean」完成加载之后对线程池进行初始化,并提交5个消费者。

 private static ThreadPoolExecutor threadPoolExecutor = ExecutorBuilder.create()
            .setCorePoolSize(Runtime.getRuntime().availableProcessors())
            .setMaxPoolSize(Runtime.getRuntime().availableProcessors() << 2)
            .setThreadFactory(new NamedThreadFactory("consumerTask-", false))
            .build();


    @PostConstruct
    private void init() {
        for (int i = 0; i < 5; i++) {
            threadPoolExecutor.execute(new ConsumerTask());
        }
    }

测试

我们将应用启动后可以看到下面这段输出,不难看出每当我们的「HTTP」请求提交一个任务到队列中后,总有一个线程池中的线程出来消费者任务,两者高效并发同步的同时又能保证线程安全:

2024-01-02 15:20:31.328  INFO 12084 --- [nio-8080-exec-8] c.s.q.controller.BasicController         : 提交任务:{"id":1742083432578711552,"taskName":"任务-1742083432578711552","params":{"userName":"9b53q"},"createTime":1704180031328,"finishTime":1704180031328}
2024-01-02 15:20:31.329  INFO 12084 --- [ consumerTask-1] c.s.queueSync.task.ConsumerTask          : 消费者消费任务,任务详情:{"id":1742083432578711552,"taskName":"任务-1742083432578711552","params":{"userName":"9b53q"},"createTime":1704180031328,"finishTime":1704180031328}
2024-01-02 15:20:31.934  INFO 12084 --- [io-8080-exec-10] c.s.q.controller.BasicController         : 提交任务:{"id":1742083435116265472,"taskName":"任务-1742083435116265472","params":{"userName":"xwoth"},"createTime":1704180031933,"finishTime":1704180031933}
2024-01-02 15:20:31.934  INFO 12084 --- [ consumerTask-3] c.s.queueSync.task.ConsumerTask          : 消费者消费任务,任务详情:{"id":1742083435116265472,"taskName":"任务-1742083435116265472","params":{"userName":"xwoth"},"createTime":1704180031933,"finishTime":1704180031933}
2024-01-02 15:20:32.623  INFO 12084 --- [nio-8080-exec-1] c.s.q.controller.BasicController         : 提交任务:{"id":1742083438010335232,"taskName":"任务-1742083438010335232","params":{"userName":"2udas"},"createTime":1704180032623,"finishTime":1704180032623}
2024-01-02 15:20:32.624  INFO 12084 --- [ consumerTask-2] c.s.queueSync.task.ConsumerTask          : 消费者消费任务,任务详情:{"id":1742083438010335232,"taskName":"任务-1742083438010335232","params":{"userName":"2udas"},"createTime":1704180032623,"finishTime":1704180032623}

  • 22
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

之乎者也·

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值