天天看點

深入Java線程管理(三):線程同步

一、 引入同步: 有一個很經典的案例,即銀行取款問題。我們可以先看下銀行取款的基本流程:

1)使用者輸入賬戶、密碼,系統判斷使用者的賬戶、密碼是否比對。

2)使用者輸入取款金額。

3)系統判斷賬戶金額是否大于取款金額。

4)如果餘額大于取款金額,則取款成功;如果餘額小于取款金額,則取款失敗。

假設,此時有兩個人,同時使用同一個賬戶并發取錢,我們模拟下取款流程:

接下來,提供一個取錢的線程類,該線程類根據執行賬戶、取錢數量進行取錢操作,取錢的邏輯是當其餘額不足時無法提取現金,當餘額足夠時系統吐出鈔票,餘額減少。

輸出:

---------- java ----------

乙取錢成功!吐出鈔票:800.0

甲取錢成功!吐出鈔票:800.0

餘額為: 200.0

餘額為: -600.0

輸出完成 (耗時 0 秒) - 正常終止

之是以會出現這樣的錯誤,是因為線程排程具有不确定性,在賬戶餘額隻有1000時,取出了1600,而且賬戶餘額出現了負值。

要解決該問題,java引入了同步螢幕,線上程開始執行同步代碼塊之前,必須先獲得同步螢幕的鎖定。

同步螢幕的目的: 阻止多個線程對同一個共享資源進行并發通路,是以通常推薦使用可能被并發通路的共享資源充當同步螢幕。

接下來,我們使用同步螢幕鎖定線程的執行體run()方法:

除了使用同步代碼塊之外,我們還可以使用同步方法。同步方法無須顯示指定同步螢幕,同步方法的同步螢幕是this,也就是對象本身。

通過通過方法可以非常友善的實作線程安全的類:

·該類的對象可以被多個線程安全的通路。

·每個線程調用該對象的任意方法之後都将得到正确的結果。

·每個線程調用該對象的任意方法之後,該對象的狀态依然保持合理狀态。

上面程式中增加了一個代表取錢的draw()方法,并使用了synchronized關鍵字修飾,該方法變為同步方法,同步方法的同步螢幕是this,是以對于同一個account賬戶而言,任意時刻隻能有一個線程account對象鎖定,然後進入draw()方法執行取錢操作。

接下來,我們看下并發的線程類該如何寫:

線程類無須事前取錢操作,而是直接調用account的draw()方法來執行取錢操作。由于已經使用了synchronized關鍵字修飾了draw()方法,同步方法的同步螢幕就是this,而this總代表調用該方法的對象——在上面的示例中,調用draw()方法的對象時account,是以多個線程并發修改一份account之前,必須先對account對象加鎖。

二、 同步鎖(lock)

lock提供了比synchronized方法和synchronized代碼塊更廣泛的鎖定操作,lock實作允許更靈活的結構。lock是控制多個線程對共享資源進行通路的工具。

某些鎖可能允許對共享資源的并發通路,比如readwritelock(讀寫鎖)。比較常用的lock有reentrantlock(可重入鎖),使用它可以顯式的加鎖、釋放鎖。

reentrantlock鎖具有可重入性,也就是說,一個線程可以對已被加鎖的reentrantlock鎖再次加鎖,reentrantlock對象會維持一個計數器來之宗lock()方法的嵌入調用,線程在每次調用lock()枷鎖後,必須顯示調用unlock()來釋放鎖,是以一段被鎖保護的代碼可以調用另一個被相同鎖保護的方法。

三、死鎖

當兩個線程互相等待對方釋放同步螢幕時就會發生死鎖,java虛拟機沒有監測,也沒有采取措施處理死鎖情況,是以多線程程式設計時應該采取避免死鎖出現。

死鎖的舉例: