2018年拍攝于日本奈良的小鹿,鹿随便摸,手感棒棒的。
王皓的github:https://github.com/tenaciousdwang
上回說到線程的生命周期,今天接着說一下并發程式設計中肯定會遇到的線程安全問題,線程安全問題隻出現在多線程環境,單線程串行環境下不會出現這樣的問題,下面引用碼出高效中的例子來說明一下。
醫生坐診,并發處理多個病人的詢問、開化驗單、檢視化驗結果、開藥等工作,任何一個環節一旦出現資料混淆,都可能引發嚴重的醫療事故。
延伸到計算機的線程處理過程中,有可能會出現同時通路同一個資源的情況,這種資源可以是各種類型的的資源:一個變量、一個對象、一個檔案、一個資料庫表等,而當多個線程同時通路同一個資源的時候,就會存在一個問題:
由于每個線程執行的過程是不可控的,是以很可能導緻最終的結果與實際上的願望相違背或者直接導緻程式出錯。
再舉一個例子,在服務端有一個賬戶,裡面有10000元,有a線程執行使用者查詢,并進行扣款100元,有一個b線程執行同樣的扣款100元操作,兩個線程并發,無法保證執行順序,a線程查詢賬戶為10000元後交出cpu執行時間片,b線程開始執行也查詢賬戶為10000元,交出cpu執行時間片後,a線程開始扣款10000元-100元寫入9900元執行完畢,b線程開始扣款10000元-100元寫入9900元執行完畢,這樣賬戶實際扣款200元,但是最後資料卻是9900元,而不是9800元,這就是線程安全問題。
為保證線程安全,在多個線程并發的競争一個共享資源時,通常采用同步機制協調各個線程的執行,以確定得到正确的結果。
保證高并發場景下的線程安全,可以從以下四個次元來說:
1、資料單線程内可見,單線程内總是安全的,資料隻在棧内可見,不共享,就不會被其他線程修改。
2、使用private final關鍵字使其隻讀,隻讀就不可修改,無法繼承,總是安全的。
3、使用線程安全的類,比如stringbuffer,concurrenthashmap線程安全容器等。
4、同步與鎖機制,在java中,提供了兩種方式來實作同步互斥通路:synchronized和lock,線程的核心概念就是“要麼隻讀,要麼加鎖”。
今天主要說一下synchronized關鍵字,同步代碼塊,在了解synchronized關鍵字的使用方法之前,我們先來看一個概念:互斥鎖,即能到達到互斥通路目的的鎖。
舉一個簡單的例子,還是我們上面所說的賬戶問題,如果我們給賬戶這個對象加上互斥鎖,當一個線程進行通路時,另一個線程無法通路,必須等待第一個線程通路完并釋放互斥鎖後,再擷取鎖來通路,就可以避免剛才出現的扣款問題,這個時候當a線程執行完後,賬戶寫入9900,接下來b線程擷取鎖進行通路,9900-100後寫入9800元,為正常預期。
在java中,每一個對象都擁有一個鎖标記(monitor),這是每個對象與生俱來的一個隐藏字段,也稱為螢幕,多線程同時通路某個對象時,線程隻有擷取了該對象的鎖才能通路。
synchronized鎖的特性由jvm負責實作,jvm通過監視monitor來實作同步。方法元資訊中會使用acc_synchronized識别該方法是否是一個同步方法。同時使用monitorenter與monitorexit來擷取和釋放monitor。
使用monitorenter進入時,monitor為0,表示該線程可以繼續持有這個鎖,并繼續後續的代碼,并将monitor加1,如果這個線程再次通路則monitor再加1,如果該對象monitor不是0,則其他通路這個對象的代碼進入阻塞狀态,等待上一個線程的釋放。
接下來我們來說一下synchronized的實作方式,有兩種實作方式:
第一種,在方法簽名處加synchronized關鍵字。
首先我們先來看一下不加synchronized關鍵字的運作效果建立一個synchronizedthread測試線程類。
建立一個測試類,建立并啟動線程。
運作結果為。
我們可以看到,兩個線程是交替插入資料的,現在我們在synchronizedthread類中的insert方法簽名處加synchronized關鍵字,再運作一下。
運作結果。
這時,我們可以看到,線程1運作插入完畢後,線程而才開始插入。synchronized關鍵字起到了鎖定對象的效果。
這裡說一下關于一些使用synchronized方法需要注意的問題,當一個線程正在通路一個對象的synchronized方法,那麼其他線程不能通路該對象的其他synchronized方法。
當一個線程正在通路一個對象的synchronized方法,那麼其他線程能通路該對象的非synchronized方法。
如果一個線程a需要通路對象object1的synchronized方法1,另外一個線程b需要通路對象object2的synchronized方法1,即使object1和object2是同一類型),也不會産生線程安全問題,因為他們通路的是不同的對象,是以不存在互斥問題。
第二種使用synchronized(對象或類){代碼}進行同步,這是一個同步代碼塊,不會鎖住整個方法,隻會鎖住包含的代碼。使用原則是盡量的縮小鎖住的範圍,使鎖的時間盡量短,能用同步代碼塊,就不用同步關鍵字。
我們來改造一下第一個代碼案例中的synchronizedthread類。
其中synchronized(對象或類)裡的對象或類可以new 一個對象,或者使用this表示目前對象的鎖。
這裡的同步代碼塊更加靈活,不會鎖住整個方法,提高執行效率。另外對于靜态同步方法,是與上面的同步方式有所差別的,靜态同步方法使用的是目前對象的class對象。
總結一下就是java中的每一個對象都可以作為鎖。
1、對于同步方法,鎖是目前執行個體對象。
2、對于靜态同步方法,鎖是目前對象的class對象。
3、對于同步方法塊,鎖是synchonized括号裡配置的對象。