我理解的Java併發基礎(三):線程安全與鎖

Java線程的狀態
新建(New)、運行(Runable)、無限期等待(Waiting)、限期等待(TimedWaiting)、阻塞(Blocked)、結束(End)。html

網上找到的一份線程的狀態圖

  無限期等待: 線程不會被分配CPU執行時間,等待其餘線程顯式的喚醒。好比:Object.wait()、Thread.join()等。
  限期等待: 線程也不會被分配CPU執行時間,必定時間後由操做系統喚醒。好比:Thread.sleep(long timeout)、Object.wait(long timeout)、Thread.join(long millis)。
  阻塞: 等待獲取排它鎖的狀態。java

  線程的優先級高是告訴操做系統的調度器以較高的頻率執行線程。jvm有10個層級的優先級,windows系統有7個層級,linux系統有100個層級,而Sun的Solaris更是具備2的31次方個層級。jvm與操做系統之間的線程優先級映射不能很好的匹配。建議調整優先級的時候選用MAX_PRIORITY、NORM_PRIORITY和MIN_PRIORITY三種級別。
  線程的優先級默認是5。linux

不一樣線程之間對線程狀態進行通訊的Api:程序員

  • yield() 能夠用來告訴調度器,該線程已經執行完了最重要的部分,能夠適當讓出CPU了。
  • join() 本線程進入等待狀態,直到被join()的線程執行完畢後再繼續執行。JDK1.7有fork/join框架。
  • sleep()和yield()都沒有釋放鎖。wait()會釋放鎖。
  • wait()、sleep()、notify()、notifyAll()能夠用來完成線程之間的協做。
  • interrupt()方法能夠中斷被阻塞的任務。推薦使用Executor的shutdownNow()來中斷它啓動的全部線程。

Thread.sleep(0)、Thread.sleep(1)、Thread.yeld()的區別數據庫

  • Thread.sleep(1):普通的sleep()方法。釋放CPU使用權並睡眠1毫秒,以後繼續爭搶CPU使用權
  • Thread.yeld():釋放當前的CPU使用權,有調度器安排其餘線程使用CPU。若是當前沒有線程爭搶CPU,則該線程繼續執行。
  • Thread.sleep(0):釋放當前CPU使用權,可是,調度器只能安排優先級比當前線程高的線程來使用CPU。若是沒有比當前線程優先級高的,則當前線程繼續執行。

什麼是線程安全問題?
  多線程執行對共享變量進行操做,操做的結果符合程序員的預期,則線程安全。若是操做結果隨機且不符合程序員預期則線程不安全。編程

備註
  一般說的線程安全指的是某一個方法或操做是不是線程安全的。當多個方法並行執行的時候,就另說了: 線程絕對安全
  某方法必定是線程安全的,即便與別的方法並行操做同一個資源。好比CopyOnWriteArrayList、AtomicReference<V>、ReentrantReadWriteLock等。
線程相對安全
  好比Vector、HashTable等這類資源,一般都是某個單一操做是線程安全的,但當多個該類對象的方法並行的時候就可能出錯,好比在A在遍歷,而B在A遍歷期間對元素進行了刪除。windows

線程安全的實現方法:
1,互斥同步(阻塞式、悲觀鎖)。
  實現互斥同步的方式:臨界區(CriticalSection)、互斥量(Mutex)、信號量(Semaphore)
  Java中最基本的互斥同步手段的關鍵字是synchronized。synchronized編譯後的字節碼在同步塊先後會造成monitorenter和monitorexit兩個字節碼指令。數組

2,非阻塞同步(樂觀鎖)。
  概念:通俗講,先進行操做,若是期間沒有其餘線程爭搶共享數據,就操做成功了。整個過程沒有鎖操做。但若是操做期間發現有其餘線程在同時操做併產生了衝突,那就採起其餘的補償措施(最多見的補償措施就是不斷的嘗試,知道成功爲止)。這種策略並不會把線程掛起,因此叫作非阻塞同步。
  這種策略由CPU提供的CAS(Compare-and-Swap)的原子性操做來保障。緩存

Compare-and-Swap(CAS):比較和交換。
  cpu拿到原始值和地址值進行運算,獲得計算後的新值。而後拿着原始值和地址值去內存中找到值進行比較,若是相同,就表示沒有其餘線程有共享操做,用它計算後的新值替換掉內存中的原始值。若是不一樣,就拿着第二次從內存中的值再進行一次運算,重複一樣的動做,直到成功。
  只比較值的CAS會有一個ABA的問題。主內容中的值爲A,線程1拿走去運算了,線程2也拿走去運算了。線程2將新值B設置成功,其餘線程拿到B後又設置回了A。這時候線程1拿着運算後的值C來比較了,由於只比較值,因此線程1會認爲這個值並無發生變化由於設置成C。於是發生線程安全問題。
  解決辦法是拿着內存值的版本號來進行對比。安全

volatile

  被volatile修飾的變量,對其餘線程具備「可見性」。
  被volatile修飾的變量,生成的彙編指令,在該變量前會有Lock指令。Lock前綴的指令在多核處理器下會引起兩件事情:

  1. 將當前處理器緩存行的數據寫會到到系統內存
  2. 這個寫會內存的操做回事其餘CPU裏緩存了該內存地址的數據無效

java線程之間的通訊有4種方式:

  1. A線程寫volatile變量, 隨後B線程讀這個volatile變量。
  2. A線程寫volatile變量, 隨後B線程用CAS更新這個volatile變量。
  3. A線程用CAS更新一個volatile變量, 隨後B線程用CAS更新這個volatile變量。
  4. A線程用CAS更新一個volatile變量, 隨後B線程讀這個volatile變量。

concurrent包的實現:AtomicXxx類的加減和賦值操做。

關於volatile的原理以及cpu對Lock前綴指令實現可見性的原理參考http://www.cnblogs.com/xrq730/p/7048693.html

synchronized

java中的每個對象均可以做爲鎖,有如下3種表現形式:

  1. synchronized修飾普通方法,鎖對象是當前實例對象
  2. synchronized修飾靜態方法,鎖對象是當前類的class對象
  3. synchronized做用於同步方法塊,鎖對象是synchronized()括號裏的對象

  synchronized代碼塊同步指令是monitorenter和monitorexit指令實現的。須要同步的代碼前使用monitorenter,同步結束後的代碼塊後使用monitorexit。monitorenter和monitorexit是必須成對出現的。虛擬機執行monitorenter指令時,會去獲取對應的對象的鎖,執行monitorexit指令時會還回對應的對象的鎖。

  java.util.concurrent.locks包中有ReentrantLock類,也是併發同步時常用的。

ReentrantLock與synchronized的區別
  synchronized是一個java的關鍵字,表示同步。而ReentrantLock是java的一個class。兩者的卻別主要體如今ReentrantLock的一些API上:

  1. 構造方法能夠選擇是否執行公平鎖,默認非公平鎖;
  2. 嘗試獲取鎖(能夠輪詢) tryLock()和嘗試一段時間內去獲取鎖tryLock(long timeout, TimeUnit unit);
  3. 鎖中斷lockInterruptibly(),死鎖的時候能夠採用鎖中斷本身來解除死鎖;
  4. 一個ReentrantLock對象能夠執行newCondition()綁定多個Condition對象來表示多條件執行;
  5. lock()與unlock()容許在不一樣的方法體中調用,增長使用的靈活性,雖然極不推薦這樣使用;

鎖優化
  鎖優化主要是編譯器或者JVM的的優化,跟開發者沒有太大的關係,但做爲一名合格的開發者,仍是須要了解的。鎖優化的幾種方式:自旋鎖與自適應自旋、鎖消除、鎖粗化、輕量級鎖、偏向鎖

自旋鎖
  互斥同步的性能消耗體如今線程阻塞時的線程掛起與恢復很消耗性能,Java虛擬機開發團隊注意到在不少應用上鎖的狀態只會持續不少時間,爲了這很短的時間而阻塞線程不是很划算。因而規定,在線程未獲取到鎖的時候,不是直接掛起,仍是執行一個忙循環(相似while(true))後看是否能獲取到鎖。這項技術就叫自旋鎖。缺點是若是鎖佔據的時間比較長的時候,白白浪費了自旋時佔用的資源。默認自旋次數是10次。
自適應自旋
  虛擬機獲取鎖的自旋時間再也不固定,而是由虛擬機根據前一次的自旋時間及擁有者的狀態來決定。換言之,虛擬機運行時間越長越「聰明」。

鎖消除
  虛擬機在運行期的優化。若是虛擬機判斷到鎖內的變量不會出現共享數據,則會將鎖進行消除。

鎖粗化
  若是虛擬機探測到有一組零碎的操做都對同一個對象加鎖,將會把加鎖範圍擴展(粗化)到整個操做序列的外部,這樣只須要加鎖一次就能夠了。

前面的文章已經介紹過對象在JVM中的佈局方式:
  每個堆中的對象在HosPost虛擬機中都有一個對象頭(ObjectHeader),對象頭裏存儲兩部分信息:一部分是運行時對象自身的哈希碼值(HashCode)、GC分代年齡(Generational GC Age)、鎖標誌位(佔用2個bit的位置)等信息,另外一部分是指向方法去中的類型信息的指針。若是是數組對象的話,還會有額外一部分用來存儲數組長度。

  當一個線程想獲取對象的鎖的時候,先讀取對象頭的鎖標誌位信息。若是鎖標誌位是01,表示該對象上沒鎖。因而會經過CAS來判斷是否能夠獲取鎖。獲取成功後將鎖標誌位置爲00,表示輕量級鎖。若是CAS失敗則表示已經有線程在競爭到鎖了,則鎖標誌位置爲10,升級爲重量級鎖,該線程進入阻塞狀態。若是線程在獲取對象頭的鎖標誌位已是輕量級鎖了,則判斷對象鎖是否屬於當前線程,若是屬於則鎖重入;若是不屬於,則鎖標誌位置爲10,直接升級爲重量級鎖,線程進入阻塞狀態。若是線程在獲取對象頭的鎖標誌位已是重量級鎖了,則線程直接進入阻塞狀態。當持有鎖的線程最終釋放鎖的時候,若是鎖的標誌位爲00(輕量級鎖)則表示在此期間只有本身獲取了鎖,沒有線程競爭。若是鎖的標誌位是10(重量級鎖)則表示有線程曾經競爭過鎖,則在釋放鎖的時候喚醒被掛起的線程。

  輕量級鎖提高性能的依據是「絕大部分的鎖,在同步期內沒有其餘線程競爭」,這個是經驗數據。輕量級鎖的本意是在多線程競爭若是存在鎖競爭,除了鎖自己互斥量的開銷外,還額外發生了CAS操做,所以在有競爭的狀況下,輕量級鎖會比傳統的重量級鎖更慢。

偏向鎖
  Hotspot的做者通過以往的研究發現大多數狀況下鎖不只不存在多線程競爭,並且老是由同一線程屢次得到,爲了讓線程得到鎖的代價更低而引入了偏向鎖。當一個對象首次被一個線程使用CAS請求鎖的時候,將對象的鎖標誌位置爲01,偏向鎖標誌位置爲1。之後該線程每次請求該對象鎖的時候,連CAS操做都省略掉,直接消除同步來執行。若是有其餘線程競爭該對象鎖的時候,偏向鎖撤銷。根據以前的偏向線程是否在執行本來應該是同步的操做的狀態來判斷升級爲輕量級鎖仍是重量級鎖。 偏向鎖能夠提升帶有同步但無競爭的程序性能。若是程序中大多數的鎖老是被多個不一樣的線程訪問,那偏向模式就是多餘的。在具體情形分析下,禁止偏向鎖優反而可能提高性能。

  關於鎖機制及其原理,參考http://www.infoq.com/cn/articles/java-se-16-synchronized/

避免死鎖的幾個經常使用方法:

  1. 避免一個線程同時獲取多個鎖
  2. 避免一個線程在鎖內同時佔用多個資源,儘可能保障每一個鎖只要用一個資源
  3. 嘗試使用定時鎖,使用lock.tryLock(timeout)來替代使用內部鎖機制
  4. 對於數據庫鎖,加鎖和解鎖必須在同一個數據庫鏈接裏,不然會出現解鎖失敗的狀況

  當死鎖不可避免的時候,通常採用鴕鳥策略。

公平性鎖與非公平鎖
  公平性鎖保證了鎖的獲取按照FIFO原則,而代價是進行大量的線程切換。非公平性鎖雖然可能形成線程「飢餓」,但極少的線程切換,保證了其更大的吞吐量。

參考資料:

相關文章
相關標籤/搜索