線程安全性

多線程程序中,若是控制很差,常常會出現各類的問題,有時候問題在調試或者測試的時候就會暴露出來,最要命的是程序部署一段時間以後纔出現各類怪異現象,感到頭疼?須要補充Java併發的知識了。該讀書筆記系列以《Java併發編程實戰》爲基礎,同時會參考網絡上一些其餘的資料,和你們一塊兒學習Java併發編程的各個方面。這方面也是筆者比較薄弱的地方,理解不對的地方請留言或者郵件指出,同時也歡迎討論。郵箱:sunwei.pyw@gmail.com編程

線程安全性這章,將從以下幾個方面入手,描述探討:設計模式

一、什麼是對象的狀態安全

二、線程安全網絡

三、無狀態和有狀態多線程

四、競態條件併發

五、加鎖機制ide

內置鎖學習

鎖的重入測試

六、用鎖來保護狀態this

七、小結

本章大部分是比較抽象的概念,不過不要緊,咱們將列舉一些代碼來逐一說明,這些概念上的認識,將會對之後理解有很大的幫助。

一、什麼是對象的狀態

書中指出,從非正式的意義上說,對象的狀態是指存儲在狀態變量(實例或者靜態域)中的數據,對象的狀態可能包括其餘的依賴對象的域,例如某個HashMap的狀態不只存儲在HashMap自己,還存儲在許多Map.Entry中。在對象的狀態中包含了任何可能影響其外部可見行爲的數據。咱們能夠簡單地理解爲對象的狀態就是對象的值和屬性,對於簡單的類型,就是其值。線程安全的代碼,核心就是要對狀態訪問操做進行管理,特別是「共享的」和「可變的」狀態。共享意味着多個線程能夠同時訪問,而可變的意思就是在其生命週期內,其值能夠改變。

若是一個變量處於方法內部,它並非共享的,由於每一個線程執行代碼的時候,都會有各自的值存儲在線程的局部變量內,其餘線程是沒法訪問的。若是一個類變量被聲明成final的,它的屬性也沒有提供可訪問的修改方法,那麼它的狀態就是不可變的。

二、線程安全

線程安全的核心就是正確性,正確性的含義是,某個類的行爲與其規範徹底一致。這裏的規範能夠理解爲預期。

當多個線程訪問某個類時,不須要添加任何的同步或者協同,這個類始終都能表現出正確的行爲和結果,那麼就稱這個類是線程安全的。

能夠這樣理解,一個對象是否須要線程安全,取決於它是否被多個線程訪問。這裏指的是程序訪問對象的方式,而不是對象要實現的功能。單線程訪問任何狀態都是安全的。若是多個線程訪問一個未知線程安全的對象,就須要對訪問方式進行控制。一個對象是不是線程安全的,指的是對象的實現自己就是線程安全的。這種狀況下,不論對線程的訪問是否作了控制,這個對象老是線程安全的。

線程安全的程序是否徹底由線程安全的類組成?答案是否認的,徹底由線程安全類構成的程序不必定就是線程安全的,而在線程安全類中也能夠包含非線程安全的類。

因此線程安全其實就是在多線程環境下,類是否始終能夠表現出正確的行爲和結果,這個正確性實際上是咱們業務上的預期。

三、無狀態和有狀態

Servlet是多線程單實例的,這意味着多個多個請求共享一份Servlet對象,是一個典型的多線程訪問的例子。Servlet分爲無狀態和有狀態,分析以下的例子,本例子對書上的例子作了稍微的簡化:

public class SafeServlet implements Servlet{

    public void service(ServletRequest req, ServletResponse res)
            throws ServletException, IOException {
        String str1 = req.getParameter("param1");
        String str2 = req.getParameter("param2");
        res.getWriter().write(str1 + str2);
    }
…
}

 

此處省略了其餘的一些方法,service方法從請求中獲取到兩個字符串,返回兩個字符串的鏈接。

這個類是線程安全的,由於它是無狀態的,它不包含任何域,也不包含任何其餘類中域的引用,雖然全部線程都共享同一個SafeServlet實例,可是全部的線程都沒有共享變量,每一個線程都各行其是,沒有交集,也不會相互影響。

所謂有狀態,就像是咱們在第一節提出來的,該對象存在着共享的變量,每一個線程均可以訪問這個變量。

像接下來的這個Servlet,用來統計處理次數:

public class UnsafeServlet implements Servlet{

    private long count = 0l;
    
    public void service(ServletRequest req, ServletResponse res)
            throws ServletException, IOException {
        String str1 = req.getParameter("param1");
        String str2 = req.getParameter("param2");
        res.getWriter().write(str1 + str2);
        count ++;
        
    }
}

 

該Servlet是有狀態的,不一樣的線程調用service時,都會處理一個共享的變量:count。

count操做並非原子的,它能夠分解爲三個步驟:

讀取count的值;

修改count的值;

寫count的值;

這三步操做可能在多線程訪問時徹底搞亂順序。當線程A剛讀取完,線程B也讀取了值,可是線程A的執行時間片(https://zh.wikipedia.org/wiki/%E6%97%B6%E9%97%B4%E7%89%87)

完了,線程B理所固然地把count+1而且寫入了count變量中。當線程A再次執行的時候,用到的count已是過時的了。

而且其結果狀態依賴上一個線程的處理。這會引起一系列不正確的結果。

無狀態的類都是線程安全的,有狀態的類,若是想線程安全,須要作一些併發控制。UnsafeServlet中的count,若是類型改爲AtomicLong類型,這樣能夠把count++操做變成原子性的,由於AtomicLong對加一操做作了併發控制。由此咱們能夠想象,若是對狀態的操做是原子性的,該對象也是線程安全的,固然方法不止這一種,後面將會提到。

四、競態條件

這是維基百科的解釋:https://zh.wikipedia.org/wiki/%E7%AB%B6%E7%88%AD%E5%8D%B1%E5%AE%B3

以上的UnsafeServlet中,因爲不恰當的執行時序而出現的不正確的結果是很是典型的,咱們稱之爲「競態條件」。當某個計算的正確性取決於多個線程的交替執行時序時,那麼就會存在競態條件。這種狀況下是否返回正確的結果,徹底靠運氣。究其根本緣由,就是可能基於一種已經失效的結果來作操做。

值得一提的是,設計模式:單例模式(https://zh.wikipedia.org/wiki/%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F),很容易被寫成線程不安全的以下方式:

public class Singlton {
    private Object obj = null;
    
    public Object getInstance(){
        if(obj == null){
            obj = new Object();
        }
        return obj;
    }
}

 

這裏包含一個競態條件,它可能破壞這個類的正確性。不難分析,當兩個線程都訪問這個類想獲取一個Object對象的時候,A、B都斷定obj是null。A、B都會建立一個Object對象。那麼他們可能返回不一樣的對象。維基百科上給出了安全的處理方式,這裏就再也不贅述了。

要避免競態條件問題,就必須在某個線程修改該變量時,經過某種方式防止其餘線程使用這個變量,從而確保其餘線程只能在修改操做完成以前或者以後讀取和修改狀態,而不是在在修改這個狀態的過程當中,也就是以前UnsafeServlet所描述的那樣,將分步的操做複合成原子性的。實際狀況中,儘量能使用現有的線程安全對象來管理類的狀態,這樣更容易驗證和維護線程的安全性問題。

五、加鎖機制

UnsafeServlet的描述中,在Servlet中添加一個狀態變量時,可使用線程安全的對象來保證類的安全性,若是須要更多的狀態變量時,是否只須要用線程安全的對象就能夠保證線程安全了呢?不是這樣的。

咱們將代碼稍做修改,添加一個變量記錄最後一次請求的參數:

一樣,省略了其餘方法的實現。

public class UnsafeServlet implements Servlet{

    private AtomicLong count = new AtomicLong(0);
    private AtomicReference<String> lastParam1 = new AtomicReference<String>();
    
    public long getCount(){return count.get();}
    public String getLastParam1(){return lastParam1.get(); } 
    
    public void service(ServletRequest req, ServletResponse res)
            throws ServletException, IOException {
        String str1 = req.getParameter("param1");
        String str2 = req.getParameter("param2");
        res.getWriter().write(str1 + str2);
        count.incrementAndGet();
        lastParam1.set(str1);
    }
…
}

 

然而這種方式並不正確,雖然咱們的兩個變量都是原子性的,也是線程安全的,可是這個類中存在競態條件。

在線程安全的定義中,要求多個線程之間的操做不管採用什麼執行時序或者交替方式,都要保證結果正確。雖然使用了set操做是原子性的變量,可是service方法沒法保證兩次set操做總體是原子性的。

內置鎖

Java提供了一種內置機制來支持原子性:同步代碼塊(synchronized)。同步代碼塊包括兩個部分:一個做爲鎖的對象引用,一個做爲由這個鎖保護的代碼塊。靜態的synchronized方法以Class對象做爲鎖。

每一個Java對象均可以用作一個實現同步的鎖,這些鎖被稱爲內置鎖。

線程在進入同步代碼塊以前會自動得到鎖,而且在退出同步代碼塊時自動釋放鎖。不管是經過正常的控制路徑退出仍是經過代碼塊中拋出的異常退出,得到內置鎖的惟一途徑就是進入由這個鎖保護的同步代碼塊或方法。

內置鎖至關於一種互斥體,意味着當線程A進入同步代碼塊的時候,其餘線程是沒法進入代碼塊的,這樣就能夠保證代碼塊是原子性的。直到線程A釋放鎖,其餘線程才能進入代碼塊。此處咱們能夠把整個servie方法都同步起來:

public synchronized void service(ServletRequest req, ServletResponse res)

可是這樣的效率過低,由於這就至關於說明只能按順序來訪問該serlvet,違背了咱們多線程訪問的初衷,一樣咱們也能夠只將操做屬性的操做同步起來:

synchronized(this){count.incrementAndGet();lastParam1.set(str1);}

重入

重入指的是一種機制,當某個線程請求一個由其餘線程持有的鎖時,發出請求的線程就會阻塞,而後因爲內置鎖是能夠重入的,所以若是某個線程試圖得到一個已經由它本身持有的鎖,那麼這個請求是會成功的。也就是說本身請求本身的鎖,是能夠成功的,這種機制避免了一些狀況

下死鎖的發生。

上面的代碼中,子類改寫了父類的synchronized方法,而後又調用父類中的方法,此時若是沒有可重入的鎖,那麼這段代碼將死鎖。因爲Widget和ChildWidget中doSth方法都是synchronized的,所以每一個doSth方法在執行前都會獲取Widget上的鎖,由於這個鎖已經被持有,而線程將永遠等待下去。

public class Widget {
    public synchronized void doSth(){
        System.out.println("Parent Do Sth");
    }
}
public class ChildWidget extends Widget{
    @Override
    public synchronized void doSth() {
        super.doSth();
    }
}

 

六、用鎖來保護狀態

因爲鎖能夠保護代碼按串行的形式來訪問,所以能夠經過鎖來構造一些協議以實現對共享狀態的獨佔訪問。共享狀態的複合操做,如:遞增、單例模式裏先判斷後建立對象等,都必須是以原子操做以免競態條件的產生。僅僅將複合操做封裝到一個同步代碼塊中是不夠的,若是用同步來協調對某個變量的訪問,那麼全部訪問這個變量的位置都須要使用同一個鎖。

對象的內置鎖與其狀態之間沒有內在的關聯,當獲取與其對象相關聯的鎖時,並不能阻止其餘線程訪問該對象,某個線程在得到對象的鎖以後,只能阻止其餘線程獲取同一個鎖。

之因此每一個對象都有一個內置鎖,只是爲了免去顯式地建立鎖對象,若是自行構造一個鎖對象,那麼久須要在程序中自始至終都使用它們。

每一個共享的可變的變量都應該只由一個鎖在保護,從而使得維護人員知道是哪個鎖。

一種常見的約定是,將全部的可變狀態都封裝在對象內部,而且經過對象的內置鎖對全部訪問可變狀態的代碼進行同步。

七、小結

本節主要講述了以下幾個點,非正式的說法對象的狀態就是對象的值和屬性,對象在多線程訪問時,若是老是能返回正確的結果,那麼這個對象就是線程安全的,無狀態的類必定是線程安全的,若是對象中存在競態條件,將會出現多線程訪問數據正確性問題。Java提供了內置鎖來支持對象的線程安全。線程能夠獲取本身的鎖,這就叫重入,因爲鎖機制只能保證同一個鎖不被不一樣的線程持有,咱們用鎖機制來保護對象的狀態時,須要注意不變性條件中的每一個變量都要使用同一個鎖來保護。

相關文章
相關標籤/搜索