最近,我收到TL分配的新任务,维护之前的新应用程序。 在开发新需求的同时,难免要检查之前代码中埋下的一些坑。 前不久,在线环境服务出现了CPU高的情况。 下面我们就来看看如何分析和解决CPU高的问题。
优化流程背景
注:由于是公司线上业务,这里的业务描述和代码已经脱敏。
网上出现了服务CPU占用率过高的问题,于是小峰使用top命令定位到CPU比较高的进程ID,结合命令,导出了CPU高的进程的线程信息,并定位到问题代码(线上问题如何排查不是本文的重点,这里简单提一下,后面会专门写一篇文章来详细阐述)。
首先说一下商业背景。 本题代码是从MQ获取信息并放入队列中进行缓存。 通过单独的线程从队列中获取数据后,进行业务处理。 小风发现,这段代码使用了一个while循环,不断从队列中获取数据,判断提取的map是否为空,如果为空则进行后续的业务处理,如果为空则继续获取数据。 表面上看,似乎没有什么问题。 但小峰发现有数据就可以了。 反正业务是不断执行的,但是如果队列中没有数据,由于程序在while循环中不断执行判断,说明CPU在空闲。 那么如何解决问题呢?
本地测试时while循环未运行时的CPU利用率:
本地测试期间运行 while 循环后的 CPU 利用率:
优化思路
这段代码的问题在于,当队列中没有数据时,继续获取并执行判断,浪费了计算机的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
优化实施
1.方法中添加阻塞处理,当队列为空且获取到的map为空时阻塞。
public static class QueueData {
private static volatile LinkedBlockingQueue
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主线程中执行
切换到
由于队列没有初始化为null,所以线程被阻塞在这里。
线程的状态从WAIT改变
切换到主线程继续执行以下代码
主线程中执行线程的启动
切换到线程,队列初始化后,原来的阻塞被唤醒,线程状态从WAIT变为
至此,小风已经完成了while循环向阻塞唤醒模式的改造,大大降低了服务在进行循环判断时的CPU占用率。
总结
经过上述代码优化过程,终于解决了线程处理数据CPU高的问题。 小风修改了所有存在类似循环问题的服务。 经测试,该服务对应的CPU占用率明显下降。 小枫松了口气,终于可以下班了,心想回家一定要加一个鸡腿来弥补受损的脑细胞。 故事还将继续,他又会遇到什么样的技术挑战,敬请期待。
好了,今天的主题就讲到这里吧,不管如何,能帮到你我就很开心了,如果您觉得这篇文章写得不错,欢迎点赞和分享给身边的朋友。