程序猿小枫的故事: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使用率有了明显的下降,小枫松了口气,终于可以下班了,想着回家一定给自己加个鸡腿补一补伤掉的脑细胞。程序猿小枫的故事还会继续,他还会遇到怎样的技术挑战,请大家敬请期待。


相关文章
|
7月前
|
编译器 Linux C语言
C++新特性“CPU优化对齐”
C++新特性“CPU优化对齐”
138 3
|
29天前
|
存储 缓存 监控
Docker容器性能调优的关键技巧,涵盖CPU、内存、网络及磁盘I/O的优化策略,结合实战案例,旨在帮助读者有效提升Docker容器的性能与稳定性。
本文介绍了Docker容器性能调优的关键技巧,涵盖CPU、内存、网络及磁盘I/O的优化策略,结合实战案例,旨在帮助读者有效提升Docker容器的性能与稳定性。
83 7
|
4月前
|
SQL 监控 关系型数据库
MySQL优化: CPU高 处理脚本 pt-kill脚本
MySQL优化: CPU高 处理脚本 pt-kill脚本
|
2月前
|
存储 缓存 算法
CPU优化
【10月更文挑战第7天】
43 1
|
4月前
|
监控 Java Linux
CPU被打满/CPU 100%:高效诊断与优化策略
【8月更文挑战第28天】在日常的工作与学习中,遇到CPU使用率飙升至100%的情况时,往往意味着系统性能受到严重影响,甚至可能导致程序响应缓慢或系统崩溃。本文将围绕这一主题,分享一系列高效诊断与优化CPU使用的技术干货,帮助大家快速定位问题并恢复系统性能。
323 1
|
5月前
|
Java
手把手教你java CPU飙升300%如何优化
手把手教你java CPU飙升300%如何优化
68 0
|
5月前
|
监控 Java 中间件
FGC频繁导致CPU 飙升定位及JVM配置优化总结
FGC频繁导致CPU 飙升定位及JVM配置优化总结
200 0
|
5月前
|
缓存 弹性计算 监控
云服务器 CPU 使用率高的问题排查与优化
云服务器 CPU 使用率高的问题排查与优化
460 0
|
6月前
|
SQL 数据处理 API
实时计算 Flink版产品使用问题之holo的io以及cpu使用较为稳定,sink端busy一直在20%左右,有时候50%,该如何优化
实时计算Flink版作为一种强大的流处理和批处理统一的计算框架,广泛应用于各种需要实时数据处理和分析的场景。实时计算Flink版通常结合SQL接口、DataStream API、以及与上下游数据源和存储系统的丰富连接器,提供了一套全面的解决方案,以应对各种实时计算需求。其低延迟、高吞吐、容错性强的特点,使其成为众多企业和组织实时数据处理首选的技术平台。以下是实时计算Flink版的一些典型使用合集。
|
7月前
|
存储 机器学习/深度学习 测试技术
mnn-llm: 大语言模型端侧CPU推理优化
mnn-llm: 大语言模型端侧CPU推理优化
1056 1