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鎖雖好,但是不能多用,會影響執行效率,阻塞隊列的線程也會不斷地嘗試獲取鎖,消耗性能。更多關於synchronized和volatile的用法這裏就不展開來說了,可以baidu一下詳細的用法。
本文參考了http://www.iteye.com/topic/806990,比較容易理解,感謝大神。
Java線程安全