- 線程安全核心:
對(duì)狀態(tài)訪問的控制,特別是共享(shared)和可變(Mutable)狀態(tài)的訪問
- 共享:
由多個(gè)線程同時(shí)訪問
- 可變:
在變量的生命周期內(nèi)可發(fā)生變化
- 同步:
包括voliatile變量、synchronized、顯示鎖(Explicit Lock)、原子變量
- 如何修復(fù)多個(gè)線程訪問可變狀態(tài)是發(fā)生的錯(cuò)誤:
1.不在線程間共享該變量
2.將狀態(tài)變量變?yōu)椴豢勺兊淖兞?3.在訪問狀態(tài)變量時(shí)使用同步
2.1 什么是線程安全性
- 無狀態(tài)對(duì)象一定是線程安全的
- 線程訪問無狀態(tài)對(duì)象的行為不會(huì)影響其他線程中操作的正確性
@ThreadSafe
public class StatelessFactorizer implements Servlet{
public void service(ServletRequest req,ServletResponse resp){
BigInteger i=extractFromRequest(req);
BigInteger[] factors=factor(i);
encodeIntoResponse(factors,resp);
}
}
這個(gè)例子中StatelessFactorizer就是無狀態(tài)的,他既不包含任何域,也不包含對(duì)其他類中域的引用,。計(jì)算中的臨時(shí)狀態(tài)僅存于線程棧(線程獨(dú)立)上的局部變量表中,并且只能由正在執(zhí)行的線程訪問
2.2 原子性
2.2.1 競(jìng)態(tài)條件
- 競(jìng)態(tài)條件
當(dāng)某個(gè)計(jì)算結(jié)果的正確性取決于多線程交替執(zhí)行的時(shí)序時(shí),就會(huì)發(fā)生競(jìng)態(tài)條件,簡(jiǎn)而言之就是靠運(yùn)氣。
最常見的就是“先檢查后執(zhí)行(Check-Then-Act)”
2.2.2 示例:延遲初始化時(shí)的競(jìng)態(tài)條件
@NotThreadSafe
public class LazyInitRace{
private ExpenciveObject instance=null;
public ExpenciveObject getInstance(){
if(instance==null){
instance=new ExpenciveObject();
}
return instance;
}
}
假設(shè)有兩個(gè)線程A和B同時(shí)訪問getInstance,A看到instance對(duì)象為空創(chuàng)建一個(gè)ExpenciveObject對(duì)象,B線程同樣需要判斷instance對(duì)象是否為空,但是instance是否為空需要取決于不可預(yù)測(cè)的時(shí)序,包括線程的調(diào)度方式,A線程初始化ExpenciveObject并設(shè)置instance所耗費(fèi)的時(shí)間。如果當(dāng)B線程判斷instance為空,則會(huì)新建一個(gè)ExpenciveObject對(duì)象,那么這兩個(gè)線程調(diào)用getInstance時(shí)得到的就是兩個(gè)不同的結(jié)果。
2.2.3 復(fù)合操作
- 避免競(jìng)態(tài)條件
在某個(gè)線程修改變量時(shí),通過某種方式阻止其他線程使用這個(gè)變量。從而確保其他線程只能在修改之前或者修改之后讀取和修改狀態(tài),而不是在修改狀態(tài)的過程中
- 原子操作
對(duì)于訪問同一個(gè)狀態(tài)的所有操作(包括該操作本身),這個(gè)操作是以原子方式執(zhí)行的操作。
假定線程A和B,如果從執(zhí)行A線程的角度來說,當(dāng)另一個(gè)線程B執(zhí)行時(shí),要么將B全部執(zhí)行完,要么完全不執(zhí)行B,那么A和B對(duì)彼此來說都是原子的
@ThreadSafe
public class CountingFactorizer implements Servlet{
private final AtomicLong count=new AtomicLong(0);
public long getCount(){
return count.get();
}
public void service(ServletRequest req,ServletResponse resp){
BigInteger i=extractFromRequest(req);
BigInteger[] factors=factor(i);
count.incrementAndGet();
encodeIntoResponse(factors,resp);
}
}
AtomicLong是一個(gè)原子變量類,在java.concurrent.atomic包中,該包還含有其他一些原子變量類,用于實(shí)現(xiàn)在數(shù)值和對(duì)象引用上的原子狀態(tài)轉(zhuǎn)換,上面代碼用例通過AtomicLong代替long類型計(jì)數(shù)器,能夠確保所有對(duì)計(jì)數(shù)器的訪問都是原子的,由于上面代碼Servlet狀態(tài)就是計(jì)數(shù)器的狀態(tài),并且計(jì)數(shù)器是線程安全的,因此Servlet也是線程安全的
2.3 加鎖操作
- 對(duì)多個(gè)線程安全狀態(tài)變量操作,并不能保證線程安全。要保持狀態(tài)的一致性,就需要在單個(gè)原子操作中更新所有相關(guān)的狀態(tài)變量。
@NotThreadSafe
public class UnsafeCachingFactorizer implements Servlet{
private final AtomicReference<BigInteger> lastNumber=new AtomicReference<BigInteger>();
private final AtomicReference<BigInteger[]> lastFactors=new AtomicReference<>();
public void service(ServletRequest req,ServletResponse resp){
BigInteger i=extractFromRequest(req);
if(i.equals(lastNumber.get())){
encodeIntoResponse(factors,resp);
}else{
BigInteger[] factors=factor(i);
lastNumber.set(i);
lastFactors.set(factors);
encodeIntoResponse(factors,resp);
}
}
}
上方代碼中,在使用原子引用的情況下,盡管對(duì)set方法的每次調(diào)用都是原子的,但仍然無法同時(shí)更新lastNumber和lastFactors。如果只修改了其中一個(gè)變量,那么在這兩次修改操作之間,其他線程將發(fā)現(xiàn)不變性條件被破壞了。同樣,也不能保證會(huì)同時(shí)獲取兩個(gè)值:在線程A獲取這兩個(gè)值得過程中,線程B可能修改了它們,這樣線程A發(fā)現(xiàn)不變性條件被破壞了
2.3.1 內(nèi)置鎖
- 同步代碼塊(Synchronized Block稱為內(nèi)置鎖或監(jiān)視器鎖)
- 鎖的對(duì)象引用
- 由這個(gè)鎖保護(hù)的代碼塊(靜態(tài)的synchronized方法以class對(duì)象作為鎖)
- 進(jìn)入同步代碼塊自動(dòng)獲得鎖,退出同步代碼塊(無論是正常退出還是拋出異常)自動(dòng)釋放鎖
- JAVA內(nèi)置鎖相當(dāng)于一種互斥鎖,最多只有一個(gè)線程能持有這種鎖,其他未持有鎖的線程必須等待或阻塞,直到持有這個(gè)鎖的線程釋放鎖,否則將一直等待下去。
- 由這個(gè)鎖保護(hù)的同步代碼塊會(huì)以原子方式執(zhí)行
@ThreadSafe
public class UnsafeCachingFactorizer implements Servlet{
private final AtomicReference<BigInteger> lastNumber=new AtomicReference<BigInteger>();
private final AtomicReference<BigInteger[]> lastFactors=new AtomicReference<>();
public synchronized void service(ServletRequest req,ServletResponse resp){
BigInteger i=extractFromRequest(req);
if(i.equals(lastNumber.get())){
encodeIntoResponse(factors,resp);
}else{
BigInteger[] factors=factor(i);
lastNumber.set(i);
lastFactors.set(factors);
encodeIntoResponse(factors,resp);
}
}
}
上面代碼在service方法上加了synchronized后線程安全了,但是假如多個(gè)請(qǐng)求同時(shí)訪問這個(gè)方法,由于同一時(shí)刻只有一個(gè)線程可以執(zhí)行該方法,會(huì)導(dǎo)致性能很低。
2.3.2 重入
- 內(nèi)置鎖是可重入的
當(dāng)一個(gè)線程試圖獲得一個(gè)已經(jīng)由他自己持有的鎖,這個(gè)請(qǐng)求可以成功。
- 獲取鎖的粒度是“線程”不是“調(diào)用”
- 實(shí)現(xiàn)方式
為每個(gè)鎖關(guān)聯(lián)一個(gè)獲取計(jì)數(shù)值和所有者線程,當(dāng)計(jì)數(shù)值為0則認(rèn)為該鎖沒有被任何線程持有。
當(dāng)線程請(qǐng)求一個(gè)未被持有的鎖時(shí),JVM將記下鎖的持有者,并加計(jì)數(shù)值加設(shè)為1。
如果這個(gè)線程再次獲得鎖,計(jì)數(shù)值遞增。
當(dāng)線程退出同步代碼塊時(shí),計(jì)數(shù)值遞減,當(dāng)計(jì)數(shù)值到0時(shí),這個(gè)鎖將被釋放。
2.4 用鎖來保護(hù)狀態(tài)
- 鎖保護(hù)狀態(tài)
對(duì)于可能被多個(gè)線程訪問的可變狀態(tài)變量,在訪問它時(shí)都需要持有同一個(gè)鎖。
- 加鎖約定
將所有的可變狀態(tài)變量(前提是需要被多個(gè)線程同時(shí)訪問)都封裝在對(duì)象內(nèi)部,并通過對(duì)象的內(nèi)置鎖對(duì)所有訪問可變狀態(tài)的代碼路徑進(jìn)行同步,使得在該對(duì)象上不會(huì)發(fā)生并發(fā)訪問
- 對(duì)于每個(gè)包含多個(gè)變量的不變性條件,其中涉及的所有變量都要由同一把鎖保護(hù)
2.5 活躍性與性能
- 不良并發(fā)
可同時(shí)調(diào)用的數(shù)量,不僅受到可用處理資源的限制,還受到應(yīng)用程序本身結(jié)構(gòu)的限制
- 避免不良并發(fā)
盡量將不影響共享狀態(tài)且執(zhí)行時(shí)間較長(zhǎng)的操作從同步代碼中分離出去,從而在這些操作的執(zhí)行過程中,其他線程可以訪問共享狀態(tài)
- 示例:
public class CachedFactorizer implements Servlet{
private BigInteger lastNumber;
private BigInteger[] lastFactors;
private long hits;
private long cacheHits;
public synchronized long getHits(){return hits;}
public synchronized double getCacheHitRatio(){
return (double)cacheHits/(double)hits;
}
public void service(ServletRequest req,ServletResponse resp){
BigInteger i=extractFromRequest(req);
BigInteger[] factors=null;
synchronized (this){
++hits;
if(i.equals(lastNumber)){
++cacheHits;
factors = lastFactors.clone();
}
}
if(factors==null){
factors=factor(i);
synchronized (this){
lastNumber=i;
lastFactors=factors.clone();
}
}
encodeIntoResponse(resp);
}
}