鎖機制-使用鎖帶來的一些問題

首先強調一點:全部鎖(包括內置鎖和高級鎖)都是有性能消耗的,在高併發的狀況下,使用鎖可能比線程自己的消耗要大,因爲鎖機制帶來的上下文切換,資源同步等消耗,因此若是可能,在任何狀況下都應該少使用鎖,若是不可避免,採用非阻塞算法是一個不錯的解決方案。算法

內部鎖

Java語言經過synchronized關鍵字來保證原子性。這是由於每個Object都有一個隱含的鎖,這個也稱做監視器對象。在進入synchronized以前自動獲取此內部鎖,而一旦離開此方式(無論經過和中方式離開此方法)都會自動釋放鎖。顯然這是一個獨佔鎖,每一個鎖請求之間是互斥的。相對於前面介紹的衆多高級鎖(Lock/ReadWriteLock等),synchronized的代價都比後者要高。可是synchronized的語法比較簡單,並且也比較容易使用和理解,不容易寫法上的錯誤。而咱們知道Lock一旦調用了lock()方法獲取到鎖而未正確釋放的話頗有可能就死鎖了。因此Lock的釋放操做老是跟在finally代碼塊裏面,這在代碼結構上也是一次調整和冗餘。另外前面介紹中說過Lock的實現已經將硬件資源用到了極致,因此將來可優化的空間不大,除非硬件有了更高的性能。可是synchronized只是規範的一種實現,這在不一樣的平臺不一樣的硬件還有很高的提高空間,將來Java在鎖上的優化也會主要在這上面。編程

性能

因爲鎖老是帶了性能影響,因此是否使用鎖和使用鎖的場合就變得尤其重要。若是在一個高併發的Web請求中使用了強制的獨佔鎖,那麼就能夠發現Web的吞吐量將急劇降低。緩存

爲了利用併發來提升性能,出發點就是:更有效的利用現有的資源,同時讓程序儘量的開拓更多可用的資源。這意味着機器儘量的處於忙碌的狀態,一般意義是說CPU忙於計算,而不是等待。固然CPU要作有用的事情,而不是進行無謂的循環。固然在實踐中一般會預留一些資源出來以便應急特殊狀況,這在之後的線程池併發中能夠看到不少例子。安全

線程阻塞

鎖機制的實現一般須要操做系統提供支持,顯然這會增長開銷。當鎖競爭的時候,失敗的線程必然會發生阻塞。JVM既能自旋等待(不斷嘗試,知道成功,不少CAS就是這樣實現的),也可以在操做系統中掛起阻塞的線程,直到超時或者被喚醒。一般狀況下這取決於上下文切換的開銷以及與獲取鎖須要等待的時間兩者之間的關係。自旋等待適合於比較短的等待,而掛起線程比較適合那些比較耗時的等待。多線程

掛起一個線程多是由於沒法獲取到鎖,或者須要某個特定的條件,或者耗時的I/O操做。掛起一個線程須要兩次額外的上下文切換以及操做系統、緩存等多資源的配合:若是線程被提早換出,那麼一旦拿到鎖或者條件知足,那麼又須要將線程換回執行隊列,這對線程而言,兩次上下文切換可能比較耗時。併發

如何減小上下文切換

  • 無鎖併發編程。多線程競爭鎖時,會引發上下文切換,因此多線程處理數據時,能夠用一些辦法來避免使用鎖,如將數據的ID按照Hash算法取模分段,不一樣的線程處理不一樣段的數據。
  • CAS算法。Java的Atomic包使用CAS算法來更新數據,而不須要加鎖。
  • 使用最少線程。避免建立不須要的線程,好比任務不多,可是建立了不少線程來處理,這樣會形成大量線程都處於等待狀態。
  • 協程:在單線程裏實現多任務的調度,並在單線程裏維持多個任務間的切換。

鎖競爭

影響鎖競爭的條件有兩個高併發

  • 持有鎖的時間
  • 請求鎖的頻率

顯然當這二者都很小的時候,鎖競爭不會成爲主要的瓶頸,可是若是使用不當,致使兩者都比較大,那麼CPU可能不能有效的處理任務,致使任務大量堆積性能

因此減小鎖競爭的方法有如下三個:優化

  • 減小持有鎖的時間
  • 減小請求鎖的頻率
  • 使用共享鎖代替獨佔鎖

死鎖

  • 若是一個線程永遠不釋放另外一個線程所須要的資源就會致使死鎖,這有兩種狀況,
  1. 線程A永遠不釋放鎖,致使B永遠拿不到鎖,因此線程B死掉
  2. 線程A須要線程B持有的鎖,線程B擁有線程A須要的鎖,致使線程AB互相等待
  • 還有一種狀況發生死鎖,若是一個線程永遠不能被調度,那麼等待此線程結果的線程可能就死掉了,這種狀況叫作飢餓死鎖,好比說在非公平鎖中,若是某些線程很是活躍,在高併發的狀況下這類線程總能拿到鎖,那麼活躍度低的線程可能永遠拿不到鎖,這樣就發生了飢餓死

死鎖產生的4個必要條件

  • 互斥條件:進程要求對所分配的資源進行排它性控制,即在一段時間內某資源僅爲一進程所佔用。
  • 請求和保持條件:當進程因請求資源而阻塞時,對已得到的資源保持不放。
  • 不剝奪條件:進程已得到的資源在未使用完以前,不能剝奪,只能在使用完時由本身釋放。
  • 環路等待條件:在發生死鎖時,必然存在一個進程--資源的環形鏈。

預防死鎖的解決方案是:

  • 儘量的按照鎖的規範使用鎖,另外請求鎖的粒度要小
  • 在高級鎖裏面使用tryLock或者定時機制,(指定獲取鎖超時的時間,若是時間到了還沒獲取到就放棄)
  • 以肯定的順序得到鎖,針對兩個特定的鎖,開發者能夠嘗試按照鎖對象的hashCode值大小的順序,分別得到兩個鎖,這樣鎖老是會以特定的順序得到鎖,那麼死鎖也不會發生。問題變得更加複雜一些,若是此時有多個線程,都在競爭不一樣的鎖,簡單按照鎖對象的hashCode進行排序(單純按照hashCode順序排序會出現「環路等待」),可能就沒法知足要求了,這個時候開發者可使用銀行家算法,全部的鎖都按照特定的順序獲取,一樣能夠防止死鎖

銀行家算法

  • 銀行家算法:首先須要定義狀態和安全狀態的概念。系統的狀態是當前給進程分配的資源狀況。所以,狀態包含兩個向量Resource(系統中每種資源的總量)和Available(未分配給進程的每種資源的總量)及兩個矩陣Claim(表示進程對資源的需求)和Allocation(表示當前分配給進程的資源)。安全狀態是指至少有一個資源分配序列不會致使死鎖。當進程請求一組資源時,假設贊成該請求,從而改變了系統的狀態,而後肯定其結果是否還處於安全狀態。若是是,贊成這個請求;若是不是,阻塞該進程知道贊成該請求後系統狀態仍然是安全的。

死鎖恢復

  • 利用搶佔恢復
  • 殺死進程

活鎖

線程老是嘗試某項操做卻老是失敗的狀況這種狀況下線程沒有被阻塞,可是任務不能被執行。操作系統

  • 好比在一個死循環里老是嘗試作某件事,結果卻老是失敗,線程將永遠沒法跳出這個循環
  • 在一個隊列中,每次從隊列頭取出一個任務來執行,每次都失敗,而後將任務放入隊列頭,繼續失敗
  • 在碰撞協議狀況下,線程優先度低的線程得不到執行,也會致使活鎖發生。
相關文章
相關標籤/搜索