Java多線程線程安全

一、什麼是線程安全

我們所說的線程安全的話題都是基於一個變量會被多個線程訪問的這樣一個前提下,如果只是單線程應用自然就不會出現這種問題。

一個變量被多個線程訪問我們稱這個變量是共享的。而一個變量在其生命週期中可以被修改,則稱這個變量時可變的。網絡上有很多人試圖給線程安全下定義,林林總總反正很多,但是歸根到底線程安全的核心點就是正確性。試想下,多個線程訪問某個共享的可變的變量的時候,其目的就是爲了得到一個正確的數據而已。


這裏給出我對線程安全的理解:一個對象被多個線程訪問的情況下,如果各個線程對該對象的讀操作都能得到正確的值,以及對該對象的寫操作都能安全的寫入(不會覆蓋之前寫入的數據以及寫入的數據符合該類的規約),簡而言之就是這個類的行爲都是正確的則這個類就是線程安全的。線程安全的類客戶可以在任何情況下拿來使用,當然如果是單線程的話最好不要使用,因爲線程安全的類裏面都封裝了必要的同步,而同步設計到鎖,鎖的開銷是比較大的,單線程下也許會影響其性能。


不知道大家有沒有聽說過無狀態的類,無狀態的類值得是類裏面沒有成員變量,也沒有引用其他變量,或者有成員變量但是不可變的、或者成員變量是單例的、比如struts1的action、可以是單例的、因爲他是沒有狀態的。無狀態的類裏面的值(可能通過計算,也可能當參數傳入)會唯一的存在本地變量中,這些本地變量存儲在線程的棧中,只有執行線程才能訪問。

public class StatelessServlet implements Servlet{
    public void service(ServletRequest req,ServletResponse resp){
        BigInteger i=extractFromRequest(req);
        BigInteger[] factors=factor(i);
        encodeIntoResponse(resp,factors);
    }
}

如上代碼中的i是從request中得到的,factors又是通過計算得到的,這個servlet是無狀態的。我們可以得到一個結論,無狀態的類都是線程安全的

二、原子性

我們修改上面的代碼,加上一個計數變量

public class StatelessServlet implements Servlet{
    private long count=0;
    public void service(ServletRequest req,ServletResponse resp){
        BigInteger i=extractFromRequest(req);
        BigInteger[] factors=factor(i);
        count++;
        encodeIntoResponse(resp,factors);
    }
}

這個代碼咋一看似乎沒什麼問題,但是這裏的count++的自增操作並不是原子性的,它包括三步,“讀-改-寫”,所以可能會出現這樣的情況:有AB兩個線程同時來訪問這個方法,AB兩個線程都看到count的值爲9,然後都對其進行加1最後都是10導致一次計數憑空消失。

爲什麼會出現這樣的情況呢?因爲這期間存在競爭條件,導致其結果不可靠。當計算的正確性依賴於運行時鐘相關的時序或者多線程的交替時,會產生競爭條件。換句話說,想要得到正確的答案,要依賴於“幸運的時序”。最常見的一種競爭條件是“檢查再運行”,使用一個潛在的過期值作爲決定下一步操作的依據。

就像我們熟知的單例設計模式中的懶漢式,就是這樣的競爭條件。

class LazySingleton{
    private LazySingleton(){}
    private static LazySingleton lazySingleton=null;
    //這樣是線程不安全的
     public static LazySingleton getInstance(){
        if(lazySingleton==null){
            return new LazySingleton();
        }
        return lazySingleton;
    }

}

我們將像count++這樣不止一步的操作稱之爲複合操作,如果這樣的複合操作要麼一次性全部執行完,要麼一點都不執行則可以將這些複合操作稱爲原子操作

三、鎖(Synchronized)

如何將一個複合操作變成原子操作,使用Synchronized關鍵字就可以做到。Java提供了強制原子性的內置鎖機制:Synchronized塊。一個Synchronized塊有兩部分:鎖對象的引用,以及這個鎖保護的代碼塊。鎖保護的代碼塊可以保證裏面的操作是原子性的。

synchronized(lock){
//訪問或修改被鎖報的共享狀態
}

每個Java對象都可以隱式地扮演一個用於同步的鎖的角色;這些內置的鎖被稱爲內部鎖或監視器鎖。執行線程進入Synchronized塊之前會自動獲取鎖;而無論正常退出還是拋出異常,線程都會放棄獲得的鎖。同一個內部鎖同一時間只能被一個線程獲取,所以內部鎖在Java中還扮演了互斥鎖的角色。正是因爲這個機制,Synchronized才能保證裏面的代碼塊同一時間只能由一個線程來執行,從而保證了其複合操作的原子性。

內部鎖還有一個叫重進入 的特性。重進入意味着線程在試圖獲得它自己佔有的鎖的時候,請求會成功,同時也意味着鎖的請求是基於“每線程”,而不是“每調用的”。重進入的實現是通過爲每個鎖關聯一個請求計數和一個佔有它的線程。當計數爲0時,認爲鎖是未被佔有的。線程請求一個未被佔有的鎖時,JVM將記錄鎖的佔有者,並將請求計數置爲1。如果同一線程再次請求這個鎖,計數器遞增,每次佔用線程退出同步塊,計數器減一。直到計數器爲0,鎖被釋放。

public class Widget{
    public synchronized void doSomething(){
        .......
    }
}
public class LoggingWidget extends Widget{
    public synchronized void doSomething(){
        super.doSomething();
    }
}

如果內部鎖不能重進入,此代碼會鎖死。

對於每個可被多個線程訪問的可變狀態變量,如果所有訪問它的線程在執行時都佔有同一個鎖,這種情況下,我們稱這個變量是由這個鎖保護的。

在使用synchronized的時候,要懂得合理的保護代碼塊,不能太大也不能太小。太大的話比如將servlet中的整個service包起來,雖然是安全了,但是違背了容器設計的初衷,這樣過個請求一起過來只能一個一個處理;而太小可能會把複合操作拆開,所以使用的時候要進行合理分析。還有一些耗時操作比如請求網絡資源,IO操作等,這樣的處理儘量不要佔有鎖

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章