臟讀
一個常見的概念。在多線程中,難免會出現在多個線程中對同一個對象的實例變量進行并發訪問的情況,如果不做正確的同步處理,那么產生的后果就是"臟讀",也就是取到的數據其實是被更改過的。
?
?
?
按照正常來看應該打印"a num = 100"和"b num = 200"才對,現在卻打印了"b num = 200"和"a num = 200",這就是線程安全問題。我們可以想一下是怎么會有線程安全的問題的:
1、mt0先運行,給num賦值100,然后打印出"a set over!",開始睡覺
2、mt0在睡覺的時候,mt1運行了,給num賦值200,然后打印出"b set over!",然后打印"b num = 200"
3、mt1睡完覺了,由于mt0的num和mt1的num是同一個num,所以mt1把num改為了200了,mt0也沒辦法,對于它來說,num只能是100,mt0繼續運行代碼,打印出"a num = 200"
分析了產生問題的原因,解決就很簡單了,給addNum(String userName)方法加同步即可:
?
打印結果的方式變了,打印的順序是交叉的,這又是為什么呢?
這里有一個重要的概念。關鍵字synchronized取得的鎖都是對象鎖,而不是把一段代碼或方法(函數)當作鎖,哪個線程先執行帶synchronized關鍵字的方法,哪個線程就持有該方法所屬對象的鎖,其他線程都只能呈等待狀態。但是這有個前提:既然鎖叫做對象鎖,那么勢必和對象相關,所以多個線程訪問的必須是同一個對象。
如果多個線程訪問的是多個對象,那么Java虛擬機就會創建多個鎖,就像上面的例子一樣,創建了兩個ThreadDomain13對象,就產生了2個鎖。既然兩個線程持有的是不同的鎖,自然不會受到"等待釋放鎖"這一行為的制約,可以分別運行addNum(String userName)中的代碼。
?
?
synchronized方法與鎖對象
?
?
從結果看到,第一個線程調用了實體類的methodA()方法,第二個線程完全可以調用實體類的methodB()方法。但是我們把methodB()方法改為同步就不一樣了,就不列修改之后的代碼了,看一下運行結果:
?
?
從這個例子我們得出兩個重要結論:
1、A線程持有Object對象的Lock鎖,B線程可以以異步方式調用Object對象中的非synchronized類型的方法
2、A線程持有Object對象的Lock鎖,B線程如果在這時調用Object對象中的synchronized類型的方法則需要等待,也就是同步
?
synchronized鎖重入
關鍵字synchronized擁有鎖重入的功能。所謂鎖重入的意思就是:當一個線程得到一個對象鎖后,再次請求此對象鎖時時可以再次得到該對象的鎖。看一個例子:
看到可以直接調用ThreadDomain16中的打印語句,這證明了對象可以再次獲取自己的內部鎖。這種鎖重入的機制,也支持在父子類繼承的環境中。
?
?
異常自動釋放鎖
最后一個知識點是異常。當一個線程執行的代碼出現異常時,其所持有的鎖會自動釋放。模擬的是把一個long型數作為除數,從MAX_VALUE開始遞減,直至減為0,從而產生ArithmeticException。看一下例子:
因為打印結果是靜態的,所以不是很明顯。在l--前一句加上Thread.sleep(1)結論會更明顯,第一句打出來之后,整個程序都停住了,直到Thread-0拋出異常后,Thread-1才可以運行,這也證明了我們的結論。
?
?