Java安全執行緒,java線程
最近做筆試題,遇到了不少關於安全執行緒的題目,比如:
synchronized和volatile的區別是什嗎?
StringBuilder和StringBuffer的區別是什嗎?
HashMap和HashTable的區別是什嗎?等等......
這些問題的答案涉及到的,就是關於安全執行緒問題。首先先要對安全執行緒有個概念,怎樣才叫安全執行緒。
安全執行緒和線程不安全:
安全執行緒指的是多個線程並發執行的時候,當一個線程訪問該類的某個資料的時候,通過加鎖的機制,保護資料,直至當前線程讀取完,釋放鎖後,其他線程才能繼續使用,我們認為這樣是安全執行緒的。
有安全執行緒,自然就有線程不安全,線程不安全指的是,多個線程並發執行的時候,資料沒有得到保護,可能會出現多個線程修改或使用某個資料,導致所得到的資料不正確,也就是髒資料。
瞭解完基本概念後,接下來要引用某大神從Java記憶體模型和線程同步機制方面來描述線程的安全性。
Java記憶體模型
不同的平台,記憶體模型是不一樣的,但是jvm的記憶體模型規範是統一的。其實java的多線程並發問題最終都會反映在java的記憶體模型上,所謂安全執行緒無 非是要控制多個線程對某個資源的有序訪問或修改。總結java的記憶體模型,要解決兩個主要的問題:可見度和有序性。
可見度: 多個線程之間是不能互相傳遞資料通訊的,它們之間的溝通只能通過共用變數來進行。Java記憶體模型(JMM)規定了jvm有主記憶體,主記憶體是多個線程共用 的。當new一個對象的時候,也是被分配在主記憶體中,每個線程都有自己的工作記憶體,工作記憶體儲存了主存的某些對象的副本,當然線程的工作記憶體大小是有限制 的。當線程操作某個對象時,執行順序如下:
(1) 從主存複製變數到當前工作記憶體 (read and load)
(2) 執行代碼,改變共用變數值 (use and assign)
(3) 用工作記憶體資料重新整理主存相關內容 (store and write)
當一個共用變數在多個線程的工作記憶體中都有副本時,如果一個線程修改了這個共用 變數,那麼其他線程應該能夠看到這個被修改後的值,這就是多線程的可見度問題。
有序性:線程在引用變數時不能直接從主記憶體中引用,如果線程工作記憶體中沒有該變數,則會從主記憶體中拷貝一個副本到工作記憶體中,完成後線程會引用該副本。當同一線程再度引用該欄位時,有可能重新從主存中擷取變數副本(read-load-use),也有可能直接引用原來的副本 (use),也就是說 read,load,use順序可以由JVM實現系統決定。
線程不能直接為主存中欄位賦值,它會將值指定給工作記憶體中的變數副本(assign),完成後這個變數副本會同步到主儲存區(store- write),至於何時同步過去,根據JVM實現系統決定.有該欄位,則會從主記憶體中將該欄位賦值到工作記憶體中,這個過程為read-load,完成後線 程會引用該變數副本。
舉個例子1,現有一變數x=10,a線程執行x=x+1操作,b線程執行x=x-1操作,倆線程同時啟動並執行時候,x的值是不確定的,有可能為9,也有可能為11,這就是多線程並發執行的順序是不可預見導致的,所以要使安全執行緒,要保證a線程和b線程的有序執行,且執行的操作必須為原子操作。
原子操作:在多線程訪問共用資源時,能夠確保所有其他的進程(線程)都不在同一時間內訪問相同的資源。原子操作(atomic operation)是不需要synchronized,這是Java多線程編程的老生常談了。所謂原子操作是指不會被線程調度機制打斷的操作;這種操作一旦開始,就一直運行到結束,中間不會有任何 context switch (切換到另一個線程)。
那麼如何確保線程執行的有序性和可見度呢?
synchronized關鍵字
synchronized關鍵字可以解決有序性和可見度的問題,它保證了多個線程之間是互斥的,當一段代碼會修改共用變數,這一段代碼成為互斥區或 臨界區,為了保證共用變數的正確性,synchronized標示了臨界區。
常用用法:
Java代碼
synchronized(lock) {臨界區代碼}//例如:public synchronized void method() {}public static synchronized void method() {}
無論synchronized關鍵字是加在方法還是對象中,都是取對象當作鎖,理論上每個對象都可以是一個鎖。
對於public synchronized void method()這種情況,鎖就是這個方法所在的對象。同理,如果方法是public static synchronized void method(),那麼鎖就是這個方法所在的class。
synchronized關鍵字有兩種鎖對象,一種是對象加鎖,另一種是對類加鎖,對類加鎖,則類鎖對類裡的所有對象都起作用,而對象鎖只是針對該類的一個指定的對象加鎖,這個類的其它對象仍然可以使用已經對前一個對象加鎖的synchronized方法。
例如:車站只剩一張票,A業務員和B業務員同時出票,執行public synchronized void sell()方法,那麼結果就會出現,剩餘票數為(-1)的情況,這就是對象鎖導致的問題,其他對象仍然可以使用這個方法;
當把sell方法加上static後,鎖的對象就是這個類,對於A業務員和B業務員來說,都要按順序來操作業務,這樣就不會出現剩餘票數為負數的輕卡UN個了。
當一個對象是鎖的時候,應該要被多個線程共用才是有意義的。這也得出一個結論:非安全執行緒!=不安全。
比如ArrayList就是線程不安全的,但是並非說多線程情況下就不用ArrayList,如果你的每一個線程都new了一個ArrayList對象,也就是對象是非共用的,線程之間不存在資源競爭,那麼多線程執行的時候也是沒有安全問題的。
每個鎖對象都有兩個隊列,一個是就緒隊列,一個是阻塞隊列,就緒佇列儲存體了將要獲得鎖的線程,阻塞佇列儲存體了被阻塞的線程,當一個線程被喚醒 後,才會進入到就緒隊列,等待cpu的調度。
一個線程執行臨界區代碼過程如下:
1 獲得同步鎖
2 清空工作記憶體
3 從主存拷貝變數副本到工作記憶體
4 對這些變數計算
5 將變數從工作記憶體寫回到主存
6 釋放鎖
可見,synchronized既保證了多線程的並發有序性,又保證了多線程的記憶體可見度。
生產者/消費者模式就是典型的同步鎖問題,多線程執行過程除了操作是互斥的以外,往往也會出現線程之間互相協作的情況。
比方說,A機器人負責造車子,造好了車子,就放在倉庫裡,倉庫每次只能放一輛車。
(1)起初,A機器人被運行,執行造車子make方法,造好了車子放在倉庫後,沒有馬上關機(釋放鎖),而是喚醒了(notify)準備上班的(阻塞隊列)B機器人,自己再下班(進入阻塞隊列)
(2)B機器人獲得同步鎖,上班,開始把造好的車子運出倉庫拿去出售,車子運出去後,它也沒有馬上溜了,而是喚醒了剛下班的A機器人繼續造車子(赤裸裸的剝削,我也要賣車子)。
(3)A機器人發現倉庫的車子被運走了,接到任務就只能繼續開始造車子,造好車子把B機器人叫回來。
(4)B機器人賣車子,叫醒A機器人造車子。
。。。。。。
可以看出,在同步鎖的作用下,造車子,賣車子,造車子,賣車子可以有序的進行,很愉快。
接下來要說的是另外一個關鍵字:volatile
volatile關鍵字
volatile同樣也是Java同步的一種方法,只不過和synchronized鎖相比,稍弱了點,它只能保證多線程執行的可見度,可不能保證有序性。
它的原理是不需要從主記憶體中複製一份副本到工作記憶體,而是直接對主記憶體的資料進行修改,這樣,其他線程都能立馬看到資料的修改。因此,volatile的使用範圍要臂synchronized小,常用於直接賦值的情況,諸如例子1的情況就是不適用的。
最後需要注意的是,synchronized鎖雖好,但是不能多用,會影響執行效率,阻塞隊列的線程也會不斷地嘗試擷取鎖,消耗效能。