1. 程式人生 > >java併發安全解析

java併發安全解析

併發安全

類的執行緒安全定義:
如果多執行緒下使用這個類,不管多執行緒如何使用和排程這個類,這個類總是表示出正確的行為,這個類就是執行緒安全的。

類的執行緒安全表現為:
	1,操作的原子性
	2,記憶體的可見性

不做正確的同步,在多個執行緒之間共享狀態的時候,就會出現執行緒不安全。

怎麼才能做到類的執行緒安全?

1,棧封閉:
所有的變數都是在方法內部宣告的,這些變數都處於棧封閉狀態,都是執行緒安全的。

2,無狀態:
沒有任何成員變數的類,就叫無狀態的類。

3,讓類不可變:

	讓狀態不可變,兩種方式:
		1,加final關鍵字,對於一個類,所有的成員變數應該是私有的,同樣的只要有可能,所有的成員變數應該加上final關鍵字,但是加上final,要注意如果成員變數又是一個物件時,這個物件所對應的類也要是不可變,才能保證整個類是不可變的。
		2、根本就不提供任何可供修改成員變數的地方,同時成員變數也不作為方法的返回值。

4,volatile:
保證類的可見性,最適合一個執行緒寫,多個執行緒讀的情景。

5,加鎖和CAS。

6,安全的釋出:
類中持有的成員變數,特別是物件的引用,如果這個成員物件不是執行緒安全的,通過get等方法釋出出去,會造成這個成員物件本身持有的資料在多執行緒下不正確的修改,從而造成整個類執行緒不安全的問題。

Servlet:
	不是執行緒安全的類,為什麼我們平時沒感覺到:
		1、在需求上,很少有共享的需求。
		2,接收到了請求,返回應答的時候,都是由一個執行緒來負責的。

執行緒安全問題

死鎖

資源一定是多於1個,同時小於等於競爭的執行緒數,資源只有一個,只會產生激烈的競爭。

死鎖的根本成因:獲取鎖的順序不一致導致。
有n個程序,共享的同類資源數為m,則避免死鎖的最少資源數是n(m-1)+1。

		產生死鎖的原因:
			1,因為系統資源不足
			2,程序執行推進的順序不合適
			3,資源分配不當等
			
		產生死鎖的四個必要條件:
			1,互斥條件:一個資源每次只能被一個程序使用
			2,請求與保持條件:一個程序因請求字眼而阻塞時,對已獲得的資源保持不放
			3,不可剝奪條件:程序已獲得的資源,在未使用完之前,不能強行剝奪
			4,迴圈等待條件:若干程序之間形成一種頭尾相接的迴圈等到資源關係
			
		懷疑傳送死鎖:
			簡單的死鎖:
				1,通過jps 查詢應用的 id,
				2,再通過jstack id 檢視應用的鎖的持有情況
				解決辦法:保證加鎖的順序性
				
			動態的死鎖:
				動態順序死鎖,在實現時按照某種順序加鎖了,但是因為外部呼叫的問題,導致無法保證加鎖順序而產生的。
				例如:
					void test(Account fromAccount,Account toAccount){
						synchronized (fromAccount) {
					        synchronized (toAccount) {
					        }
					    }
					}
				雖然內部固定了加鎖的順序,但是外部傳入的鎖的順序不一樣,所以可能造成死鎖。
				
				解決:
				1、	通過內在排序,保證加鎖的順序性(呼叫System.identityHashCode(obj)獲取原始的hashcode值,再排序確定順序,如果hash值一樣了,就在外部再定義一個鎖,讓這兩個執行緒去競爭一次這個鎖,誰拿到鎖再進入依次獲取之前兩個鎖)
				2、	通過嘗試拿鎖,也可以,在傳入的物件內部定義顯示鎖,然後自旋,依次呼叫tryAcquired,嘗試拿鎖,拿不到就自旋再獲取:
				
						while(true) {
				    		if(from.getLock().tryLock()) {
				    			try {
				    				if(to.getLock().tryLock()) {
				    					try {
				    					}finally {
				    						to.getLock().unlock();
				    					}
				    				}
				    			}finally {
				    				from.getLock().unlock();
				    			}
				    		}
				    		SleepTools.ms(r.nextInt(10));//休眠一個隨機數,否則會發生活鎖。
				    	}
				    }
				    
				3、 銀行家演算法是一種最有代表性的避免死鎖的演算法。又被稱為“資源分配拒絕”法。在避免死鎖方法中允許程序動態地申請資源,但系統在進行資源分配之前,應先計算此次分配資源的安全性,若分配不會導致系統進入不安全狀態,則分配,否則等待。為實現銀行家演算法,系統必須設定若干資料結構。

活鎖

嘗試拿鎖的機制中,發生多個執行緒之間互相謙讓,不斷髮生拿鎖,釋放鎖的過程,比如執行緒1嘗試拿到A鎖,嘗試拿B鎖失敗,執行緒2嘗試拿到B鎖,嘗試拿A鎖失敗,這樣執行緒1和執行緒2都迴圈再次嘗試獲取兩個鎖,這時候又是之前的情況。

解決辦法:每個執行緒休眠隨機數,錯開拿鎖的時間。

執行緒飢餓

低優先順序的執行緒,總是拿不到執行時間。

效能和思考

使用併發的目標是為了提高效能,引入多執行緒後,其實會引入額外的開銷,如執行緒之間的協調、增加的上下文切換,執行緒的建立和銷燬,執行緒的排程等等。過度的使用和不恰當的使用,會導致多執行緒程式甚至比單執行緒還要低。

衡量應用的程式的效能:服務時間,延遲時間,吞吐量,可伸縮性等等,其中服務時間,延遲時間(多快),吞吐量(處理能力的指標,完成工作的多少)。多快和多少,完全獨立,甚至是相互矛盾的。

對伺服器應用來說:多少(可伸縮性,吞吐量)這個方面比多快更受重視。

	我們做應用的時候:
		1、	先保證程式正確,確實達不到要求的時候,再提高速度。(黃金原則)
		2、	一定要以測試為基準。
	一個應用程式裡,序列的部分是永遠都有的。
	Amdahl定律  :  1/(F+(1-N)/N)   F:必須被序列部分,程式最好的結果, 1/F。

影響效能的因素

		1,上下文切換:
			是指CPU 從一個程序或執行緒切換到另一個程序或執行緒。一次上下文切換花費5000~10000個時鐘週期,幾微秒。在上下文切換過程中,CPU會停止處理當前執行的程式,並儲存當前程式執行的具體位置以便之後繼續執行。從這個角度來看,上下文切換有點像我們同時閱讀幾本書,在來回切換書本的同時我們需要記住每本書當前讀到的頁碼。
			上下文切換通常是計算密集型的。也就是說,它需要相當可觀的處理器時間。所以,上下文切換對系統來說意味著消耗大量的 CPU 時間,事實上,可能是作業系統中時間消耗最大的操作。
			
		2,記憶體同步:
			一般指加鎖,對加鎖來說,需要增加額外的指令,這些指令都需要重新整理快取等等操作。
			
		3,阻塞:
			會導致執行緒掛起【掛起:掛起程序在作業系統中可以定義為暫時被淘汰出記憶體的程序,機器的資源是有限的,在資源不足的情況下,作業系統對在記憶體中的程式進行合理的安排,其中有的程序被暫時調離出記憶體,當條件允許的時候,會被作業系統再次調回記憶體,重新進入等待被執行的狀態即就緒態,系統在超過一定的時間沒有任何動作】。很明顯這個操作包括兩次額外的上下文切換。

優化效能

		1,減少鎖的競爭
		2,減少鎖的粒度:
			使用鎖的時候,鎖所保護的物件是多個,當這些多個物件其實是獨立變化的時候,不如用多個鎖來一一保護這些物件。但是如果有同時要持有多個鎖的業務方法,要注意避免發生死鎖
			縮小鎖的範圍
			對鎖的持有實現快進快出,儘量縮短持由鎖的的時間。將一些與鎖無關的程式碼移出鎖的範圍,特別是一些耗時,可能阻塞的操作
		3,避免多餘的縮減鎖的範圍:
			兩次加鎖之間的語句非常簡單,導致加鎖的時間比執行這些語句還長,這個時候應該進行鎖粗化—擴大鎖的範圍。
		4,鎖分段:
			ConcurrrentHashMap就是典型的鎖分段。
		5,替換獨佔鎖:
			在業務允許的情況下:
				1、	使用讀寫鎖,
				2、	用自旋CAS
				3、	使用系統的併發容器