併發編程學習(11)-----性能

思維導圖:

引言:

    使用多線程最主要的目的就是提升程序的運行性能,本章的主要內容就是介紹如何分析多線程程序的性能以及如何提高性能。全文大體分爲以下兩個部分:

  • 理論部分:性能分析,包括如何分析多線程程序的執行性和可伸縮性
  • 使用部分:性能提升,包括使用鎖分段,減小鎖粒度,減小鎖範圍等一系列手段以提升程序的性能。

一.性能分析

    這個小節會對兩部分性能進行分析。

  • 執行性:指在當前已有資源下程序的處理能力
  • 可伸縮性:指在可獲得資源增加後,程序的處理能力是否能夠增加的能力

1.1 執行性

    我們將從外部環境條件和線程引入開銷來分析執行性。

1.1.1 執行性的外部條件

    在我們分析併發程序之前,首先,應該確保其外部環境是優良的。

  1. 我們得首先保證程序是可正常運行的,然後才能去分析其性能如何
  2. 分析併發程序的性能如何不能只靠理論分析,分析併發程序性能的基準是測試
  3. 應對不同的環境,性能差別可能很大,比如快速排序和冒泡排序的性能會因爲待排序元素個數的多少而變化,元素少時冒泡排序會高一些,元素多時,快速排序則高一些。總之,我們需要分析外部環境的各種因素,才能編寫最合適的併發程序。

1.1.2 線程引入的開銷

    使用多個線程是比只是用單個線程會多出一些線程開銷的。一般來說,可以分析三個部分

  • 上下文切換

    如果可運行的線程數量大於CPU的數量,那麼,操作系統就會在某個時刻將某個正在運行的線程調度出去,然後將某個即將執行的線程調度進來。這將導致一次上下文切換,在保存當前線程的上下文後,將把待執行線程的上下文設置爲當前上下文。

    切換上下文時,操作系統和JVM會訪問他們共享的數據結構,這將會消耗一段時間。如果當前線程如果從沒有加載過,或者它所需要的某些數據不再緩存中時,會導致緩存缺失,其結果就是會在消耗一段時間。這也是爲什麼線程都有一個最小執行時間的原因,以此將上下文切換的開銷分攤到不會中斷的執行時間上。

    上下文切換的實際開銷會隨着平臺的不同而變化,在大多數處理器中,上下文切換將花費5000-10000個時鐘週期,即幾微秒的時間。

  • 內存同步

    使用volatile和synchronized關鍵字會對內存進行同步以保證可見性。其實質操作是使用“內存柵欄”指令去刷新緩存,刷新硬件的寫緩衝,以及停止執行管道。所以,這就可能會導致一些編譯器的類似指令重排的優化手段無效化,從而導致性能降低。

   現代的JVM能通過優化來去掉一些不會發生競爭的鎖,從而減少不必要的同步開銷。

  • 阻塞

    多個線程競爭某個鎖時,競爭失敗的線程會陷入阻塞。JVM實現阻塞行爲時,可以採用自旋等待(通過循環不斷嘗試獲取鎖),或者通過操作系統將此線程掛起。

    如果等待時間段,使用自旋等待,如果時間長,則直接掛起。一般來說,JVM大多數會選擇直接掛起。

1.2 可伸縮性

    是不是隨着資源增加,併發程序的處理能力就能無限的增加呢?

1.2.1 Amdahl定律

    我們用加速比Speedup來衡量處理能力隨着資源增加而增加的能力。F表示必須併發程序中被串行執行的部分,N表示CPU的數量。

    Amdahl定律的以上公式告訴我們,只有併發程序還有需要串行執行的部分(基本上都有),那麼性能就不會隨着CPU的增加而無限制的增加。看下圖:

    我們在看看同步List和併發List的性能區別,這就是因爲他們鎖的粒度不同和鎖分段而導致的。

二.性能提升

    那麼,我們應該如何提升併發程序的性能呢?主要手段就是減少鎖之間的競爭,並使用合適的CPU數量。

 

2.1 縮小鎖的範圍

    我們可以儘可能的縮小鎖的持有時間。將不需要加鎖的代碼移除同步代碼塊是個不存的選擇。

    在下列代碼中,其實只有get(key)時需要進行同步。

@ThreadSafe
public class AttributeStore {
    @GuardedBy("this") private final Map<String, String> attributes = new HashMap<String, String>();

    public synchronized boolean userLocationMatches(String name, String regexp) {
        String key = "users." + name + ".location";
        String location = attributes.get(key);
        if (location == null) {
            return false;
        } else {
            return Pattern.matches(regexp, location);
        }
    }
}

    範圍縮小以後代碼如下:

@ThreadSafe
public class BetterAttributeStore {
    @GuardedBy("this") private final Map<String, String> attributes = new HashMap<String, String>();

    public boolean userLocationMatches(String name, String regexp) {
        String key = "users." + name + ".location";
        String location;
        synchronized (this) {
            location = attributes.get(key);
        }
        if (location == null) {
            return false;
        } else {
            return Pattern.matches(regexp, location);
        }
    }
}

2.2 減小鎖的粒度

    另一種減小鎖的持有時間的方式是減低線程請求鎖的頻率。我們可以通過鎖分解和鎖分段來實現。

2.2.1 鎖分解

    如果一個鎖需要保護多個獨立的狀態變量,那麼可以將這個鎖分解爲多個鎖,並且每個鎖只保護一個變量。

    在下列代碼中,我們直接對整個對象加鎖。

@ThreadSafe
public class ServerStatusBeforeSplit {
    @GuardedBy("this") public final Set<String> users;
    @GuardedBy("this") public final Set<String> queries;

    public ServerStatusBeforeSplit() {
        users = new HashSet<String>();
        queries = new HashSet<String>();
    }

    public synchronized void addUser(String u) {
        users.add(u);
    }

    public synchronized void addQuery(String q) {
        queries.add(q);
    }

    public synchronized void removeUser(String u) {
        users.remove(u);
    }

    public synchronized void removeQuery(String q) {
        queries.remove(q);
    }
}

    但是我們可以將鎖分解爲兩個鎖,以減小出現競爭的可能性,代碼如下所示:

@ThreadSafe
public class ServerStatusAfterSplit {
    @GuardedBy("users") public final Set<String> users;
    @GuardedBy("queries") public final Set<String> queries;

    public ServerStatusAfterSplit() {
        users = new HashSet<String>();
        queries = new HashSet<String>();
    }

    public void addUser(String u) {
        synchronized (users) {
            users.add(u);
        }
    }

    public void addQuery(String q) {
        synchronized (queries) {
            queries.add(q);
        }
    }

    public void removeUser(String u) {
        synchronized (users) {
            users.remove(u);
        }
    }

    public void removeQuery(String q) {
        synchronized (users) {
            queries.remove(q);
        }
    }
}

2.2.2 鎖分段

    我們將鎖分解進一步擴展爲對一組獨立對象上鎖進行分解,這種情況被稱爲鎖分段。例如ConcurrentHashMap的實現中,使用了一個包含16個鎖的數據,每個所鎖保護所有散列桶的1/16。

2.2.3 避免熱點域

    在一個普通的或者加鎖了的List中,只要修改了List的大小,那麼,就必須修改List的size值,這個size就被稱爲熱點域。這將導致在併發量過大時缺乏伸縮性的問題。

    解決手段就是儘量避免使用熱點域。比如ConcurrentHashMap會對每個分段的大小進行維護,然後統計ConcurrentHashMap的大小時返回所有分段的size之和,這是一種避免熱點域的手段。

2.2.4 替代獨佔鎖

    可以使用獨佔鎖的地方則不用,比如某個數據的讀操作,在保證了可見性的前提下,就完全不需要加鎖。相反,例如某個數據的寫操作,就必須獲得獨佔鎖。

    爲了保證可見性,我們可以恰當的使用原子變量。

2.2.5  不使用對象池

    由於早期Java的垃圾回收的執行速度不是特別快,所以會使用一些對象池,循環使用對象以提升效率。但是現在的JVM垃圾回收的性能已經相當快了。事實上Java分配操作已經比C語言的malloc調用更快了。

    在併發對象中,如果使用對象池就必然意味着使用某種同步機制以在多個線程之間保持數據的線程安全性,這種操作的開銷將是分配操作的數百倍。所以,不要使用對象池。

 

 

    

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章