1. 程式人生 > >併發程式設計中的鎖

併發程式設計中的鎖

一、併發與並行

1、併發:一個處理器同時處理多個任務,邏輯上的同時發生

2、並行:多個處理器或者多核處理器同時處理不同的任務,物理上的同時發生

併發是一個人同時吃三個饅頭,並行是三個人同時吃三個饅頭

二、程序與執行緒

程序是作業系統的結構基礎,程序也是作業系統進行資源分配的獨立單元;一個程序就是一個程式的執行,可將.exe程式看成是一個程序,程序中有很多的子任務,每個子任務對應一個執行緒,執行緒是大多數作業系統排程的基本單元;比如在qq.exe程序中,有好友線上視訊執行緒,有檔案上傳下載執行緒,這些執行緒擁有自己特有的計數器、棧、、堆、區域性變量表等屬性,並且可以訪問共享記憶體的變數。

三、為什麼使用併發程式設計

1、更多的處理器核心

隨著處理器核心處越來越多,以及超執行緒技術的廣泛運用,現在大多數計算機都比以往更加擅長平行計算,而處理器效能的提升方式也從更高的主頻向更多的核心發展。試想一下,一個單執行緒程式在執行時只能使用一個處理器核心,那麼再多的處理器核心加入也無法顯著提升該程式的執行效率。相反,如果該程式使用多執行緒技術,將計算邏輯分配到多個處理器核心上,就會顯著減少程式的處理時間,並且隨著更多的核心加入而變得更有效率。

2、更快的響應時間

例如,一筆訂單的建立,它包括插入訂單資料、生成訂單快照、傳送郵件通知賣家和記錄貨品銷售數量等。使用者從點選“購買”開始,就要等這些操作全部完成之後才能看到購買成功的結果。這時候可以採用多執行緒技術,就是將資料一致性不強的操作派發給其他執行緒處理(也可以使用訊息佇列),這樣做的好處是響應使用者請求的執行緒可以儘快的處理完成,提升了使用者體驗。

3、更好的程式設計模型

Java為多執行緒提供了良好、考究並且一致的程式設計模型。

四、併發程式設計的挑戰

併發程式設計的目的是為了讓程式執行的更快,但是並不是啟動更多的執行緒就能讓程式最大限度的併發執行,併發程式設計會面臨很多的挑戰,比如上下文的切換、死鎖、以及受限軟體和硬體的資源限制問題

1、建立銷燬執行緒消耗資源

2、上下文切換

Cpu通過時間片分配演算法來迴圈執行任務,在切換前會儲存上一個任務的狀態,以便下回切換為這個任務,可以載入這個任務的狀態。任務從儲存到再次載入的過程就是一次上下文的切換,這樣的切換會造成執行緒執行緒的阻塞和喚醒。

java中的執行緒是對映到作業系統原生執行緒之上的,如果阻塞或者喚醒一個執行緒就會需要作業系統的介入,需要在使用者態和核心態之間切換,這種切換會消耗大量的系統資源。因為使用者態和核心態都有各自的專用記憶體空間、專用暫存器等。使用者態切至核心態會傳遞大量的變數、引數給核心,核心需要儲存使用者態的引數變數等,以便於核心態呼叫結束後切換回使用者態繼續工作。

上下文每秒鐘切換1000多次,如何減少?

(1)無鎖併發程式設計:多執行緒競爭鎖時會引起上下文的切換,將資料的ID按照hash演算法取模分段,不同的執行緒處理不同段的資料。

(2)CAS演算法:使用cas演算法更新資料不用加鎖(Atomic包)

(3)使用最少執行緒:

(4)協程:在單執行緒裡實現多執行緒的排程,並且在單執行緒裡維持多個任務之間的切換

3、死鎖:兩個或者多個執行緒互相持有對方所需要的資源,並且互相等待對方釋放資源。如果都不主動釋放資源,將會產生死鎖

避免死鎖?

(1)避免一個執行緒同時獲得多個鎖

(2)避免一個執行緒在鎖內同時佔用多個資源

(3)使用定時鎖

檢測死鎖?

(1)Jconsole:圖形化工具,用於連線jvm程序以監控java程式碼

(2)Jstack:命令列工具,用於生成jvm當前時刻的執行緒快照

4、受限資源

(1)軟體:資料庫、socket連線數,可以使用資源池將資源複用來解決。

(2)硬體:寬頻的上傳下載速度、硬碟的讀寫速度、cpu的處理速度等。解決辦法:使用叢集並行執行程式,不同的機器處理不同的資料。使用“資料ID%機器數”得到機器編號,由對應機器編號處理這筆資料。

五、鎖的升級與優化

Jdk為了減少獲得/釋放鎖帶來的消耗,引入了偏向鎖和輕量級鎖

1、鎖的狀態(可升級不可降級)

無鎖狀態→偏向鎖(樂觀鎖)→輕量級鎖(樂觀鎖)→重量級鎖(悲觀鎖)

2、樂觀鎖和悲觀鎖

樂觀鎖:假定不會發生資料衝突,只是在資料提交的時候才去檢查是否違反資料完整性,核心思路:每次不加鎖而是假定沒有衝突去完成某次操作,如果因為衝突失敗就重試,直到成功為止。CAS、偏向所、輕量級鎖

悲觀鎖:假定會發生衝突,遮蔽一切可能違反資料完整性的操作。

3、鎖的升級

A:偏向鎖:只有一個執行緒進入臨界區、兩個或者多個執行緒交替進入(之前進入的執行緒都不在進入臨界區)

B:輕量級鎖:兩個執行緒同時進入臨界區(第二個執行緒通過自旋獲得鎖)

C:重量級鎖:兩個執行緒同時進入臨界區(自旋超過一定的時間)、多個執行緒同時進入臨界區

Synchorized(lock){

//doSomething

}
假設有Thread#1和Thread#2分三種情況:

情況一:只有Thread#1進入臨界區

情況二:Thread#1和Thread#2同時進入臨界區

情況三:Thread#1和Thread#2同時進入臨界區,再進來Thread#3
情況一是偏向鎖的應用場景:只有Thread#1進入臨界區,jvm將lock的物件頭Mark Word的鎖標誌位設為01,同時使用cas操作將獲取到Thread#1的執行緒ID記錄到Mark Word中。如果cas成功,則以後Thread#1在進入或者退出同步塊時不需要使用cas操作來進行加鎖或者解鎖,只是簡單測試物件頭中Mark Word裡是否儲存指向當前執行緒的偏向鎖,對比ID,不在進行cas操作

情況二:若Thread#2嘗試進入臨界區時,因為偏向鎖使用了一種等到競爭出現才會釋放鎖的機制,因此Thread#2可以看到物件偏向狀態,這時候表示已經存在競爭了,檢查持有該鎖的執行緒是否存活,如果掛了(Thread#1已死),則可以將物件變成無鎖狀態,然後重新偏向Thraed#2,如果原來執行緒依舊存活,則馬上執行Thread#1的運算元棧檢查該物件的使用情況,如果還需要持有偏向鎖,則升級為偏向鎖,如果不存在使用了(不在進入臨界區),則將物件恢復無鎖狀態重新偏向。

情況三:輕量級鎖認為競爭存在,但是競爭程度很低,一般兩個執行緒對一個鎖的操作會錯開或者稍等一下(自旋)另一個執行緒就會釋放鎖。但是當自旋超過一定次數或者有一個執行緒持有鎖,另一個在自旋這時候又有第三個執行緒來訪時,輕量級鎖膨脹為重量級鎖,重量級鎖除了持有鎖的執行緒以外的執行緒都阻塞,防止cpu空轉

4、鎖的對比

偏向鎖:優點加鎖或者解鎖都不需要額外的消耗,與執行非同步方法僅存在納米級差距,缺點如果存在競爭帶來額外撤銷鎖的消耗

輕量級鎖:優點競爭的執行緒不會阻塞,缺點自旋會消耗cpu

重量級鎖:優點競爭不會自旋消耗cpu,缺點執行緒阻塞,響應時間緩慢

5、鎖的優化

1、自旋鎖和自適應鎖

同步互斥對效能最大的影響就是阻塞的實現,掛起執行緒和恢復執行緒的實現均需要轉入核心態完成,共享資料的鎖狀態只會維持很短時間,為了這段時間去掛起執行緒和恢復執行緒不值得,因此可以讓後面的執行緒稍等一下,給執行緒執行忙迴圈,這就是自旋鎖。自旋鎖會浪費cpu,自旋鎖不能代替阻塞,當自旋次數超所一定的次數(預設10次),則使用傳統掛起執行緒。

但是在jdk1.6中引入了自適應的自旋鎖。自適應意味著自旋的時間不在固定。如果對於同一個鎖,自旋等待剛剛成功,並且持有鎖的執行緒正在執行,那麼jvm會將自旋等待延遲更久(比如100個迴圈)。如果對於某個鎖,自旋很少得到鎖,jvm會忽略以後這個鎖的自旋過程,以避免浪費cpu

2、鎖清除

鎖清除指的是JIT在執行時,對一些程式碼上要求同步,但是檢測到不可能存在共享資料競爭的鎖進行,則會將其消除。主要依據逃逸分析的資料支援,如果一段程式碼中,堆上所有資料都不會逃逸出去而被其他執行緒訪問到,則可以認為執行緒私有無需加鎖

3、鎖粗化

如果連續一系列操作對同一個鎖進行加鎖解鎖,甚至加鎖會出現在迴圈體中(如連續的append()操作),會把整個加鎖同步的範圍擴充套件到整個操作序列的外部。