程序猿小枫的故事:while循环导致的CPU暴涨问题优化实践

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

引言

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

优化过程

背景

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

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


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

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

image.png

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

image.png

优化思路

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

优化实现

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

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为空时,进行阻塞。

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、在进行队列初始化以及网队列中缓存数据的时候进行线程唤醒。

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();
    }
}
...
}

调试代码

本地进行代码调试:

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的启动

image.png

切换到 TakeDataThread

image.png


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

image.png

TakeDataThread线程的状态由RUNNING转为WAIT


image.png

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

image.png

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

image.png

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

image.png

至此,小枫完成了将while循环转化为阻塞唤醒的模式,大大降低了服务在进行循环判断时候的CPU使用率。

总结

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


相关文章
|
2月前
|
编译器 Linux C语言
C++新特性“CPU优化对齐”
C++新特性“CPU优化对齐”
|
2月前
|
存储 机器学习/深度学习 测试技术
mnn-llm: 大语言模型端侧CPU推理优化
mnn-llm: 大语言模型端侧CPU推理优化
382 1
|
2月前
|
存储 缓存 算法
如何优化 CPU 通道的使用
如何优化 CPU 通道的使用
44 0
|
4月前
|
存储 人工智能 缓存
探索AIGC未来:CPU源码优化、多GPU编程与中国算力瓶颈与发展
近年来,AIGC的技术取得了长足的进步,其中最为重要的技术之一是基于源代码的CPU调优,可以有效地提高人工智能模型的训练速度和效率,从而加快了人工智能的应用进程。同时,多GPU编程技术也在不断发展,大大提高人工智能模型的计算能力,更好地满足实际应用的需求。 本文将分析AIGC的最新进展,深入探讨以上话题,以及中国算力产业的瓶颈和趋势。
|
5月前
|
存储 缓存 Linux
高效利用CPU缓存一致性:优化技巧与策略分析
高效利用CPU缓存一致性:优化技巧与策略分析
|
8月前
|
SQL 存储 关系型数据库
记一次MySQL CPU被打满的SQL优化案例分析
记一次MySQL CPU被打满的SQL优化案例分析
171 0
|
10月前
|
NoSQL 安全 Linux
Redis 从入门到精通之内存和CPU配置优化
Redis 是一种基于内存的数据存储系统,因此内存的规划是非常重要的。在配置 Redis 内存时,应该避免物理内存使用过量导致大量使用 Swap,同时需要考虑内存碎片的问题。根据多年的经验整理了一些建议
541 1
|
SQL 缓存 负载均衡
线上cpu报警的一次接口优化
春天到了大地都复苏了,沉寂了很久的cpu也开始慢慢复苏了,所谓前人埋坑后人填坑,伴随着阿里云监控报警,线上CPU使用率暴增,于是就开始了排查之路。
|
SQL API 容器
MogDB or openGauss关于CPU占用问题的优化
MogDB or openGauss关于CPU占用问题的优化
193 0
|
存储 缓存 Java
【优化技术专题】「底层架构原理系列」CPU处理器鲜为人知的那些秘密
【优化技术专题】「底层架构原理系列」CPU处理器鲜为人知的那些秘密
336 0
【优化技术专题】「底层架构原理系列」CPU处理器鲜为人知的那些秘密

相关实验场景

更多