1. 程式人生 > >Java性能調優

Java性能調優

占用 read 文件的 gre urn 同時 隊列 頻繁 tostring

  

技術分享圖片

  一個用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;

  }

  程序運行速度提升驚人,之前幾十秒才能出的結果,現在不到一秒就完成,問題迎刃而解。 至於為什麽不用StringBuffer是因為StringBuffer是StringBuffer的並發安全版本,性能略微不及StringBuider,而這裏不存在並發處理問題,因此自然選著性能更優的StringBuilder。

  只改三行代碼就能解決的問題卻令我折騰了半天, 究其原因在於以下原因

  ①:思路先入為主, 總是把性能問題和並發掛上鉤,利用多線程處理優化

  ②:憑直覺判斷問題形成的原因,沒有確切的依據, 致使優化方向錯誤

  很多情況下, 程序性能問題跟並發處理無關, 當一個程序運行緩慢時, 並不會因為多加幾個線程處理就會變的很快,真正引起問題的原因往往出人意料,而且極有可能是非常低級的問題, 排除掉這些問題所在,保證代碼健康,如果性能問題依舊存在, 再使用並發手段去解決不遲。

  再者,優化程序性能必須確定造成性能問題的代碼塊所在,不能評直覺判斷, 否者不但解決不了問題,還浪費時間。

?

Java性能調優