为了账号安全,请及时绑定邮箱和手机立即绑定
首页 手记 Java性能调优

Java性能调优

2018.10.23 16:11 812浏览

一个用Java写的GUI程序,作用是分析日志, 它会将一定数量的格式相同的文本日志文件读入内存分析处理,然后将结果合并输出。

文件数量几十个,文件大小几KB, 日志记录几千条左右, 此工具可以流畅处理, 轻松满足需求。

然而, 因为记录日志的方案调整,记录日志类型范围从warn、error级别扩大到了连info、debug级别的日志也要记录,从而导致了日志量激增, 固定时间范围内产生的日志文件增加到了几百个, 单个大小也增加到几M,日志记录一下子达到几十万条,然后工具就跑的跟蜗牛一样慢, 几十秒才能出结果。

要继续使用这个工具,自然需要优化,要优化则需要定位性能瓶颈在哪里。 这个工具不复杂,没有复杂的计算, 也没有无谓的运行损耗, 最可能导致问题的地方是文件内容读取的I/O开销,可是以Java的能耐,从几百个文件中读取几百M内容, 没有可能需要几十秒时间。 而且为了提升速度我还使用了多线程处理,使用线程池为每个日志文件的读取和处理分配一个线程,于是我以此处为中心开始分析问题所在,直觉上, 线程的使用总是容易和性能问题联系在一起。

因为使用

Executors.newCachedThreadPool()


创建我所使用的线程池,而使用这个线程池太过于潦草, 它没有大小限制,为每个日志文件的处理分配一个线程,假如有100个文件需要处理, 那么线程池就会创建100个线程, 而我的机器CPU只有4核心,显然这100个线程无法被同时执行,而且还增加了线程之间切换的开销,所以怀疑这是导致性能问题的原因之一。

从相关网络资料中得知,最佳线程数目可通过以下公式确定

最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目

显然,公式中所有变量除了CPU数目, 其它一概不知,所以我采用了另外一个简单粗暴的公式

线程数据  = 2 * CPU核心数 + 1

这是I/O密集型程序的计算公式。因为这个工具需要频繁读取磁盘文件,所以我将他定位为I/O密集型

ExecutorServiceexecutorService=Executors.newFixedThreadPool(9);


通过使用有数量限制的线程池对程序进行改进后,运行速度似乎有所改善, 可是对于让程序以可以接受的速度运行而言却是杯水车薪。当然,我本来也对此改进没有抱多大希望,因为上百个线程的切换不可能要花几十秒。

我知道程序存在问题, 可对于一个没有问题的程序在正常情况下处理这些日志文件需要花多少时间没有一个明确的概念,所以并没有充足的信心确定程序出问题的模块所在,只能没有证据的猜测, 因此不能迅速的定位问题所在。

可能是直觉的将性能与并发和线程挂钩,因此接下来的优化仍旧以这个主题进行。

我决定将日志文件的读取与处理分开,读取文件使用一个线程, 日志数据处理使用一个线程, 读取文件的线程将读取的内容存入一个阻塞队列,数据处理响线程从阻塞队列中读取数据进行处理,这是典型的生产消费模式。

费了很大力气实现这个模式,信心满满的以为程序的运行速度会有所改善,然而,出乎意料的程序运行的速度非但没有改善,而且比以前更加缓慢了,并且伴随卡死的现象, 我很沮丧, 可不得不承认,性能瓶颈不在并发处理上,朝这个方向优化永远不能解决问题,高大上的并发技能在这里毫无用武之地。

我意识必须不断缩小问题范围,精确定位到问题代码所在,才能啃掉这块骨头。

我虽然怀疑性能瓶颈在文件读取上,可并没有明确证据, 必须需要一个明确的数据指标才行。

我在程序的关键部位加上调试代码,计算执行各个环节所需要的时间,从数据中明确得知,大部分时间的确被文件读取占用了。

我定位到最接近文件的代码,一个从其它项目中拷贝过来的方法

publicstaticStringreadStringFromFile(Filefile){

try{

if(file.isFile()&&file.exists()){

InputStreamReaderread=newInputStreamReader(newFileInputStream(file),"UTF-8");

BufferedReaderbufferedReader=newBufferedReader(read);

StringlineTxt=bufferedReader.readLine();

Stringresult="";

while(lineTxt!=null){

result+=lineTxt;

lineTxt=bufferedReader.readLine();

}

returnresult;

}

}catch(IOExceptione){

e.printStackTrace();

}

returnnull;

}


这个方法似乎没什么毛病, 还使用 BufferedReader 来提升性能,我百思不得其解,并开始推测问题所在。

程序是逐行读去内容的,问题会不会出现在这里?

如果一次性读取整个文件的内容速度会不会快一点?

正思考间,目光被一行代码吸引

result+=lineTxt;


然后灵光一现,一个编程概念再我脑海中闪现:String类型是线程安全的,状态不可变, 每次操作都会复制一个自身的副本,因此性能很低,在频繁拼接字符串的情况下, 应该以StringBuffer或者StringBuilder代替String。

几百个日志文件, 几十万条日志记录,即使一行一条记录, Java虚拟机也执行了几十万次的字符串的拷贝,速度慢是必然的。

确定问题所在,改进就很容易了,我用StringBuilder类替代String进行字符串处理

publicstaticStringreadStringFromFile(Filefile){

try{

if(file.isFile()&&file.exists()){

InputStreamReaderread=newInputStreamReader(newFileInputStream(file),"UTF-8");

BufferedReaderbufferedReader=newBufferedReader(read);

StringlineTxt=bufferedReader.readLine();

StringBuilderresult=newStringBuilder();

while(lineTxt!=null){

result.append(lineTxt);

lineTxt=bufferedReader.readLine();

}

returnresult.toString();

}

}catch(IOExceptione){

e.printStackTrace();

}

returnnull;

}



作者:Java互联网架构师
链接:https://www.jianshu.com/p/5c36a3d74bd3


点击查看更多内容
0人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
全栈工程师
手记
粉丝
224
获赞与收藏
986

关注TA,一起探索更多经验知识

同主题相似文章浏览排行榜

风间影月说签约讲师

50篇手记,涉及Java、MySQL、Redis、Spring等方向

进入讨论

Tony Bai 说签约讲师

145篇手记,涉及Go、C、Java、Python等方向

进入讨论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消