天天看點

《Java多線程程式設計核心技術》——2.1節synchronized同步方法

本節書摘來自華章社群《java多線程程式設計核心技術》一書中的第2章,第2.1節synchronized同步方法,作者高洪岩,更多章節内容可以通路雲栖社群“華章社群”公衆号檢視

2.1 synchronized同步方法

在第1章中已經接觸“線程安全”與“非線程安全”相關的技術點,它們是學習多線程技術時一定會遇到的經典問題。“非線程安全”其實會在多個線程對同一個對象中的執行個體變量進行并發通路時發生,産生的後果就是“髒讀”,也就是取到的資料其實是被更改過的。而“線程安全”就是以獲得的執行個體變量的值是經過同步處理的,不會出現髒讀的現象。此知識點在第1章也介紹,但本章将細化線程并發通路的内容,在細節上更多接觸在并發時變量值的處理方法。

2.1.1 方法内的變量為線程安全

“非線程安全”問題存在于“執行個體變量”中,如果是方法内部的私有變量,則不存在“非線程安全”問題,所得結果也就是“線程安全”的了。

下面的示例項目就是實作方法内部聲明一個變量時,是不存在“非線程安全”問題的。

建立t1項目,hasselfprivatenum.java檔案代碼如下:

程式運作後的效果如圖2-1所示。

《Java多線程程式設計核心技術》——2.1節synchronized同步方法

可見,方法中的變量不存在非線程安全問題,永遠都是線程安全的。這是方法内部的變量是私有的特性造成的。

2.1.2 執行個體變量非線程安全

如果多個線程共同通路1個對象中的執行個體變量,則有可能出現“非線程安全”問題。

用線程通路的對象中如果有多個執行個體變量,則運作的結果有可能出現交叉的情況。此情況在第1章中非線程安全的案例示範過。

如果對象僅有1個執行個體變量,則有可能出現覆寫的情況。

建立t2項目,hasselfprivatenum.java檔案代碼如下:

程式運作後的結果如圖2-2所示。

《Java多線程程式設計核心技術》——2.1節synchronized同步方法

本實驗是兩個線程同時通路一個沒有同步的方法,如果兩個線程同時操作業務對象中的執行個體變量,則有可能會出現“非線程安全”問題。此示例的知識點在前面已經介紹過,隻需要在public void addi(string username)方法前加關鍵字synchronized即可。更改後的代碼如下:

程式再次運作結果如圖2-3所示。

《Java多線程程式設計核心技術》——2.1節synchronized同步方法

實驗結論:在兩個線程通路同一個對象中的同步方法時一定是線程安全的。本實驗由于是同步通路,是以先列印出a,然後列印出b。

2.1.3 多個對象多個鎖

再來看一個實驗,建立項目名稱為twoobjecttwolock,建立hasselfprivatenum.java類,代碼如下:

上面的代碼中有同步方法addi,說明此方法應該被順序調用。

建立線程threada.java和threadb.java代碼,如圖2-4所示。

《Java多線程程式設計核心技術》——2.1節synchronized同步方法

類run.java代碼如下:

建立了2個hasselfprivatenum.java類的對象,程式運作的結果如圖2-5所示。

《Java多線程程式設計核心技術》——2.1節synchronized同步方法

上面示例是兩個線程分别通路同一個類的兩個不同執行個體的相同名稱的同步方法,效果卻是以異步的方式運作的。本示例由于建立了2個業務對象,在系統中産生出2個鎖,是以運作結果是異步的,列印的效果就是先列印b,然後列印a。

從上面程式運作結果來看,雖然在hasselfprivatenum.java中使用了synchronized關鍵字,但列印的順序卻不是同步的,是交叉的。為什麼是這樣的結果呢?

關鍵字synchronized取得的鎖都是對象鎖,而不是把一段代碼或方法(函數)當作鎖,是以在上面的示例中,哪個線程先執行帶synchronized關鍵字的方法,哪個線程就持有該方法所屬對象的鎖lock,那麼其他線程隻能呈等待狀态,前提是多個線程通路的是同一個對象。

但如果多個線程通路多個對象,則jvm會建立多個鎖。上面的示例就是建立了2個hasselfprivatenum.java類的對象,是以就會産生出2個鎖。

同步的單詞為synchronized,異步的單詞為asynchronized。

2.1.4 synchronized方法與鎖對象

為了證明前面講述線程鎖的是對象,建立實驗用的項目synchronizedmethodlockobject,類myobject.java檔案代碼如下:

程式運作後的效果如圖2-6所示。

更改myobject.java代碼如下:

如上面代碼所示,在methoda方法前加入了關鍵字synchronized進行同步處理。程式再次運作效果如圖2-7所示。

《Java多線程程式設計核心技術》——2.1節synchronized同步方法

通過上面的實驗得到結論,調用用關鍵字synchronized聲明的方法一定是排隊運作的。另外需要牢牢記住“共享”這兩個字,隻有共享資源的讀寫通路才需要同步化,如果不是共享資源,那麼根本就沒有同步的必要。

那其他的方法在被調用時會是什麼效果呢?如何檢視到lock鎖對象的效果呢?繼續建立實驗用的項目synchronizedmethodlockobject2,類檔案myobject.java代碼如下:

兩個自定義線程類分别調用不同的方法,代碼如圖2-8所示。

《Java多線程程式設計核心技術》——2.1節synchronized同步方法

檔案run.java代碼如下:

程式運作結果如圖2-9所示。

通過上面的實驗可以得知,雖然線程a先持有了object對象的鎖,但線程b完全可以異步調用非synchronized類型的方法。

繼續實驗,将myobject.java檔案中的methodb()方法前加上synchronized關鍵字,代碼如下:

本示例是兩個線程通路同一個對象的兩個同步的方法,運作結果如圖2-10所示。

 

《Java多線程程式設計核心技術》——2.1節synchronized同步方法

此實驗的結論是:

1)a線程先持有object對象的lock鎖,b線程可以以異步的方式調用object對象中的非synchronized類型的方法。

2)a線程先持有object對象的lock鎖,b線程如果在這時調用object對象中的synchronized類型的方法則需等待,也就是同步。

2.1.5 髒讀

在2.1.4節示例中已經實作多個線程調用同一個方法時,為了避免資料出現交叉的情況,使用synchronized關鍵字來進行同步。

雖然在指派時進行了同步,但在取值時有可能出現一些意想不到的意外,這種情況就是髒讀(dirtyread)。發生髒讀的情況是在讀取執行個體變量時,此值已經被其他線程更改過了。

建立t3項目,publicvar.java檔案代碼如下:

程式運作後的結果如圖2-11所示。

《Java多線程程式設計核心技術》——2.1節synchronized同步方法

出現髒讀是因為public void getvalue()方法并不是同步的,是以可以在任意時候進行調用。解決辦法當然就是加上同步synchronized關鍵字,代碼如下:

程式運作後的結果如圖2-12所示。

《Java多線程程式設計核心技術》——2.1節synchronized同步方法

可見,方法setvalue()和getvalue()被依次執行。通過這個案例不僅要知道髒讀是通過synchronized關鍵字解決的,還要知道如下内容:

當a線程調用anyobject對象加入synchronized關鍵字的x方法時,a線程就獲得了x方法鎖,更準确地講,是獲得了對象的鎖,是以其他線程必須等a線程執行完畢才可以調用x方法,但b線程可以随意調用其他的非synchronized同步方法。

當a線程調用anyobject對象加入synchronized關鍵字的x方法時,a線程就獲得了x方法所在對象的鎖,是以其他線程必須等a線程執行完畢才可以調用x方法,而b線程如果調用聲明了synchronized關鍵字的非x方法時,必須等a線程将x方法執行完,也就是釋放對象鎖後才可以調用。這時a線程已經執行了一個完整的任務,也就是說username和password這兩個執行個體變量已經同時被指派,不存在髒讀的基本環境。

髒讀一定會出現操作執行個體變量的情況下,這就是不同線程“争搶”執行個體變量的結果。

2.1.6 synchronized鎖重入

關鍵字synchronized擁有鎖重入的功能,也就是在使用synchronized時,當一個線程得到一個對象鎖後,再次請求此對象鎖時是可以再次得到該對象的鎖的。這也證明在一個synchronized方法/塊的内部調用本類的其他synchronized方法/塊時,是永遠可以得到鎖的。

建立實驗用的項目synlockin_1,類service.java代碼如下:

程式運作結果如圖2-13所示。

《Java多線程程式設計核心技術》——2.1節synchronized同步方法

“可重入鎖”的概念是:自己可以再次擷取自己的内部鎖。比如有1條線程獲得了某個對象的鎖,此時這個對象鎖還沒有釋放,當其再次想要擷取這個對象的鎖的時候還是可以擷取的,如果不可鎖重入的話,就會造成死鎖。

可重入鎖也支援在父子類繼承的環境中。

建立實驗用的項目synlockin_2,類main.java代碼如下:

程式運作後的效果如圖2-14所示。

《Java多線程程式設計核心技術》——2.1節synchronized同步方法

此實驗說明,當存在父子類繼承關系時,子類是完全可以通過“可重入鎖”調用父類的同步方法的。

2.1.7 出現異常,鎖自動釋放

當一個線程執行的代碼出現異常時,其所持有的鎖會自動釋放。

建立實驗用的項目throwexceptionnolock,類service.java代碼如下:

兩個自定義線程代碼如圖2-15所示。

《Java多線程程式設計核心技術》——2.1節synchronized同步方法

運作類run.java代碼如下:

程式運作後的效果如圖2-16所示。

《Java多線程程式設計核心技術》——2.1節synchronized同步方法

線程a出現異常并釋放鎖,線程b進入方法正常列印,實驗的結論就是出現異常的鎖被自動釋放了。

2.1.8 同步不具有繼承性

同步不可以繼承。

建立測試用的項目synnotextends,類main.java代碼如下:

類mythreada.java和mythreadb.java代碼如圖2-17所示。

《Java多線程程式設計核心技術》——2.1節synchronized同步方法

類test.java代碼如下:

程式運作後的效果如圖2-18所示。

《Java多線程程式設計核心技術》——2.1節synchronized同步方法