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


相关文章
涨姿势了!原来这才是多线程正确实现方式
线程同步机制是一套适用于协调线程之间的数据访问机制,该机制可以保障线程安全 java平台提供的线程同步机制包括:锁、volatile关键字、final关键字,static关键字、以及相关API如object.wait/object.notify
|
SQL XML 前端开发
别再学了!这些技术已经被淘汰了,少走点弯路。。。
别再学了!这些技术已经被淘汰了,少走点弯路。。。
|
NoSQL Java 程序员
要学的东西太多,自己能力不足,很焦虑怎么办
总有人问我,兔哥,现在java要学的知识点这么多,记不住,怕学不精很焦虑怎么办? 这是很多初学者都有的痛点。 其实吧,你可以试试贪多而不必嚼烂。
195 0
|
SQL 存储 监控
程序员新人频繁使用count(*),被组长批评后怒怼:性能并不拉垮!
程序员新人频繁使用count(*),被组长批评后怒怼:性能并不拉垮!
|
存储 消息中间件 Linux
看完这篇文章,我再也不用担心线上出现 CPU 性能问题了(上)
生产环境上出现 CPU 性能问题是非常典型的一类问题,往往这个时候就比较考验相关人员排查问题的能力
|
IDE Linux 调度
看完这篇文章,我再也不用担心线上出现 CPU 性能问题了(下)
在上一篇文章中咸鱼给大家介绍了 CPU 常见的性能指标,当生产环境出现 CPU 性能瓶颈的时候,优先观察这些指标有没有什么异常的地方,能解决大部分情况
|
移动开发 小程序 JavaScript
在自学编程这条道上,有人半途而废,有人效率暴增
在自学编程这条道上,有人半途而废,有人效率暴增
140 0
|
安全 程序员 虚拟化
【Windows核心编程+第一个内核程序】爆肝120小时整理-80%程序员最欠缺的能力,一半以上研究生毕业了还不懂?理解各种深度技术的基本功
【Windows核心编程+第一个内核程序】爆肝120小时整理-80%程序员最欠缺的能力,一半以上研究生毕业了还不懂?理解各种深度技术的基本功
132 0
【Windows核心编程+第一个内核程序】爆肝120小时整理-80%程序员最欠缺的能力,一半以上研究生毕业了还不懂?理解各种深度技术的基本功
|
程序员
能让程序员瞬间崩溃的五个瞬间,共鸣的同学请举手!
在我们的眼里,程序员好像是无所不能的,那么复杂的App和那些游戏都是他们做出来的,这让我们很难相信还有什么是他做不出来的。不过,就是我们每天眼里看着很厉害的程序员,每天都要面临的就是头疼,头疼,头好疼,特别是我接下来要说的几件事情,几乎是所有程序员都会把头抓秃的事     那么这五件事情究竟是什么事呢? 写着代码停电,代码没有保存 如果有一天突然代码写到一半,眼看就快要完工了,突然一下就断电,代码没保存。
1350 0
不起眼,但是足以让你收获的JVM内存案例
不起眼,但是足以让你收获的JVM内存案例
不起眼,但是足以让你收获的JVM内存案例