java并發(fā)--線程安全

處理器的時鐘頻率已經(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ù)類的線程安全性。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 從三月份找實習(xí)到現(xiàn)在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂視家的研發(fā)崗...
    時芥藍(lán)閱讀 42,372評論 11 349
  • 簡書 賈小強轉(zhuǎn)載請注明原創(chuàng)出處,謝謝! 線程安全的定義常常讓人迷惑,搜索引擎會發(fā)現(xiàn)無數(shù)定義,比如: 多個線程同時執(zhí)...
    賈小強閱讀 239評論 0 0
  • 下面是我自己收集整理的Java線程相關(guān)的面試題,可以用它來好好準(zhǔn)備面試。 參考文檔:-《Java核心技術(shù) 卷一》-...
    阿呆變Geek閱讀 14,907評論 14 507
  • 一.線程安全性 線程安全是建立在對于對象狀態(tài)訪問操作進(jìn)行管理,特別是對共享的與可變的狀態(tài)的訪問 解釋下上面的話: ...
    黃大大吃不胖閱讀 867評論 0 3
  • 我聽見 空調(diào)水落在窗臺 樹葉飄在草地 地球在轉(zhuǎn) 燈光灑在被子上 夜 還很長…
    青鵝閱讀 158評論 0 0