1. 程式人生 > >Java多執行緒---多執行緒安全問題

Java多執行緒---多執行緒安全問題

   上一次我們說到在賣票問題中如果不將總票數設定為static靜態變數,就會出現錯票,

    即同樣一張票會出售多次。

    在今天的問題中,我們繼續通過賣票問題來進行研究。

    我們在每一個執行緒進行判斷條件後讓執行緒睡眠一段時間(讓判斷條件與資料操作之間相隔一段時間),看看會有什麼效果?

    

執行結果:                                                                                   

通過執行結果我們可以發現有出售第0張票和第-1張票的情況 ,這是為什麼呢?

是因為當有多個執行緒對一個共享資料進行操作時,可能會出現多執行緒的安全問題

這也是我們今天研究的主題。

當遇到這樣的多執行緒安全問題時,我們可以通過synchronized關鍵字將它解決。

synchronized是Java中的關鍵字,是一種同步鎖。它修飾的物件有以下幾種:
1. 修飾一個程式碼塊,被修飾的程式碼塊稱為同步語句塊,其作用的範圍是大括號{}括起來的程式碼,作用的物件是呼叫這個程式碼塊的物件;
2. 修飾一個方法,被修飾的方法稱為同步方法,其作用的範圍是整個方法,作用的物件是呼叫這個方法的物件;
3. 修改一個靜態的方法,其作用的範圍是整個靜態方法,作用的物件是這個類的所有物件;
4. 修改一個類,其作用的範圍是synchronized後面括號括起來的部分,作用主的物件是這個類的所有物件。

我們用synchronized關鍵字修飾一個程式碼塊,將if判斷語句“包”起來--->放入程式碼塊中,再次執行

結果如下:

我們可以看出,沒有再發生出售第0,-1張票的情況。

這是因為我們使用了synchronized關鍵字,使得一個執行緒在呼叫此同步鎖的時候,將物件鎖鎖住(擁有物件鎖)

注意,這裡鎖的是作為物件鎖的物件,並不是程式碼塊,切記切記......

執行緒擁有物件鎖即只有該執行緒在執行完程式碼塊中的內容後,將物件鎖釋放,其它執行緒才能擁有此物件鎖,執行程式碼快中的內容,

若該執行緒沒有將物件鎖釋放,則其它執行緒便只能等待,等到該執行緒執行完程式碼塊中內容後將鎖釋放,然後其它執行緒擁有此物件鎖,進而執行程式碼快中的內容。

 

若沒有synchronized關鍵字,則假設此時總票數ticket為1,A執行緒此時獲得CPU執行權,通過判斷ticket=1 > 0,進入if語句,然後睡眠。(此時ticket沒有減1,仍為1)

A執行緒進入睡眠後,B執行緒獲得CPU執行權,通過判斷ticket=1 > 0,進入if語句,然後睡眠。

A執行緒甦醒後,進行ticket--操作,此時ticke輸出t為1,之後ticket=0,通過判斷ticket=0 > 0失敗,不執行if語句,進而退出while迴圈,執行完畢。

B執行緒甦醒後,此時ticket為不為1,為0,則ticket進行--操作後,輸出為0,之後ticket=-1,通過判斷ticket= -1 > 0失敗,不執行if語句,進而退出while迴圈,執行完畢。

若程式主執行緒中還定義了第三個執行緒,則如執行緒B,輸出為-1 ----->這就是出現出售第0,-1張票情況的原因。

 

若加了synchronized程式碼塊,則,A執行緒在執行完程式碼塊中的內容後(進行ticket-1操作後),

其它執行緒才能根據ticket進行判斷,若不滿足條件,就會結束迴圈,執行完畢。就不會發生上述出售第0,-1張票的情況

從而解決多個執行緒對一個共享資料進行操作時出現的安全問題......

 

接下來我們使用兩個執行緒分別呼叫同步程式碼塊和同步方法

 

執行結果仍會有出售第0張票的情況,,小夥伴們可以自己試一下。

這是因為兩個執行緒的物件鎖是不同的,A執行緒的對像鎖是obj,而B執行緒的對像鎖是this,

(這裡需要注意的是,B執行緒呼叫了同步方法【同步方法的對像鎖是this】

【靜態同步方法的物件鎖是當前類的位元組碼檔案物件】,定義方法--->【類.class | 物件.getClass()】

即A執行緒雖然將它的物件鎖(obj)鎖住,但並不影響B執行緒擁有它自己的對像鎖(this),

它們的對像鎖是不同的,結果很明顯--->這樣並不能解決多執行緒安全問題

當我們將同步程式碼塊的對像鎖設定為this時,兩個執行緒的對像鎖相同,便能解決多執行緒安全問題

使用靜態同步方法與同步方法類似,將同步程式碼塊的對像鎖設定為  類.class  或者  物件.getClass(),也能解決多執行緒安全問題。

 

最後再說兩個相關知識點: 

同步方法與普通方法可以同時呼叫

synchronized獲得的鎖是可重入的

一個同步方法可以呼叫另外一個同步方法,一個執行緒已經擁有某個物件的鎖,再次申請的時候仍然會得到該物件的鎖.

【具體理解為:在使用synchronized時,當一個執行緒得到一個物件鎖後,再次請求此物件鎖時,是可以再次得到該物件的鎖的。

                         也就是說在一個synchronized方法或塊的內部呼叫本類的其他synchronized方法或塊時,是永遠可以得到鎖的。】

這裡需要注意的是,子類的同步方法中也可以呼叫父類的同步方法(通過super關鍵字)。