最近,我收到TL分配的新任务,维护之前的新应用程序。 在开发新需求的同时,难免要检查之前代码中埋下的一些坑。 前不久,在线环境服务出现了CPU高的情况。 下面我们就来看看如何分析和解决CPU高的问题。

优化流程背景

注:由于是公司线上业务,这里的业务描述和代码已经脱敏。

网上出现了服务CPU占用率过高的问题,于是小峰使用top命令定位到CPU比较高的进程ID,结合命令,导出了CPU高的进程的线程信息,并定位到问题代码(线上问题如何排查不是本文的重点,这里简单提一下,后面会专门写一篇文章来详细阐述)。

首先说一下商业背景。 本题代码是从MQ获取信息并放入队列中进行缓存。 通过单独的线程从队列中获取数据后,进行业务处理。 小风发现,这段代码使用了一个while循环,不断从队列中获取数据,判断提取的map是否为空,如果为空则进行后续的业务处理,如果为空则继续获取数据。 表面上看,似乎没有什么问题。 但小峰发现有数据就可以了。 反正业务是不断执行的,但是如果队列中没有数据,由于程序在while循环中不断执行判断,说明CPU在空闲。 那么如何解决问题呢?

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

win10cpu占用率高_cpu占用率高_system磁盘占用率高

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

cpu占用率高_system磁盘占用率高_win10cpu占用率高

优化思路

这段代码的问题在于,当队列中没有数据时,继续获取并执行判断,浪费了计算机的CPU资源。 这时,小枫灵光一闪。 他前段时间不是看过源码吗? 里面的take方法是在队列没有数据的时候阻塞,避免不断循环判断。 当队列中有数据时,会在阻塞之前被唤醒。 该线程执行后续的数据采集。 那么这里,我们是否可以借鉴take方法的思想,使用阻塞-唤醒的方式来解决while循环空转的问题呢? 想到这里,小枫有些兴奋,仿佛看到了曙光,她立即搓着手,准备开始编码测试。

优化实施

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


public static class TakeDataThread extends Thread {
    @Override
    public void run() {
        //循环获取数据
        while(true) {
            Map 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 recordInfoQueue;
    public static Map getRecordList(int size, Long timeout) {
        if(Objects.isNull(recordInfoQueue)) {
            return Collections.emptyMap();
        }
        return recordInfoQueue.poll();
}
}

优化实施

1.方法中添加阻塞处理,当队列为空且获取到的map为空时阻塞。


public static class QueueData {
    
    private static volatile LinkedBlockingQueue recordInfoQueue;
    private final static ReentrantLock handleLock = new ReentrantLock();
    private final static Condition notEmpty = handleLock.newCondition();
    ...
    public static Map getRecordList(int size, Long timeout) {
        Map 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主线程中执行

cpu占用率高_system磁盘占用率高_win10cpu占用率高

切换到

system磁盘占用率高_win10cpu占用率高_cpu占用率高

由于队列没有初始化为null,所以线程被阻塞在这里。

线程的状态从WAIT改变

cpu占用率高_win10cpu占用率高_system磁盘占用率高

切换到主线程继续执行以下代码

win10cpu占用率高_system磁盘占用率高_cpu占用率高

主线程中执行线程的启动

win10cpu占用率高_system磁盘占用率高_cpu占用率高

切换到线程,队列初始化后,原来的阻塞被唤醒,线程状态从WAIT变为

至此,小风已经完成了while循环向阻塞唤醒模式的改造,大大降低了服务在进行循环判断时的CPU占用率。

总结

经过上述代码优化过程,终于解决了线程处理数据CPU高的问题。 小风修改了所有存在类似循环问题的服务。 经测试,该服务对应的CPU占用率明显下降。 小枫松了口气,终于可以下班了,心想回家一定要加一个鸡腿来弥补受损的脑细胞。 故事还将继续,他又会遇到什么样的技术挑战,敬请期待。

好了,今天的主题就讲到这里吧,不管如何,能帮到你我就很开心了,如果您觉得这篇文章写得不错,欢迎点赞和分享给身边的朋友。

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注