//检测是否存在未对账订单
checkOrders = checkOrders();
}
好了,上述就是系统优化的背景,想必看到这里,很多小伙伴应该知道问题出在哪里了。我们继续往下看。
虽然很多小伙伴应该已经知道系统性能低下的问题所在了,这里,我们就一起详细分析下校对系统性能低下的原因。
既然运营的同事说数据校对系统越来越慢了,我们首先要做的就是找到系统的性能瓶颈所在。据了解,目前的数据对账系统,由于订单记录和库存扣减记录数据量巨大,所以查询未校对的订单信息的方法getHasNoOrders()和查询为校对的库存记录的方法getHasNoStock()相对来说比较慢。并且在数据校对系统中,校对订单和库存记录的方法是单线程执行的,我们可以简单画一个时间抽线图,如下所示。
由图可以看出,以单线程的方式getHasNoOrders()方法和getHasNoStock()方法耗费了大量的时间,这两个方法本身在逻辑上就是两个独立的方法,并且这两个方法没有先后的执行的顺序依赖。那这两个方法能不能并行执行呢?很显然是可以的。那我们把getHasNoOrders()方法和getHasNoStock()方法分别放到两个不同的线程中,优化下系统的性能,整体流程如下所示。
优化后,我们将getHasNoOrders()方法放到线程1中执行,getHasNoStock()方法放到线程2中执行,checkData()方法和saveCheckResult()方法发放到线程3中执行,优化后的系统性能相比优化前的系统性能几乎提升了一倍,优化效果相对来说还是比较明显的。
说到这里,大家应该应该知道具体怎么优化了吧?好,我们继续往下看!
解决问题的思路有了,接下来,我们看看如何使用代码实现我们上面分析的解决问题的思路。这里,我们可以分别开启两个线程执行getHasNoOrders()方法和getHasNoStock()方法,在主线程中执行checkData()方法和saveCheckResult()方法。这里需要注意的是:主线程需要等待两个子线程执行完毕之后再执行checkData()方法和saveCheckResult()方法。
为了实现这个功能,我们可以使用Thread类中join()方法,有关Thread类中join()方法的具体说明,小伙伴们可以参见《高并发之——朋友去面试竟然栽在了Thread类的源码上》一文。这里,具体的逻辑就是在主线程中调用两个子线程的join()方法实现阻塞等待,当两个子线程执行完毕退出时,调用两个子线程join()方法的主线程会被唤醒,从而执行主线程中的checkData()方法和saveCheckResult()方法。大体代码如下所示。
//检测是否存在未对账订单
checkOrders = checkOrders();
while(checkOrders != null){
Thread t1 = new Thread(()->{
//查询未校对的订单信息
hasNoOrders = getHasNoOrders();
});
t1.start();
Thread t2 = new Thread(()->{
//查询未校对的库存记录
hasNoStock = getHasNoStock();
});
t2.start();
//阻塞主线程,等待线程t1和线程t2执行完毕
t1.join();
t2.join();
//校对数据并返回结果
checkResult = checkData(hasNoOrders, hasNoStock);
//将结果信息保存到数据校对信息表中
saveCheckResult(checkResult);
//检测是否存在未对账订单
checkOrders = checkOrders();
}
至此,我们基本上能够解决问题了。但是,还有没有进一步优化的空间呢?我们进一步往下看。
通过上面对系统优化,基本能够达成我们的优化目标,但是上面的解决方案存在着不足的地方,那就是在while循环里每次都要新建两个线程分别执行getHasNoOrders()方法和getHasNoStock()方法,了解Java多线程的小伙伴们应该都知道,在Java中创建线程可是个非常耗时的操作。所以,最好是能够将创建出来的线程反复使用。这里,估计很多小伙伴都会想到使用线程池,没错,我们可以使用线程池进一步优化上面的代码。
不过在使用线程池进一步优化时,我们会遇到一个问题,就是主线程如何等待子线程中的结果数据呢?说直白点就是:主线程如何知道子线程中的getHasNoOrders()方法和getHasNoStock()方法执行完了? 由于在之前的代码中我们是在主线程中调用子线程的join()方法等待子线程执行完毕,获取到子线程执行的结果后,继续执行主线程的逻辑。但是如果使用了线程池的话,线程池中的线程根本不会退出,此时,我们无法使用线程的join()方法等待线程执行完毕。
所以,主线程如何知道子线程中的getHasNoOrders()方法和getHasNoStock()方法执行完了? 这个问题就成了关键的突破点。这里,我们使用线程池进一步优化的代码如下所示。
//检测是否存在未对账订单
checkOrders = checkOrders();
//创建线程池
Executor executor = Executors.newFixedThreadPool(2);
while(checkOrders != null){
executor.execute(()->{
//查询未校对的订单信息
hasNoOrders = getHasNoOrders();
});
executor.execute(()->{
//查询未校对的库存记录
hasNoStock = getHasNoStock();
});
/如何知道子线程中的getHasNoOrders()方法和getHasNoStock()方法执行完了成为关键/
//校对数据并返回结果
checkResult = checkData(hasNoOrders, hasNoStock);
//将结果信息保存到数据校对信息表中
saveCheckResult(checkResult);
//检测是否存在未对账订单
checkOrders = checkOrders();
}
那么,如何解决这个问题呢?我们继续往下看。
相信细心的小伙伴们能够看出,整个业务的场景就是:一个线程需要等待其他两个线程的逻辑执行完毕后再执行。在Java的并发类库中,为我们提供了一个能够在这种场景下使用的类库,那就是CountDownLatch类。如果对CountDownLatch类不了解的小伙伴可以参考《浅谈AQS中的CountDownLatch、Semaphore与CyclicBarrier》一文。
使用CountDownLatch类优化我们程序的具体做法就是:在程序的while()循环中首先创建一个CountDownLatch对象,计数器的值初始化为2。分别在hasNoOrders = getHasNoOrders();
代码和hasNoStock = getHasNoStock();
代码的后面调用latch.countDown()
方法使得计数器的值分别减1。在主线程中调用latch.await()
方法,等待计数器的值变为0,继续往下执行。这样,就能够完美解决我们遇到的问题了。优化后的代码如下所示。
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
那么如何才能正确的掌握Redis呢?
为了让大家能够在Redis上能够加深,所以这次给大家准备了一些Redis的学习资料,还有一些大厂的面试题,包括以下这些面试题
-
并发编程面试题汇总
-
JVM面试题汇总
-
Netty常被问到的那些面试题汇总
-
Tomcat面试题整理汇总
-
Mysql面试题汇总
-
Spring源码深度解析
-
Mybatis常见面试题汇总
-
Nginx那些面试题汇总
-
Zookeeper面试题汇总
-
RabbitMQ常见面试题汇总
JVM常频面试:
Mysql面试题汇总(一)
Mysql面试题汇总(二)
Redis常见面试题汇总(300+题)
r面试题汇总
- RabbitMQ常见面试题汇总
JVM常频面试:
[外链图片转存中…(img-CArGapFr-1711861361904)]
Mysql面试题汇总(一)
[外链图片转存中…(img-Q1AJPQ4e-1711861361904)]
Mysql面试题汇总(二)
[外链图片转存中…(img-DdITQbSS-1711861361905)]
Redis常见面试题汇总(300+题)
[外链图片转存中…(img-YOOzOj0N-1711861361905)]