在構建穩健的并發程序時,必須正確地使用線程和鎖,但這些終歸只是一些機制。要編寫線程安全的代碼,其核心在于要對狀態訪問操作進行管理,特別是對共享的(Shared)和可變的(Mutable)狀態的訪問。
從非正式的意義上說,對象的狀態是指存儲在狀態變量(例如實例或靜態域)中的數據。對象的狀態可能包括其他依賴對象的域,比如某個HashMap的狀態不僅存儲在HashMap對象本身,還存儲在許多Map.Entry對象中。
共享 意味著變量可以由多個線程同時訪問 可變 意味著變量的值在其生命周期內可以發生變化
一個對象是否需要是線程安全的,取決于它是否被多個線程訪問。
當多個線程訪問某個狀態變量并且其中有一個線程執行寫入操作(說明是是共享 可變的)時,必須采用同步機制來協同這些線程對變量的訪問。(保證線程安全性)
java中主要的同步機制:
synchronizedvolatile類型的變量顯式鎖(Explicit Lock)原子變量變量為線程安全的方法組合:
不共享共享+不可變共享+可變+同步程序狀態的封裝性越好,就越容易實現程序的線程安全性,并且代碼的維護人員也越容易保持這種方式。
線程安全的程序不一定完全由線程安全類構成。(可以有非線程安全類,然后在程序中增加同步措施) 完全由線程安全類構成的程序并不一定就是線程安全的。(兩個線程安全類不同鎖,構成的程序不能保證原子性) 線程安全類中也可以包含非線程安全的類(同上,只要再增加同步措施即可)
線程安全性定義:當多個線程訪問某個類時,不管運行時環境采用何種調度方式或者這些線程如何交替執行,并且在主調代碼中不需要任何額外的同步或協同,這個類始終都能表現出正確的行為,那么就稱這個類是線程安全的。正確性的含義是,某個類的行為與其規范一致。(如果你覺得‘正確性’的定義有些模糊,那么可以將線程安全類認為是一個在并發環境和單線程環境中都不會被破壞的類)
通常,線程安全性的需求并非來源于對線程的直接使用,而是使用像Servlet這樣的框架。
無狀態對象一定是線程安全的。
競態條件(Race Condition):由于不恰當的執行時序而出現了不正確的結果(出現這種狀況則不是線程安全的,因為違反了線程安全性的定義)。當某個計算的正確性取決于多個線程的交替執行時序時,那么就會發生競態條件,換句話說,就是正確的結果取決于運氣。
數據競爭(Data Race):如果在訪問共享的非final類型的域(共享 可變)時沒有采用同步來進行協同,那么就會出現數據競爭。在java內存模型中,如果在代碼中存在數據競爭,那么這段代碼就沒有確定的語義。
并非所有的競態條件都是數據競爭,同樣并非所有的數據競爭都是競態條件。???
競態條件的類型:
讀取-修改-寫入(++count 操作并非原子,結果狀態依賴于之前的狀態)先檢查后執行(Check-Then-Act,通過一個可能失效的觀測結果來做出判斷或者執行某個計算)使用“先檢查后執行”的一種常見情況就是延遲初始化。延遲初始化的母的:
將對象的初始化操作推遲到實際被使用時才進行確保只被初始化一次。與大多數并發錯誤一樣,競態條件并不總是產生錯誤,還需要某種不恰當的執行時序。
要避免競態條件問題,就必須在某個線程修改該變量時,通過某種方式防止其他線程使用這個變量。也就是說必須原子操作來避免產生競態條件。
原子操作:對于訪問同一個狀態的所有操作(包括該操作本身)來說,這個操作是 一個 以原子方式執行的操作。
如果++count是一個原子操作,那么競態條件就不會發生。 使++count不會發生競態條件的方法
加鎖,確保原子性使用線程安全類,將count聲明為AtomicLong類型當在無狀態的類中添加一個狀態時,如果該狀態完全由線程安全的對象來管理,那么這個類仍然是線程安全的。(0 –> 1 當0/1變多時,并不是這么簡單)
當一個類引入了多個狀態變量時,狀態變量之間可能不是彼此獨立的,而是某個變量的值會對其他變量的值產生約束,這時,要保持狀態的一致性(也就是保證線程安全),就需要在單個原子操作中更新所有相關的狀態變量。
同步機制的兩個重要方面:
原子性可見性同步代碼塊包含兩部分:
作為鎖的對象引用作為由這個鎖保護的代碼塊每個java對象都可以用作一個實現同步的鎖,這些鎖被稱為內置鎖(Intrinsic Lock)或者監視器鎖(Monitor Lock)。線程在進入同步代碼塊之前會自動獲得鎖,并且在退出同步代碼塊時自動釋放鎖,而無論是通過正常的控制路徑退出,還是通過從代碼塊中拋出異常退出。獲得內置鎖的唯一途徑就是進入由這個鎖保護的同步代碼塊或方法。
java的內置鎖相當于一種互斥體(或互斥鎖 mutex),這意味著最多只有一個線程能夠持有這種鎖。
并發環境中的原子性與實務應用程序中的原子性有著相同的含義———一組語句作為一個不可分割的單元被執行。
內置鎖是可重入的(reentrant),也就是說如果某個線程試圖獲得一個已經由他自己持有的鎖,那么這個請求就會成功。“重入”意味著獲取鎖的操作粒度是“線程”,而不是“調用”(這與pthread(POSIX線程)互斥體的默認加鎖行為不同,pthread互斥體的獲取操作是以“調用”為粒度的)。
重入進一步提升了加鎖行為的封裝性。在java中子類改寫父類synchronized方法,然后在其中調用父類的方法,如果沒有可重入的鎖,那么這段代碼將產生死鎖。
對于可能被多個線程同時訪問的可變狀態變量,在訪問它時都需要持有同一個鎖,在這種情況下,我們稱狀態變量是由這個鎖保護的。
一種常見的加鎖約定是,將所有的可變狀態都封裝在對象內部,并通過對象的內置鎖對所有訪問可變狀態的代碼路徑進行同步,使得在該對象上不會發生并發訪問。
對于包含多個變量的不變性條件,其中涉及的所有變量都需要由同一個鎖來保護。
如果同步可以避免競態條件的問題,那么為什么不在每個方法聲明時都是用關鍵字synchronized?
如果不加區別的濫用synchronized,可能導致程序中出現過多的同步如果只是將每個方法都作為同步方法,那么并不足以確保Vector上復合操作都是原子的,比如在程序代碼中使用Vector:if(!vector.contains(element)) vector.add(element);contains和add方法均為syn方法,但是上面這段代碼為先檢查后執行,存在競態條件,需要將這兩個操作合并為復合操作。
不良并發(Poor concurrency)應用程序:可同時調用的數量不僅受到可用處理資源的限制,還受到應用程序本身結構的限制。
縮小同步代碼塊的作用范圍,可以確保程序的并發性,同時又維護線程安全性。但是,如果將同步代碼塊分解的過細,那么在獲取鎖與釋放鎖等操作上都需要一定的開銷。
當執行時間較長的計算或者可能無法快速完成的操作時(例如,網絡I/O或者控制臺I/O),一定不要持有鎖。
新聞熱點
疑難解答