併發-Java併發程式設計基礎
Java併發程式設計基礎
併發
在電腦科學中,併發是指將一個程式,演算法劃分為若干個邏輯組成部分,這些部分可以以任何順序進行執行,但與最終順序執行的結果一致。併發可以在多核作業系統上顯著的提高程式執行速度。
併發與並行聯絡與區別
這裡參考ErLang之父的解釋,ErLang之父談到了如何向一個5歲小孩解釋併發與並行。 [](
直觀來講,併發是兩個等待佇列中的人同時去競爭一臺咖啡機,兩佇列中的排隊者也可能約定交替使用咖啡機,也可能是大家同時競爭咖啡機,誰先競爭到咖啡機誰使用,不過後一種的方法可能引發衝突,因為兩個佇列裡面排在佇列首位的人可能同時使用咖啡機),每個等待者在使用咖啡機之前不僅需要知道排在他前面那個人是否已經使用完了咖啡機,還需知道另一個佇列中排在首位的人是否也正準備使用咖啡機;而並行是每個佇列擁有自己的咖啡機,兩個佇列之間並沒有競爭的關係,佇列中的某個排隊者只需等待佇列前面的人使用完咖啡機,然後再輪到自己使用咖啡機。
因此,併發意味著多個執行實體(比方說上面例子中的人)可能需要競爭資源(咖啡機),因此就不可避免帶來競爭和同步的問題;而並行則是不同的執行實體擁有各自的資源,相互之間可能互不干擾。
為什麼我們非常熱衷於併發程式設計呢,其實有一個很簡單的原因,那就是我們希望自己的程式更快!然後,併發程式設計卻是一個複雜的主題,如果不能對併發有一個深刻的理解,很多時候會適得其反,程式不但沒有加快,很可能跑的更慢(比如單核作業系統上跑多個執行緒,多個執行緒對某個資源有激烈的競爭等等),更有甚者,還會帶來很多併發產生問題。
執行緒無處不在
在我們現今的大規模分散式系統下,處處充滿了多執行緒,如何良好的應用多執行緒技術成為了現在越來越大的挑戰.
多執行緒的優勢
- 發揮處理器的強大能力(尤其在CPU核數高的處理器上).
- 建模的簡單性(每個執行緒處理各自的任務).
- 非同步事件的簡化處理.
- 響應更靈敏的使用者介面
多執行緒帶來的風險
執行緒安全性問題. 安全性:永遠不會發生糟糕的事情.多執行緒訪問共享變數在未良好同步的情況下,產生共享變數的執行緒安全問題.
活躍性問題. 活躍性:某件正確的事情最終會發生。 活躍性的問題:死鎖,飢餓,活鎖.
效能問題. 效能問題:服務時間過長,響應不靈敏,吞吐率過低,資源消耗過高,可伸縮性低. 例:執行緒上下文切換帶來的CPU資源排程問題.
執行緒安全
定義:當多個執行緒訪問某個類時,不管執行時環境採用何種排程方式或者這些執行緒如何交替執行,並且在主調程式碼中不需要任何額外的同步或協同,這個類都能表現出正確的行為,那麼就稱這個類是執行緒安全的。(線上程安全類中封裝了必要的同步操作,因此客戶端不需要進一步採取同步措施)
重入:當某個執行緒請求一個由其他執行緒持有的鎖時,發出請求的執行緒就會阻塞,然後由於內建鎖是可重入的,因此如果某個執行緒試圖獲取一個已經由自己持有的鎖的時候,那麼這個請求就會成功,重入意味著獲取鎖的粒度是執行緒,而不是呼叫。
同步主要作用: 1.執行緒之前可以互斥的訪問臨界資源。 2.保證執行緒的順序性。 3.保證共享變數的可見性。
競態條件
競態條件:由於多執行緒不恰當的執行時序而出現不正確的結果.最常見的競態條件型別就是"先檢查後執行"操作,即通過一個可能失效的觀察結果來決定下一步的動作。與大多數併發錯誤一樣,競態條件並不總是會產生錯誤,還需要某種不恰當的執行時序。
物件共享
編寫正確併發程式的關鍵在於:在訪問共享的可變狀態時需要進行正確的管理。
可見性
可見性是一種複雜的屬性,在單執行緒環境下,向某個共享變數寫入值後然後再去讀,會得到剛剛寫入的值。但如果是是在多執行緒環境下,就不一定,執行緒寫入共享變數之後,另一個執行緒再去讀,並不一定會讀到第一個執行緒寫入的值。為了保證可見性問題,我們要正確的使用同步。
java記憶體模型要求,變數的讀取操作和寫入操作都必須是原子操作,但對於非volatile型別的long和double變數,JVM允許將64位的讀操作或寫操作分解為兩個32位的操作。在多執行緒程式中使用共享且可變的long和double等型別的變數也是不安全的,除非使用同步機制將變數保護起來。
可見性與鎖 加鎖的含義不僅僅侷限於互斥行為,還包括記憶體可見性。為了確保所用執行緒都能看到共享變數的最新值,所有執行讀操作或寫操作的執行緒都必須在同一個鎖上同步。
可見性與volatile 當把變數宣告為volatile型別之後,編譯器與執行時都會注意到這個變數是共享的,因此不會將該變數上的操作與其他記憶體操作一起重排序。volatile變數不會被快取在暫存器或者其他處理器不可見的地方,因此在讀取volatile型別的變數時候總會返回最新寫入的值。
volatile 相對synchronized 來說,是一種更輕量級的同步機制。當執行緒A首先寫入一個volatile變數並且執行緒B隨後讀取該變數時,在寫入volatile變數之前對A可見的所有變數的值,在B讀取了volatile變數之後,對B也是可見的。因此,從記憶體可見性的角度來看,寫入volatile變數相當於退出同步程式碼塊,而讀取volatile變數就相當於進入同步程式碼塊。
不變性
如果某個物件在被建立之後其狀態就不能被修改,那麼這個物件就被稱為不可變物件。執行緒安全性是不可變物件的固有屬性之一,他們的不變形條件是由建構函式建立的,只要他們的狀態不改變,那麼這些不變性條件就能得以維持。
當滿足以下條件時,物件才是不可變的。
- 物件建立以後其狀態就不能修改。
- 物件的所有域都是final型別的。
- 物件是正確建立的(this引用沒有逸出).
編寫多執行緒程式時,一些實用的策略
- 執行緒封閉。執行緒封閉的物件只能由一個執行緒擁有,物件被封閉在該執行緒中,並且只能由這個執行緒修改。
- 只讀共享。在沒有額外同步的情況下,共享的只讀物件可以由多個執行緒併發訪問,但任何執行緒都不能修改。
- 執行緒安全共享。執行緒安全的物件在其內部實現同步(封裝執行緒安全性),因此多個執行緒可以通過物件的公有介面來進行訪問而不需要進一步的同步。
關於執行緒上下文的切換
執行緒切換上下文需要消耗1-100微妙