線程是CPU調(diào)度的最小單位,與進(jìn)程不同,它們擁有相同的地址和fd描述符,操作系統(tǒng)的基本調(diào)度單元是線程。進(jìn)程為線程提供了獨立的地址(通過vm)和獨立的資源,文件句柄,是實體單元。
總的來說,多線程的安全性問題主要是對于多線程之間的可變共享變量如何進(jìn)行操作的問題。共享意味著可以由多個線程共同訪問,可變意味著變量可以在這個過程中發(fā)生改變。所以可能會產(chǎn)生的競爭條件。必須使用同步來保證各個線程之間對于這些共享變量的協(xié)調(diào)問題。
不包含類的實例域或者靜態(tài)域的方法是無狀態(tài)的(stateless),由于方法內(nèi)的局部變量是棧上的,棧是線程私有的,其他線程無法主動訪問線程的棧。如果不主動發(fā)布一個局部變量到外部的話,那么該方法總是線程安全的,因為這些不發(fā)布的局部變量只在棧內(nèi)部。如果方法想要發(fā)布局部變量到其他線程的話1.如果是一個基本類型的引用,由于方法之間的參數(shù)傳遞是值傳遞,外部方法只能得到這個基本類型的一個拷貝,Java的語言特性保證了該應(yīng)用是線程安全的。因為其他對象無法實際訪問到它(只是得到值的拷貝)。如果是指向?qū)ο蟮囊茫敲赐ㄟ^參數(shù)傳遞外部線程可以得到在棧上的某個實際對象的應(yīng)用,所以必須要進(jìn)行必要的線程安全措施。
例如,來看一下書上的這個例子就可以明白什么是無狀態(tài)了。因為這個線程沒有引用外部線程的方法,亦沒有將局部引用變量發(fā)布到線程之外,其service方法只有兩個線程私有的局部變量BigInteger i 和 BigInteger[] factors ,所以這個線程一定是線程安全的。
@ThreadSafe
public class StatelessFactorizer implements Servlet {
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
encodeIntoResponse(resp, factors);
}
}
這里,我們可以考慮一下StatelessFactorizer類在如果加入一個實例域count,會發(fā)生什么??紤]這樣一件事,對于一個實例域來說,它并不是線程私有的,作為類的實例域,他被分配到線程們共享的堆上。對于線程來說,如果是一個PUBLIC的域,那么所有線程都擁有訪問他的權(quán)限,而對于一個private變量來說,訪問限制是在類層次上,不是對象層次上的。只要是調(diào)用了privat域所屬類的方法的線程都可以訪問它。
@NotThreadSafe
public class UnsafeCountingFactorizer implements Servlet {
private long count = 0;
public long getCount() { return count; }
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
++count;
encodeIntoResponse(resp, factors);
}
}
現(xiàn)在問題出現(xiàn)了,主要在于count++這里,對于多個線程來說很可能出現(xiàn)競爭問題。最主要的是count++雖然看上去是一個操作,但他實際上由讀取-自加-寫回三步組成。所以很可能出現(xiàn)兩個線程同時讀取,同時自加,最后同時寫回,這樣count只自加了一次。對于這種問題通常稱為競爭狀態(tài)(Race-Condition)。必須提出一種手段,當(dāng)線程在修改時不允許其他線程對這個共享域的訪問,并且要保證一旦值發(fā)生了改變,所有線程都必須能看到這種改變,這就是原子性和可見性這同步的兩大要素。
Java通過同步快的形式來實現(xiàn)內(nèi)部鎖,內(nèi)部鎖的對象是this自身。
1.內(nèi)部鎖。(Intrinsic Locks)
synchronized (lock) {
// Access or modify shared state guarded by lock
}
通過傳入lock對象進(jìn)入synchronized塊來實現(xiàn)同步。當(dāng)執(zhí)行線程進(jìn)入到同步塊時自動獲得內(nèi)部鎖對象。離開時自動釋放。
2.重入(Reentrancy)
重入意味著java中的內(nèi)部鎖持有單位是進(jìn)程本身(one lock per thread)而不是POSIX標(biāo)準(zhǔn)中的每一次方法調(diào)用(one lock per tinvocation).這意味著JVM將鎖與線程以及計數(shù)相關(guān)聯(lián)。當(dāng)計數(shù)是零,鎖被認(rèn)為不被任何線程持有。當(dāng)一個線程請求一個未被持有的鎖時,此時如果計數(shù)是零,則JVM會記錄此時的線程,并將計數(shù)寫成1。如果不是零,那么JVM會檢查這個進(jìn)程是否是鎖的持有者,如果是,JVM會允許對于鎖的再次獲取,并將計數(shù)自加,換句說JAVA允許持有鎖的對象對于鎖的重復(fù)進(jìn)入。如果否,那么會吊起進(jìn)程直到持有鎖的進(jìn)程釋放鎖再進(jìn)行鎖的分配。
下面來看一個例子,這個例子在基于調(diào)用的鎖下回造成死鎖。例如:在Widget的doSomething方法中調(diào)用子類的doSomething,此時父類的方法持有一個獨立的鎖,子類的方法也持有一個獨立的鎖,在子類的方法中我們調(diào)用了父類的方法,對于基于調(diào)用的鎖來說,由于父類的鎖的存在,在想要調(diào)用父類的方法時,子類會被掛起,由于父類在等待子類方法的結(jié)束,而子類由于父類的鎖的存在被掛起,這樣造成了這個進(jìn)程無法再繼續(xù)執(zhí)行,陷入了死鎖。
public class Widget {
public synchronized void doSomething() {
}
}
public class LoggingWidget extends Widget {
public synchronized void doSomething() {
System.out.println(toString() + ": calling doSomething");
super.doSomething();
}
}
3.性能和同步的折中。
通常情況下不需要對所有塊都進(jìn)行同步保護(hù),這樣會大大的降低性能,也違背了使用多線程的初衷。一般只在出現(xiàn)了共享變量,并對其進(jìn)行了操作的塊才需要添加同步,這樣我們在性能和正確性上進(jìn)行了折中。要注意的一點是不止是setter方法,getter方法也需要同步,才能保證讀到的不是一個無效的值,這涉及到線程的可見性問題。一般編譯器會對沒有同步保護(hù)和沒有volatile說明的變量進(jìn)行優(yōu)化到cache或者register,對于多核來說,不同線程屬于不同的核,他們的cache和register內(nèi)的值對于各個其他核是不可見的。如果一個setter方法改變了值,而其getter方法沒有同步,那么其他線程可能讀到的是一個setter方法之前的過期值。使用同步可以保證我讀到的是最新的。當(dāng)然這里也可以使用Volatile進(jìn)行可見性聲明。