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