處理器的時鐘頻率已經(jīng)很難再提高,想要提升計算機的性能,一個很明顯的趨勢是使用多處理器。
因為程序調(diào)度的基本單元是線程,一個單線程程序一次只能運行在一個處理器上,在雙處理器系統(tǒng),就浪費了一般的CPU資源,在100個CPU系統(tǒng)中,就浪費了99%的CPU資源。而使用多線程編程則能夠充分利用處理器資源,提高吞吐量。
多線程的使用并不是絕對有利的,它同時也引入了單線程環(huán)境不存在的安全性問題,在多線程環(huán)境中,因為競爭條件的存在,在沒有充分同步的情況下,多線程中的各個操作的順序是不可預(yù)測的,有時甚至讓人驚訝。
在設(shè)計良好的應(yīng)用程序使用多線程,能夠獲得不錯的性能收益,但多線程仍會帶來一定程度的性能開銷。上下文切換,當(dāng)調(diào)度程序時掛起正在運行的線程,另一個線程開始運行--這在多線程系統(tǒng)中是很頻繁的,會帶來很大的性能消耗;保存和恢復(fù)線程執(zhí)行的上下文,會讓CPU的時間花費在對線程的調(diào)度而不是運行上。當(dāng)線程共享數(shù)據(jù)時,需要使用同步機制,這回限制編譯器的優(yōu)化。
......
什么是線程安全
當(dāng)多個線程訪問一個類時,如果不用考慮這些線程在運行時環(huán)境下的調(diào)度和交替執(zhí)行,并且不需要同步及在調(diào)用代碼時不需要額外的協(xié)調(diào),這個類的行為仍是正確的,那么這個類是線程安全的。
無狀態(tài)的對象永遠(yuǎn)是線程安全的
public class StatelessServlet implements Servlet{
public void service(ServletRequest req, ServletResponse res){
Integer i = extractFromRequest(req);
Integer[] factors = factor(i);
encodeIntoResponse(res, factors);
}
}
以一段偽代碼來說明。
StatelessServlet是一個無狀態(tài)的servlet,因為它不包含域也沒有引用其他類的域。線程之間不共享狀態(tài),一個請求過來,會唯一地存在本地變量,這些變量保存在線程的棧中,只有執(zhí)行線程才能訪問,不會影響同一個servlet的其他請求線程。
因為線程訪問無狀態(tài)對象的行為,不會影響其他線程訪問該對象的正確性,因此無狀態(tài)對象是線程安全的。
原子性
給上面的偽代碼加上計數(shù)功能,如下:
public class UnsafeCountServlet implements Servlet{
private long count = 0;
public long getCount(){return count;}
public void service(ServletRequest req, ServletResponse res){
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
++count;
encodeIntoResponse(res, factors);
}
}
此時的UnsafeCountServlet是一個有狀態(tài)的類,在多線程環(huán)境中不再是線程安全的,因為++count不是一個原子操作,而是三個離散操作的的簡寫:獲取當(dāng)前值,加1,返回新值,是一個“讀-改-寫”操作。
這樣的操作在多線程環(huán)境中很容易出現(xiàn)問題,加入初始值為0,某個時刻線程一讀取到值0,此時線程調(diào)度,另一個線程二也讀到0,然后加1,返回新值1,再切回線程一,加1,返回新值1,這就缺少了一次自增。
出現(xiàn)這樣的錯誤是因為:競爭條件的存在。
競爭條件
當(dāng)計算的正確性依賴于運行時的時序或者多線程的交替時,就會產(chǎn)生競爭條件。最常見的競爭條件就是“檢查再運行”。
下面是一個惰性初始化的例子:
public class LazyInitClass{
private ExpensiveObject instance = null;
public ExpensiveObject getInstance(){
if(instance == null){
instance = new ExpensiveObject();
}
return instance;
}
}
LazyInitClass的競爭條件會破壞其正確性。假如兩個線程A和B同時執(zhí)行g(shù)etInstance(),A看到instance為null,執(zhí)行初始化,此時B也在檢查instance是否為null,而instance是否為null,依賴于時序,是無法預(yù)期的,如果B檢查也為null,則線程B也會執(zhí)行初始化,得到兩個不同的對象,和惰性初始化只初始化一次是矛盾的。
使用線程安全對象管理類的全部狀態(tài),可以維護(hù)類的線程安全性
將UnsafeCountServlet改造一下,代碼如下:
public class SafeCountServlet{
private final AtomicLong count = new AtomicLong(0);
public long getCount(){return count.get();}
public void service(ServletRequest req, ServletResponse res){
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
count.incrementAndGet();
encodeIntoResponse(res, factors);
}
}
java.util.concurrent.atomic包中包括了原子變量類,這些類用來實現(xiàn)數(shù)字和對象引用的原子狀態(tài)轉(zhuǎn)換,把long換成AtomicLong,可以確保所有訪問計數(shù)器狀態(tài)的操作都是原子的,計數(shù)器是線程安全的了,而計數(shù)器的狀態(tài)就是SafeCountServlet的狀態(tài),所以SafeCountServlet也變成了線程安全的。
使用鎖可以維護(hù)類的線程安全性
使用線程安全對象管理類的全部狀態(tài),可以維護(hù)類的線程安全性,但如果類中存在多個實例域,即有多個狀態(tài),僅僅加入更多的線程安全的狀態(tài)變量時不夠的。
為了保護(hù)狀態(tài)的一致性,要在單一的原子操作中更新相互關(guān)聯(lián)的的狀態(tài)變量。
java提供了鎖可以保護(hù)類的線程安全性。