人人都能掌握的Java服務端效能優化方案

HollisChuang發表於2019-02-28

作為一個Java後端開發,我們寫出的大部分程式碼都決定著使用者的使用體驗。如果我們的後端程式碼效能不好,那麼使用者在訪問我們的網站時就要浪費一些時間等待伺服器的響應。這就可能導致使用者投訴甚至使用者的流失。

關於效能優化是一個很大的話題。《Java程式效能優化》說效能優化包含五個層次:設計調優、程式碼調優、JVM調優、資料庫調優、作業系統調優等。而每一個層次又包含很多方法論和最佳實踐。本文不想大而廣的概述這些內容。只是舉幾個常用的Java程式碼優化方案,讀者看完之後可以真正的實踐到自己程式碼中的方案。

使用單例

對於IO處理、資料庫連線、配置檔案解析載入等一些非常耗費系統資源的操作,我們必須對這些例項的建立進行限制,或者是始終使用一個公用的例項,以節約系統開銷,這種情況下就需要用到單例模式。

單例模式有很多種語法,我的公眾號也推送過多篇和單例相關的文章

使用Future模式

假設一個任務執行起來需要花費一些時間,為了省去不必要的等待時間,可以先獲取一個“提貨單”,即Future,然後繼續處理別的任務,直到“貨物”到達,即任務執行完得到結果,此時便可以用“提貨單”進行提貨,即通過Future物件得到返回值。

public class RealData implements Callable<String> {  
    protected String data;  

    public RealData(String data) {  
        this.data = data;  
    }  

    @Override  
    public String call() throws Exception {  
        //利用sleep方法來表示真是業務是非常緩慢的  
        try {  
            Thread.sleep(1000);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
        return data;  
    }  
}  

public class Application {  
    public static void main(String[] args) throws Exception {  
        FutureTask<String> futureTask =   
                new FutureTask<String>(new RealData("name"));  
        ExecutorService executor =   
                Executors.newFixedThreadPool(1); //使用執行緒池  
        //執行FutureTask,相當於上例中的client.request("name")傳送請求  
        executor.submit(futureTask);  
        //這裡可以用一個sleep代替對其他業務邏輯的處理  
        //在處理這些業務邏輯過程中,RealData也正在建立,從而充分了利用等待時間  
        Thread.sleep(2000);  
        //使用真實資料  
        //如果call()沒有執行完成依然會等待  
        System.out.println("資料=" + futureTask.get());  
    }  
}  
複製程式碼

使用執行緒池

合理利用執行緒池能夠帶來三個好處。第一:降低資源消耗。通過重複利用已建立的執行緒降低執行緒建立和銷燬造成的消耗。第二:提高響應速度。當任務到達時,任務可以不需要等到執行緒建立就能立即執行。第三:提高執行緒的可管理性。執行緒是稀缺資源,如果無限制的建立,不僅會消耗系統資源,還會降低系統的穩定性,使用執行緒池可以進行統一的分配,調優和監控。

在 Java 5 之後,併發程式設計引入了一堆新的啟動、排程和管理執行緒的API。Executor 框架便是 Java 5 中引入的,其內部使用了執行緒池機制,它在 java.util.cocurrent 包下,通過該框架來控制執行緒的啟動、執行和關閉,可以簡化併發程式設計的操作。

public class MultiThreadTest {
    public static void main(String[] args) {
        ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("thread-%d").build();
        ExecutorService executor = new ThreadPoolExecutor(2, 5, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), threadFactory);
        executor.execute(new Runnable() {
            @Override
            public void run() {
               System.out.println("hello world !");
            }
        });
        System.out.println(" ===> main Thread! " );
    }
}
複製程式碼

使用NIO

JDK自1.4起開始提供全新的I/O程式設計類庫,簡稱NIO,其不但引入了全新高效的Buffer和Channel,同時,還引入了基於Selector的非阻塞 I/O機制,將多個非同步的I/O操作集中到一個或幾個執行緒當中進行處理,使用NIO代替阻塞I/O能提高程式的併發吞吐能力,降低系統的開銷。

對於每一個請求,如果單獨開一個執行緒進行相應的邏輯處理,當客戶端的資料傳遞並不是一直進行,而是斷斷續續的,則相應的執行緒需要 I/O等待,並進行上下文切換。而使用NIO引入的Selector機制後,可以提升程式的併發效率,改善這一狀況。

public class NioTest {  
    static public void main( String args[] ) throws Exception {  
        FileInputStream fin = new FileInputStream("c:\\test.txt");  
        // 獲取通道  
        FileChannel fc = fin.getChannel();  
        // 建立緩衝區  
        ByteBuffer buffer = ByteBuffer.allocate(1024);  
        // 讀取資料到緩衝區  
        fc.read(buffer);  
        buffer.flip();  
        while (buffer.remaining()>0) {  
            byte b = buffer.get();  
            System.out.print(((char)b));  
        }  
        fin.close();  
    }  
}  
複製程式碼

鎖優化

在併發場景中,我們的程式碼中經常會用到鎖。存在鎖,就必然存在鎖的競爭,存在鎖的競爭,就會消耗很多資源。那麼,如何優化我們Java程式碼中的鎖呢?主要可以從以下幾個方面考慮:

  • 減少鎖持有時間
    • 可以使用同步程式碼塊來代替同步方法。這樣既可以減少鎖持有的時間。
  • 減少鎖粒度
    • 要在併發場景中使用Map的時候,記得使用ConcurrentHashMap來代替HashTable和HashMap。
  • 鎖分離
    • 普通鎖(如syncronized)會導致讀阻塞寫、寫也會阻塞讀,同時讀讀與寫寫之間也會進行阻塞,可以想辦法將讀操作和寫操作分離開。
  • 鎖粗化
    • 有些情況下我們希望把很多次鎖的請求合併成一個請求,以降低短時間內大量鎖請求、同步、釋放帶來的效能損耗。
  • 鎖消除
    • 鎖消除是Java虛擬機器在JIT編譯是,通過對執行上下文的掃描,去除不可能存在共享資源競爭的鎖,通過鎖消除,可以節省毫無意義的請求鎖時間。

關於鎖優化的內容,後面會出一篇文章詳細介紹。

壓縮傳輸

在進行資料傳輸之前,可以先將資料進行壓縮,以減少網路傳輸的位元組數,提升資料傳輸的速度,接收端可以將資料進行解壓,以還原出傳遞的資料,並且,經過壓縮的資料還可以節約所耗費的儲存介質(磁碟或記憶體)的空間以及網路頻寬,降低成本。當然,壓縮也並不是沒有開銷的,資料壓縮需要大量的CPU計算,並且,根據壓縮演算法的不同,計算的複雜度以及資料的壓縮比也存在較大差異。一般情況下,需要根據不同的業務場景,選擇不同的壓縮演算法。

快取結果

對於相同的使用者請求,如果每次都重複的查詢資料庫,重複的進行計算,將浪費很多的時間和資源。將計算後的結果快取到本地記憶體,或者是通過分散式快取來進行結果的快取,可以節約寶貴的CPU計算資源,減少重複的資料庫查詢或者是磁碟I/O,將原本磁頭的物理轉動變成記憶體的電子運動,提高響應速度,並且執行緒的迅速釋放也使得應用的吞吐能力得到提升。

參考

Java多執行緒程式設計中Future模式的詳解 java鎖優化的方法與思路

人人都能掌握的Java服務端效能優化方案

相關文章