1、介紹java
本文重點討論多線程應用程序的性能問題。如用何種技術方法來減小鎖競爭,以及如何用代碼來實現。緩存
2、性能數據結構
咱們都知道,多線程能夠提升線程的性能。性能提高的根本緣由在於咱們有多核的CPU或多個CPU。每一個CPU的內核均可以本身完成任務,所以把一個大的任務分解成一系列的可彼此獨立運行的小任務就能夠提升程序的總體性能了。能夠舉個例子,好比有個程序用來將硬盤上某個文件夾下的全部圖片的尺寸進行修改,應用多線程技術就能夠提升它的性能。使用單線程的方式只能依次遍歷全部圖片文件而且執行修改,若是咱們的CPU有多個核心的話,毫無疑問,它只能利用其中的一個核。使用多線程的方式的話,咱們可讓一個生產者線程掃描文件系統把每一個圖片都添加到一個隊列中,而後用多個工做線程來執行這些任務。若是咱們的工做線程的數量和CPU總的核心數同樣的話,咱們就能保證每一個CPU核心都有活可幹,直到任務被所有執行完成。多線程
對於另一種須要較多IO等待的程序來講,利用多線程技術也能提升總體性能。假設咱們要寫這樣一個程序,須要抓取某個網站的全部HTML文件,而且將它們存儲到本地磁盤上。程序能夠從某一個網頁開始,而後解析這個網頁中全部指向本網站的連接,而後依次抓取這些連接,這樣周而復始。由於從咱們對遠程網站發起請求到接收到全部的網頁數據須要等待一段時間,因此咱們能夠將此任務交給多個線程來執行。讓一個或稍微更多一點的線程來解析已經收到的HTML網頁以及將找到的連接放入隊列中,讓其餘全部的線程負責請求獲取頁面。ide
高性能就是在短的時間窗口內作儘可能多的事情。這個固然是對性能一詞的最經典解釋了。可是同時,使用線程也能很好地提高咱們程序的響應速度。想象咱們有這樣一個圖形界面的應用程序,上方有一個輸入框,輸入框下面有一個名字叫「處理」的按鈕。當用戶按下這個按鈕的時候,應用程序須要從新對按鈕的狀態進行渲染(按鈕看起來被按下了,當鬆開鼠標左鍵時又恢復原狀),而且開始對用戶的輸入進行處理。若是處理用戶輸入的這個任務比較耗時的話,單線程的程序就沒法繼續響應用戶其餘的輸入動做了,函數
可擴展性(Scalability)的意思是程序具有這樣的能力:經過添加計算資源就能夠得到更高的性能。想象咱們須要調整不少圖片的大小,由於咱們機器的CPU核心數是有限的,因此增長線程數量並不總能相應提升性能。相反,由於調度器須要負責更多線程的建立和關閉,也會佔用CPU資源,反而有可能下降性能。性能
一、對性能的影響優化
寫到這裏,咱們已經取得這樣一個觀點:增長更多的線程能夠提升程序的性能和響應速度。可是另外一方面,想要取得這些好處卻並不是垂手可得,也須要付出一些代價。線程的使用對性能的提高也會有所影響。網站
首先,第一個影響來自線程建立的時候。線程的建立過程當中,JVM須要從底層操做系統申請相應的資源,而且在調度器中初始化數據結構,以便決定執行線程的順序。操作系統
若是你的線程的數量和CPU的核心數量同樣的話,每一個線程都會運行在一個核心上,這樣或許他們就不會常常被打斷了。可是事實上,在你的程序運行的時候,操做系統也會有些本身的運算須要CPU去處理。因此,即便這種情形下,你的線程也會被打斷而且等待操做系統來從新恢復它的運行。當你的線程數量超過CPU的核心數量的時候,狀況有可能變得更壞。在這種狀況下,JVM的進程調度器會打斷某些線程以便讓其餘線程執行,線程切換的時候,剛纔正在運行的線程的當前狀態須要被保存下來,以便等下次運行的時候能夠恢復數據狀態。不只如此,調度器也會對它本身內部的數據結構進行更新,而這也須要消耗CPU週期。全部這些都意味着,線程之間的上下文切換會消耗CPU計算資源,所以帶來相比單線程狀況下沒有的性能開銷。
多線程程序所帶來的另一個開銷來自對共享數據的同步訪問保護。咱們可使用synchronized關鍵字來進行同步保護,也可使用Volatile關鍵字來在多個線程之間共享數據。若是多於一個線程想要去訪問某一個共享數據結構的話,就發生了爭用的情形,這時,JVM須要決定哪一個進程先,哪一個進程後。若是決定該要執行的線程不是當前正在運行的線程,那麼就會發生線程切換。當前線程須要等待,直到它成功得到了鎖對象。JVM能夠本身決定如何來執行這種「等待」,假如JVM預計離成功得到鎖對象的時間比較短,那JVM可使用激進等待方法,好比,不停地嘗試得到鎖對象,直到成功,在這種狀況下這種方式可能會更高效,由於比較進程上下文切換來講,仍是這種方式更快速一些。把一個等待狀態的線程挪回到執行隊列也會帶來額外的開銷。
所以,咱們要盡力避免因爲鎖競爭而帶來的上下文切換。
下面將具體闡述兩種下降這種競爭發生的方法。
二、鎖競爭
兩個或更多線程對鎖的競爭訪問會帶來額外的運算開銷,由於競爭的發生逼迫調度器來讓一個線程進入激進等待狀態,或者讓它進行等待狀態而引起兩次上下文切換。有某些狀況下,鎖競爭的惡果能夠經過如下方法來減輕:
1.少鎖的做用域;
2.少須要獲取鎖的頻率;
3.量使用由硬件支持的樂觀鎖操做,而不是synchronized;
4.量少用synchronized;
5.少使用對象緩存
2.1 縮減同步域
若是代碼持有鎖超過必要的時間,那麼能夠應用這第一種方法。一般咱們能夠將一行或多行代碼移出同步區域來下降當前線程持有鎖的時間。在同步區域裏運行的代碼數量越少,當前線程就會越早地釋放鎖,從而讓其餘線程更早地得到鎖。這與Amdahl法則相一致的,由於這樣作減小了須要同步執行的代碼量。
2.2 分拆鎖
另一種減小鎖競爭的方法是將一塊被鎖定保護的代碼分散到多個更小的保護塊中。若是你的程序中使用了一個鎖來保護多個不一樣對象的話,這種方式會有用武之地。假設咱們想要經過程序來統計一些數據,而且實現了一個簡單的計數類來持有多個不一樣的統計指標,而且分別用一個基本計數變量來表示(long類型)。由於咱們的程序是多線程的,因此咱們須要對訪問這些變量的操做進行同步保護,由於這些操做動做來自不一樣的線程。要達到這個目的,最簡單的方式就是對每一個訪問了這些變量的函數添加synchronized關鍵字。
2.3 分離鎖
上面一個例子展現瞭如何將一個單獨的鎖分開爲多個單獨的鎖,這樣使得各線程僅僅得到他們將要修改的對象的鎖就能夠了。可是另外一方面,這種方式也增長了程序的複雜度,若是實現不恰當的話也可能形成死鎖。
分離鎖是與分拆鎖相似的一種方法,可是分拆鎖是增長鎖來保護不一樣的代碼片斷或對象,而分離鎖是使用不一樣的鎖來保護不一樣範圍的數值。JDK的java.util.concurrent包裏的ConcurrentHashMap即便用了這種思想來提升那些嚴重依賴HashMap的程序的性能。在實現上,ConcurrentHashMap內部使用了16個不一樣的鎖,而不是封裝一個同步保護的HashMap。16個鎖每個負責保護其中16分之一的桶位(bucket)的同步訪問。這樣一來,不一樣的線程想要向不一樣的段插入鍵的時候,相應的操做會受到不一樣的鎖來保護。可是反過來也會帶來一些很差的問題,好比,某些操做的完成如今須要獲取多個鎖而不是一個鎖。若是你想要複製整個Map的話,這16個鎖都須要得到才能完成。
2.4 原子操做
另一種減小鎖競爭的方法是使用原子操做。java.util.concurrent包對一些經常使用基礎數據類型提供了原子操做封裝的類。原子操做類的實現基於處理器提供的「比較置換」功能(CAS),CAS操做只在當前寄存器的值跟操做提供的舊的值同樣的時候纔會執行更新操做。
這個原理能夠用來以樂觀的方式來增長一個變量的值。若是咱們的線程知道當前的值的話,就會嘗試使用CAS操做來執行增長操做。若是期間別的線程已經修改了變量的值,那麼線程提供的所謂的當前值已經跟真實的值不同了,這時JVM來嘗試從新得到當前值,而且再嘗試一次,反反覆覆直到成功爲止。雖然循環操做會浪費一些CPU週期,可是這樣作的好處是,咱們不須要任何形式的同步控制。
2.5 避免熱點代碼段
一個典型的LIST實現經過會在內容維護一個變量來記錄LIST自身所包含的元素個數,每一次從列表裏刪除或增長元素的時候,這個變量的值都會改變。若是LIST在單線程應用中使用的話,這種方式無可厚非,每次調用size()時直接返回上一次計算以後的數值就好了。若是LIST內部不維護這個計數變量的話,每次調用size()操做都會引起LIST從新遍歷計算元素個數。
這種不少數據結構都使用了的優化方式,到了多線程環境下時卻會成爲一個問題。假設咱們在多個線程之間共享一個LIST,多個線程同時地去向LIST裏面增長或刪除元素,同時去查詢大的長度。這時,LIST內部的計數變量成爲一個共享資源,所以全部對它的訪問都必須進行同步處理。所以,計數變量成爲整個LIST實現中的一個熱點。
本文所講述的這些優化方案再一次的代表,每一種優化方式在真正應用的時候必定須要多多仔細觀測。不成熟的優化方案表面看起來好像頗有道理,可是事實上頗有可能會反過來成爲性能的瓶頸。