前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >while循环导致的CPU暴涨问题优化实践

while循环导致的CPU暴涨问题优化实践

作者头像
慕枫技术笔记
发布2023-03-20 11:55:26
6800
发布2023-03-20 11:55:26
举报
文章被收录于专栏:慕枫技术笔记慕枫技术笔记

引言

程序猿小K最近接到TL分配的新任务,维护一个之前的新应用,在开发新需求的同时,不免也需要排查一些前人代码中埋下的坑。这不最近就出现了线上环境服务CPU较高的情况,让我们一起来围观下程序猿小枫是怎么对CPU过高问题进行分析以及解决的。

优化过程

背景

线上出现服务CPU占用过高的问题,于是小枫使用top命令定位到CPU比较高的进程ID,再结合jstack命令,导出CPU高的进程的线程信息,定位到问题代码(如何进行线上问题排查不是本文的重点,这里一笔带过,后面再写专门的文章来进行重点阐述)。

首先说一下业务背景,这段问题代码是从MQ中获取信息并放在队列中进行缓存,在通过单独的线程从队列中获取到数据进行后的业务处理。小枫发现,这段代码中使用了while循环不断从队列中获取数据,判断取出来的map是否为空,不为空进行后面的业务处理,为空的话就继续获取数据。表面上看似乎没有什么问题。但是小枫发现有数据的时候还好,反正就是不断执行业务,但是如果队列中没有数据的话,由于在while循环中,程序依据在不断执行判断,有点CPU空转的意思了。那么该怎么解决问题呢?

本地测试时未运行while循环时的CPU利用率:

优化思路

这段代码的问题就在于队列中没有数据的时候还是不断获取并执行判断,浪费了计算机的CPU资源。这个时候小枫灵光一现,前段时间不是看过LinkedBlockingQueue的源码嘛,其中的take方法实现的是在队列中没有数据的时候进行阻塞,避免一直循环判断,当队列中有数据的时候再唤醒之前阻塞的线程进行后续的数据获取。那么在此处我们可不可以借助于take方法的思想,使用阻塞-唤醒的方式来解决这个while循环空转的问题呢?一想到这里,小枫有些激动,仿佛看到了曙光,立马搓了搓自己的双手,准备开始编码测试。

原先的while循环代码如下所示:

说明:由于是公司线上业务,这里的业务说明以及代码都进行了脱敏处理。

代码语言:javascript
复制
public static class TakeDataThread extends Thread {

    @Override
    public void run() {
        //循环获取数据
        while(true) {
            Map<String, String> map = QueueData.getRecordList(1,2L);
            //如果map一直为空,则一直获取判断,造成CPU空转
            if (CollectionUtils.isEmpty(map)) {
                System.out.println("continue");
                continue;
            }
            System.out.println("next step");
        }
    }
}


public static class QueueData {

private static volatile LinkedBlockingQueue<Map> recordInfoQueue;
    public static Map<String, String> getRecordList(int size, Long timeout) {
        if(Objects.isNull(recordInfoQueue)) {
            return Collections.emptyMap();
        }
        return recordInfoQueue.poll();
}


}

优化实现

1、在getRecordList方法中增加阻塞处理,当队列为空以及获取的map为空时,进行阻塞。

代码语言:javascript
复制
public static class QueueData {
    
    private static volatile LinkedBlockingQueue<Map> recordInfoQueue;

    private final static ReentrantLock handleLock = new ReentrantLock();

    private final static Condition notEmpty = handleLock.newCondition();

    ...

    public static Map<String, String> getRecordList(int size, Long timeout) {
        Map<String, String> map = null;
        try {
            handleLock.lockInterruptibly();
            //队列为空进行阻塞
            while (recordInfoQueue == null || CollectionUtils.isEmpty(recordInfoQueue.poll())) {
                notEmpty.await();
            }

        }catch (InterruptedException e) {
            e.printStackTrace();
        }
        finally {
            handleLock.unlock();
        }
        return map;
    }
    
    ...
    
 }

2、在进行队列初始化以及网队列中缓存数据的时候进行线程唤醒。

代码语言:javascript
复制
public static class QueueData {
    
    ...
    
    public static void putRecord(Map alarmVo) throws InterruptedException {
        if (recordInfoQueue == null) {
            synchronized (QueueData.class) {
                if(recordInfoQueue == null){
                    recordInfoQueue = new LinkedBlockingQueue(10000);
                }
            }
        }
        recordInfoQueue.put(alarmVo);
        //队列创建以及缓存数据的时候,唤醒线程
        signalNotEmpty();
}


private static void signalNotEmpty() {
    final ReentrantLock takeLock = QueueData.handleLock;
    takeLock.lock();
    try {
        notEmpty.signal();
    } finally {
        takeLock.unlock();
    }
}

...

}

调试代码

本地进行代码调试:

代码语言:javascript
复制
public class TestMain {


public static void main(String[] args) throws IOException, InterruptedException {

    System.out.println("--------takeDataThread start------");
    TakeDataThread takeDataThread = new TakeDataThread();
    takeDataThread.start();
    System.out.println("--------takeDataThread end------");

    System.out.println("--------dataNotifyThread start------");
    DataNotifyThread  dataNotifyThread = new DataNotifyThread(0);
    dataNotifyThread.start();
    System.out.println("--------dataNotifyThread end------");

    System.out.println("--------dataNotifyThread2 start------");
    DataNotifyThread  dataNotifyThread2 = new DataNotifyThread(1);
    dataNotifyThread2.start();
    System.out.println("--------dataNotifyThread2 end------");

}
...

}

在main主线程中执行TakeDataThread的启动

切换到 TakeDataThread

由于队列没有进行初始化为null,所以此处线程进行阻塞处理。

TakeDataThread线程的状态由RUNNING转为WAIT

切换到主线程继续往下执行后面的代码

主线程中执行DataNotifyThread线程的启动

切换到DataNotifyThread线程,初始化队列后,原先阻塞的TakeDataThread被唤醒,线程状态由WAIT转变为RUNNING。

总结

经过了上述的代码优化过程,程序猿小枫终于解决了处理数据的线程CPU过高的问题,小枫将服务中存在类似循环问题的都进行了修改,经过测试服务对应的CPU使用率有了明显的下降,小枫松了口气,终于可以下班了,想着回家一定给自己加个鸡腿补一补伤掉的脑细胞。程序猿小枫的故事还会继续,他还会遇到怎样的技术挑战,请大家敬请期待。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-10-31,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 慕枫技术笔记 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 优化过程
    • 优化实现
      • 调试代码
      • 总结
      相关产品与服务
      测试服务
      测试服务 WeTest 包括标准兼容测试、专家兼容测试、手游安全测试、远程调试等多款产品,服务于海量腾讯精品游戏,涵盖兼容测试、压力测试、性能测试、安全测试、远程调试等多个方向,立体化安全防护体系,保卫您的信息安全。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档