使用ScheduledFuture导致OOM

 业务场景描述:

我们公司有一个项目,它会在一个比较固定的时间oom。后来根据输出的堆日志定位到了问题所在。问题存在于一个定时任务接口,该接口的业务流程是先查询令牌桶中有没有令牌,如果有令牌就执行业务逻辑,如果没有令牌就自旋5次,要是还没拿到令牌就打印日志。我排查一下发现令牌桶中的ScheduledFuture线程不会自动停止,它会不断添加令牌除非系统停止。下面是我对这次事故的分析。

错误写法:

令牌桶类:

package com.haiwang.hotel.utils;

import lombok.extern.log4j.Log4j2;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.*;

@Log4j2
public class TokenLimiter {


    private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");

    private ScheduledFuture<?> future;

    private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();

    /**
     * 令牌
     */
    private static final String TOKEN = "token";

    /**
     * 阻塞队列,线程安全,非公平容量有限
     */
    private ArrayBlockingQueue<String> arrayBlockingQueue;

    /**
     * 令牌桶容量
     */
    private int limit;

    /**
     * 令牌每次生成的数量
     */
    private int amount;

    /**
     * 令牌桶间隔时间——单位时间
     */
    private int period;

    public TokenLimiter(int limit, int period, int amount) {
        this.limit = limit;
        this.amount = amount;
        this.period = period;
        arrayBlockingQueue = new ArrayBlockingQueue<>(limit);
        init();
    }

    /**
     * 初始化生成令牌,按桶容量生成
     */
    private void init() {
        for (int i = 0; i < limit; i++) {
            arrayBlockingQueue.offer(TOKEN);
        }
    }


    /**
     * 启动添加线程
     * @param lock
     */
    public void start(Object lock) {


        this.future = this.executor.scheduleWithFixedDelay(() -> {
            synchronized (lock) {
                // 往令牌桶添加令牌
                addToken();
                lock.notifyAll();
            }
        }, this.period, period, TimeUnit.MILLISECONDS);

    }


    /**
     * 添加令牌
     */
    private void addToken() {
        for (int i = 0; i < this.amount; i++) {
            // 溢出返回false
            arrayBlockingQueue.offer(TOKEN);
            log.info("添加令牌");
        }
    }

    public boolean tryAcquire() {
        // 队首元素出队
        return arrayBlockingQueue.poll() != null;
    }

    @Override
    public String toString() {
        return "TokenLimiter{" +
                "arrayBlockingQueue=" + arrayBlockingQueue +
                ", limit=" + limit +
                ", amount=" + amount +
                ", period=" + period +
                '}';
    }
}

业务代码:

    //规定每秒并发数为3
    @ApiOperation(value = "测试锁")
    @GetMapping("/testLock")
    public Result<?> testLock() {



        //定义好信息阻塞队列
        ArrayBlockingQueue<String> arrayBlockingQueue = new ArrayBlockingQueue(10);
        arrayBlockingQueue.add("A1");
        arrayBlockingQueue.add("A2");
        arrayBlockingQueue.add("A3");
        arrayBlockingQueue.add("A4");
        arrayBlockingQueue.add("A5");
        arrayBlockingQueue.add("A6");
        arrayBlockingQueue.add("A7");
        arrayBlockingQueue.add("A8");
        arrayBlockingQueue.add("A9");
        arrayBlockingQueue.add("A10");



        //定义好令牌桶
        TokenLimiter limiter = new TokenLimiter(3, 1000, 3);
        // 生产令牌
        limiter.start(LOCK);
        int size = arrayBlockingQueue.size();

        //创建线程并进行推送
        for (int i = 0; i < size; i++) {
            threadPoolTaskExecutor.execute(() -> {
                String msg = arrayBlockingQueue.poll();
                if(limiter.tryAcquire()){
                    log.info("推送信息{},当前线程名称为:{},当前时间为:{},",msg,Thread.currentThread().getName(),sdf.format(new Date()));
                }else {
                    boolean flag = false;
                    //重试五次,成功就推送,失败就不推送
                    for (int j = 0; j < 4; j++){
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            log.error("线程中断错误");
                        }
                        if(limiter.tryAcquire()){

                            log.info("推送信息{},当前线程名称为:{},当前时间为:{}",msg,Thread.currentThread().getName(),sdf.format(new Date()));
                            flag = true;
                            break;
                        }
                    }
                    if(!flag){
                        log.info("线程繁忙,无法进行业务,当前线程名称为:{},推送信息{}",Thread.currentThread().getName(),msg);
                    }
                }

            });
        }
       


        return Result.success(1);
    }

 错误原因:

这个写法咋看之下好像没什么问题,但是实际上问题很大,因为ScheduledFuture这个类不像CompletableFuture.runAsync一样会随着主线程结束而自动销毁,它会一直不断的运行,并且你新建一个令牌桶类,它就会新建一个线程并且一直运行不断消耗服务器资源。可以从下图看到,即使主方法返回结束了,该线程也会不断运行。

 

 如果你打开OOM时自动输出的堆日志时,经过分析你可以发现大量ScheduledFuture处于阻塞状态,导致了资源被大量占用导致了oom。因此java程序启动的脚本一定要加上oom自动输出堆日志的参数。具体怎么操作可以参考这篇文章:JAVA常用启动脚本_java启动脚本_循环网络不循环的博客-CSDN博客

 

正确写法:

我们在调用完令牌桶类后,需要手动关闭线程,避免资源过度占用。

令牌桶类:

令牌桶类需要增加停止方法。

    /**
     * 关闭线程
     *
     */
    public void stop() {
        executor.shutdown();
        future.cancel(true);

    }

业务代码:

业务代码也需要执行完业务代码后手动关闭线程。

    @ApiOperation(value = "测试锁")
    @GetMapping("/testLock")
    public Result<?> testLock() {



        //定义好信息阻塞队列
        ArrayBlockingQueue<String> arrayBlockingQueue = new ArrayBlockingQueue(10);
        arrayBlockingQueue.add("A1");
        arrayBlockingQueue.add("A2");
        arrayBlockingQueue.add("A3");
        arrayBlockingQueue.add("A4");
        arrayBlockingQueue.add("A5");
        arrayBlockingQueue.add("A6");
        arrayBlockingQueue.add("A7");
        arrayBlockingQueue.add("A8");
        arrayBlockingQueue.add("A9");
        arrayBlockingQueue.add("A10");



        //定义好令牌桶
        TokenLimiter limiter = new TokenLimiter(3, 1000, 3);
        // 生产令牌
        limiter.start(LOCK);
        int size = arrayBlockingQueue.size();
          //定义好门阀,等执行完毕业务代码执行完毕后再执行其它操作
        final CountDownLatch latch = new CountDownLatch(size);

        //创建线程并进行推送
        for (int i = 0; i < size; i++) {
            threadPoolTaskExecutor.execute(() -> {
                String msg = arrayBlockingQueue.poll();
                if(limiter.tryAcquire()){
                    log.info("推送信息{},当前线程名称为:{},当前时间为:{},",msg,Thread.currentThread().getName(),sdf.format(new Date()));
                    latch.countDown();
                }else {
                    boolean flag = false;
                    //重试五次,成功就推送,失败就不推送
                    for (int j = 0; j < 4; j++){
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            log.error("线程中断错误");
                        }
                        if(limiter.tryAcquire()){

                            log.info("推送信息{},当前线程名称为:{},当前时间为:{}",msg,Thread.currentThread().getName(),sdf.format(new Date()));
                            latch.countDown();
                            flag = true;
                            break;
                        }
                    }
                    if(!flag){
                        log.info("线程繁忙,无法进行业务,当前线程名称为:{},推送信息{}",Thread.currentThread().getName(),msg);
                    }
                }

            });
        }
        try {
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

         //停止令牌桶线程
        limiter.stop();



        return Result.success(1);
    }

参考文章:

java - ScheduledFuture.get() is still blocked after Executor shutdown - Stack Overflow

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
ScheduledFuture是一个接口,它代表了ScheduledExecutorService中的任务调度结果。通过ScheduledFuture,我们可以获取任务的执行结果,并对任务进行取消或获取剩余执行时间等操作。提供的例子中,我们可以看到使用ScheduledExecutorService的schedule方法来创建一个延迟执行的任务。这个方法接受一个Callable或Runnable对象作为任务,以及延迟执行的时间和时间单位。返回的ScheduledFuture对象可以用于获取任务的执行结果,通过调用get方法。在这个例子中,任务将在5秒后执行,执行完毕后返回字符串"Called!",通过调用scheduledFuture.get()获取任务的执行结果。最后,我们使用scheduledExecutorService.shutdown()来关闭线程池。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [e_ScheduledExecutorService_基本使用-20210227](https://blog.csdn.net/for_my_faith/article/details/114182288)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* [Java并发包:ScheduledExecutorService](https://blog.csdn.net/zxc123e/article/details/51911652)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

小海海不怕困难

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

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

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

打赏作者

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

抵扣说明:

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

余额充值