Java線程安全

  最近作筆試題,遇到了很多關於線程安全的題目,好比:java

  synchronized和volatile的區別是什麼?編程

  StringBuilder和StringBuffer的區別是什麼?安全

  HashMap和HashTable的區別是什麼?等等......多線程

  這些問題的答案涉及到的,就是關於線程安全問題。首先先要對線程安全有個概念,怎樣才叫線程安全。併發

線程安全和線程不安全:

  線程安全指的是多個線程併發執行的時候,當一個線程訪問該類的某個數據的時候,經過加鎖的機制,保護數據,直至當前線程讀取完,釋放鎖後,其餘線程才能繼續使用,咱們認爲這樣是線程安全的。jvm

  有線程安全,天然就有線程不安全,線程不安全指的是,多個線程併發執行的時候,數據沒有獲得保護,可能會出現多個線程修改或使用某個數據,致使所獲得的數據不正確,也就是髒數據。性能

 

  瞭解完基本概念後,接下來要引用某大神從Java內存模型和線程同步機制方面來描述線程的安全性。ui

Java內存模型

  不一樣的平臺,內存模型是不同的,可是jvm的內存模型規範是統一的。其實java的多線程併發問題最終都會反映在java的內存模型上,所謂線程安全無 非是要控制多個線程對某個資源的有序訪問或修改。總結java的內存模型,要解決兩個主要的問題:可見性和有序性。 atom

  可見性: 多個線程之間是不能互相傳遞數據通訊的,它們之間的溝通只能經過共享變量來進行。Java內存模型(JMM)規定了jvm有主內存,主內存是多個線程共享 的。當new一個對象的時候,也是被分配在主內存中,每一個線程都有本身的工做內存,工做內存存儲了主存的某些對象的副本,固然線程的工做內存大小是有限制 的。當線程操做某個對象時,執行順序以下:spa

 (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,比較容易理解,感謝大神。

相關文章
相關標籤/搜索