引言
程序猿小枫最近接到TL分配的新任务,维护一个之前的新应用,在开发新需求的同时,不免也需要排查一些前人代码中埋下的坑。这不最近就出现了线上环境服务CPU较高的情况,让我们一起来围观下程序猿小枫是怎么对CPU过高问题进行分析以及解决的。
优化过程
背景
说明:由于是公司线上业务,这里的业务说明以及代码都进行了脱敏处理。
线上出现服务CPU占用过高的问题,于是小枫使用top命令定位到CPU比较高的进程ID,再结合jstack命令,导出CPU高的进程的线程信息,定位到问题代码(如何进行线上问题排查不是本文的重点,这里一笔带过,后面再写专门的文章来进行重点阐述)。
首先说一下业务背景,这段问题代码是从MQ中获取信息并放在队列中进行缓存,在通过单独的线程从队列中获取到数据进行后的业务处理。小枫发现,这段代码中使用了while循环不断从队列中获取数据,判断取出来的map是否为空,不为空进行后面的业务处理,为空的话就继续获取数据。表面上看似乎没有什么问题。但是小枫发现有数据的时候还好,反正就是不断执行业务,但是如果队列中没有数据的话,由于在while循环中,程序依据在不断执行判断,有点CPU空转的意思了。那么该怎么解决问题呢?
本地测试时未运行while循环时的CPU利用率:
本地测试时运行while循环后的CPU利用率:
优化思路
这段代码的问题就在于队列中没有数据的时候还是不断获取并执行判断,浪费了计算机的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的启动
切换到 TakeDataThread
由于队列没有进行初始化为null,所以此处线程进行阻塞处理。
TakeDataThread线程的状态由RUNNING转为WAIT
切换到主线程继续往下执行后面的代码
主线程中执行DataNotifyThread线程的启动
切换到DataNotifyThread线程,初始化队列后,原先阻塞的TakeDataThread被唤醒,线程状态由WAIT转变为RUNNING
至此,小枫完成了将while循环转化为阻塞唤醒的模式,大大降低了服务在进行循环判断时候的CPU使用率。
总结
经过了上述的代码优化过程,程序猿小枫终于解决了处理数据的线程CPU过高的问题,小枫将服务中存在类似循环问题的都进行了修改,经过测试服务对应的CPU使用率有了明显的下降,小枫松了口气,终于可以下班了,想着回家一定给自己加个鸡腿补一补伤掉的脑细胞。程序猿小枫的故事还会继续,他还会遇到怎样的技术挑战,请大家敬请期待。