java併發編程(十)性能與可伸縮行

線程的最主要目的是提高程序的運行性能
要想通過併發來獲得更好的性能,需要努力做好兩件事情:更有效地利用現有處理資源,以及在出現新的處理資源時使程序儘可能地利用這些新資源。從性能監視的視角來看,CPU需要儘可能保持忙碌狀態。這也就是程序的性能與可伸縮性。

線程引入的開銷

儘管使用多個線程的目標是提升整體性能,但與單線程的方法相比,使用多個線程總會引人一些額外的性能開銷。造成這些開銷的操作包括:線程之間的協調(例如加鎖、觸發信號以及內存同步等),增加的上下文切換,線程的創建和銷燬,以及線程的調度等。
一個併發設計很糟糕的應用程序,其性能甚至比實現相同功能的串行程序的性能還要差。
對於爲了提升性能而引入的線程來說,並行帶來的性能提升必須超過併發導致的開銷。

  • 上下文切換
    切換上下文需要一定的開銷,而在線程調度過程中需要訪問由操作系統和JVM共享的數據結構,但上下文切換的開銷並不只是包含JVM和操作系統的開銷。當一個新的線程被切換進來時,它所需要的數據可能不在當前處理器的本地緩存中,因此上下文切換將導致一些緩存缺失,因而線程在首次調度運行時會更加緩慢。
  • 內存同步
    同步操作的性能開銷包括多個方面。在synchronized和volatile提供的可見性保證中可能會使用一些特殊指令,它們將抑制一些編譯器優化操作。在內存柵欄中,大多數操作都是不能被重排序的。
    在評估同步操作帶來的性能影響時,區分有競爭的同步和無競爭的同步非常重要。 synchronized機制針對無競爭的同步進行了優化(volatile通常是非競爭的)這個基本的機制已經非常快了,並且JVM還能進行額外的優化以進一步降低或消除開銷。因此,不要過度擔心非競爭同步帶來的開銷,我們應該將優化重點放在那些發生鎖競爭的地方。
  • 阻塞
    當線程阻塞時,需要被掛起,在這個過程中將包含兩次額外的上下文切換,以及所有必要的操作系統操作和緩存操作:被阻塞的線程在其執行時間片還未用完之前就被交換出去,而在隨後當要獲取的鎖或者其他資源可用時,又再次被切換回來。(由於鎖競爭而導致阻塞時,線程在持有鎖時將存在一定的開銷:當它釋放鎖時,必須告訴操作系統恢復運行阻塞的線程。)

減少鎖的競爭

串行操作會降低可伸縮性,並且上下文切換也會降低性能。在鎖上發生競爭時將同時導致這兩種問題,因此減少鎖的競爭能夠提高性能和可伸縮性。

  • 縮小鎖的範圍(“快進快出”)
    降低發生競爭可能性的一種有效方式就是儘可能縮短鎖的持有時間。例如,可以將一些與鎖無關的代碼移出同步代碼塊,尤其是那些開銷較大的操作,以及可能被阻塞的操作,例如l/O操作。
  • 減小鎖的粒度
    鎖分解和鎖分段等技術可以減小鎖的粒度,從而降低鎖的競爭。
    鎖分解:如果一個鎖需要保護多個相互獨立的狀態變量,那麼可以將這個鎖分解爲多個鎖,並且每個鎖只保護一個變量,從而提高可伸縮性,並最終降低每個鎖被請求的頻率。
    鎖分段:典型的ConcurrentHashMap就是採用了分段鎖的思想。鎖分段的一個劣勢在於:與採用單個鎖來實現獨佔訪問相比,要獲取多個鎖來實現獨佔訪問將更加困難並且開銷更高。解決辦法參考ConcurrentHashMap的size方法實現。
  • 避免熱點域
    每個操作都請求多個變量時,鎖的粒度將很難降低。這是在性能與可伸縮性之間相互制 衡的另一個方面,一些常見的優化措施,例如將一些反覆計算的結果緩存起來,都會引人一些 “熱點域(Hot Field) ”,而這些熱點域往往會限制可伸縮性。
  • 一些替換獨佔鎖的方法
    降低競爭鎖還有一種方式是放棄使用獨佔鎖,從而有助於使用一種友好併發的方式來管理共享狀態。例如,使用併發容器、讀·寫鎖、不可變對象以及原子變量。
    ReadWriteLock實現了一種在多個讀取操作以及單個寫人操作情況下的加鎖規則,比如ReadWriteLock對於只讀的數據結構,其中包含的不變性可以完全不需要加鎖操作。
    原子變量提供了一種方式來降低更新“熱點域"時的開銷,例如靜態計數器、序列發生器、或者對鏈表數據結構中頭節點的引用。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章