java中的線程安全

在Java中,線程的安全實際上指的是內存的安全,這是由操做系統決定的。編程

目前主流的操做系統都是多任務的,即多個進程同時運行。爲了保證安全,每一個進程只能訪問分配給本身的內存空間,而不能訪問別的、分配給別的進程的內存空間,這一安全特性是由操做系統保障的。可是線程卻與進程不一樣,由於在每一個進程的內存空間中都會有一塊特殊的公共區域,一般被稱爲堆(內存),這塊內存區域是進程內全部的線程均可以訪問獲得的,這個特性是線程之間通訊的一種方式,可是卻會引起多個線程同時訪問一塊內存區域可能產生的一系列問題,這些問題被統稱爲線程的安全問題。promise

如何在Java中保證線程安全,也就是保證內存的安全,是一個重要的知識點。安全

使用局部變量保證線程安全(內存隔離法)多線程

在程序中,操做系統會爲每一個線程分配專屬的內存空間,一般被稱爲棧內存。棧內存是當前線程所私有的,其它線程無權訪問,這是由操做系統保障的。那麼,若是想要一些數據只能被某個線程訪問的話,就能夠把這些數據放入線程專屬的棧內存中,其中最多見的就是局部變量,局部變量在線程執行方法時被分配到線程的棧內存中。併發

double avgScore(double[] scores) {
    double sum = 0;
    for (double score : scores) {
        sum += score;
    }
    int count = scores.length;
    double avg = sum / count;
    return avg;
}

上面定義了一個算平均成績的方法,其中的sum、count和avg都是局部變量,當有一個線程A來執行這個方法的時候,這些變量就會在A的棧內存中分配。若是在這時候有另一個B線程來執行這個方法,這些變量也會在B的棧內存中分配,可是B的棧內存中的這些變量和A的棧內存中的這些變量是相互獨立的,並不會相互影響。編程語言

簡單來講,就是這些局部變量會在每一個獨立線程私有的棧內存中分配一份,而因爲線程的棧內存只能被當前線程本身訪問,因此棧內存分配的這些變量不能被別的線程訪問,也就不會有線程安全的問題了。而局部變量之因此是安全的,是由於它的使用範圍僅僅侷限於方法中,生命週期隨着方法的執行從開始到結束。然而實際開發中卻不可能僅僅將一個變量侷限於一個方法中,老是要有一個變量被多個方法使用的狀況,這時就又會產生線程安全的問題了。高併發

使用ThreadLocal類保證線程安全(標記隔離法)this

若是想要一個變量能被多個方法使用,一般是將變量定義爲類的成員變量。而按照主流編程語言的規定,類的成員變量不能再被分配在線程的棧內存中,而應該分配在公共的堆內存中。這樣,變量從單個線程的私有變成了多個線程的公有,要保證線程安全就須要想一些特殊的辦法,其中的一個方法就是使用ThreadLocal類。使用ThreadLocal類修飾變量以後,每一個線程若是須要訪問這個變量,都會拷貝一份出來,而後當前線程就只會訪問這份拷貝。這樣,由於每一個線程都只能訪問本身拷貝的變量,即這些拷貝出來的變量是線程私有的,也就保證了線程的安全了。spa

class SugarFactory {
    ThreadLocal<String> sugar = new ThreadLocal<>();

    String getSugar() {
        return sugar.get();
    }
}

上面是一個SugarFactory類,類中有一個ThreadLocal類型的成員變量sugar,每一個線程在運行時,若是須要用到sugar變量,就會從堆中拷貝一份sugar變量出來,存放到線程對象(Thread類的實例對象)的成員變量中去。線程類(Thread)是有一個相似於Map類型的成員變量專門用於存儲ThreadLocal類型的數據的。操作系統

從邏輯的從屬關係來理解,這些ThreadLocal類型的數據是屬於Thread類的成員變量級別的。但若是是從邏輯的內存位置上來看,實際上這些ThreaLocal類型的數據仍是分配在公共區域的堆內存中。這種作法就相似於給該內存區域打上某種標記的作法,在堆內存中標記了這塊內存是這個線程私有的。

完整地講,就是當一個線程須要訪問該ThredLocal類型變量的時候,就從堆內存中複製一份出來,並給這份複製打上一個標記,標記這份複製是該線程私有的。

使用常量保證線程安全(只讀標記法)

咱們知道,在Java中的常量是隻能被讀取而不能被修改的,常量一般使用final修飾符進行修飾,這時候對於多線程來講是安全的。

class MyPromise {
    final String promise = "i love you forever.";
}

常量不會引發不經意被修改的問題,不管多少次讀取,結果都是同樣的。

使用悲觀鎖保證線程安全(加鎖標記法)

悲觀鎖一般的理解就是互斥鎖。所謂的悲觀,指的就是悲觀地認爲必定會發生線程安全問題,因而就給公共的數據加上一把鎖,若是一個線程想要訪問該數據,就須要先獲取該數據上所加的鎖,才能訪問該數據,而且在該線程沒有釋放鎖以前,其餘的線程是不可以訪問該數據的,這樣就保證了只有持有鎖的線程可以訪問該數據,也就保證了線程安全。

class LoveYou {
    double love = 100;
    final Lock lock = new Lock();

    void increaseLove(double love) {
        lock.obtain();
        this.love += love;
        lock.release();
    }
    
    void decreaseLove(double love) {
        lock.obtain();
        this.love -= love;
        lock.release();
    }
}

上面的代碼中展現了一個這樣的場景:我對你的愛初始值是100,若是你作了一些讓我開心的事情,我對你的愛意就會增長;若是你作了一些讓我難過的事情,我對你的愛意就會減小。由於你讓我開心或難過老是反反覆覆的,若是把時間線拉得無限長,這就是一個併發的場景。增長愛意和減小愛意這兩個方法被併發調用,它們共同操做總的愛意,而爲了保證愛意的先後一致性,就須要在每次對數據進行操做以前先獲取鎖,操做完成以後再釋放鎖。

這種對數據進行加鎖的作法,雖然可以很好地解決線程安全問題,可是鎖的獲取和釋放是須要耗費資源的,若是在線程不多的狀況下(併發不多),即線程安全問題發生機率較小的狀況下,就很容易形成資源的浪費。

使用樂觀鎖(CAS)保證線程安全(狀態比較法)

樂觀鎖是在併發量小的狀況下對悲觀鎖的一種替代方案,具體是爲了下降悲觀鎖可能產生的資源浪費。

所謂的樂觀,指的就是樂觀地認爲數據在併發量小的狀況下,被意外修改的可能性較小。

樂觀鎖一般的實現就是CAS(Compare and Swap,比較並交換)。假若有一個線程在操做數據,操做到一半想要休眠(掛起)了,而後它就會記錄下當前數據的狀態(當前數據值),而後就休眠(掛起)了。而後線程從新喚醒以後想要接着操做數據,這時候又擔憂數據可能被修改了,因而就把線程休眠前保存的數據狀態和如今的數據狀態作一個比較,若是是同樣的話,說明在線程休眠的過程當中數據沒有被別的線程動過(也有可能數據已經被別的線程改過好多輪了,只是最後的數據和該線程休眠前的數據一致,這就是所謂的ABA問題),而後就能夠接着完成線程還沒完成的操做。若是數據先後不一致,則說明數據被修改,那麼這時候線程前面的全部操做都要放棄,從頭開始從新再處理一遍邏輯。

而後說一下ABA問題的解決方案,解決方案一般是給數據另外加一個做爲標記的版本號字段,並規定每次修改數據都使版本號加1,就能有效判斷數據到底有沒有被修改過了。

最後再說一下樂觀鎖和悲觀鎖的使用場景。樂觀鎖一般適用在併發量較小的場景下,由於這種場景下數據被併發操做的機率很小,加互斥鎖會浪費資源;而悲觀鎖一般適用在併發量很大的場景下,由於這種場景下數據被併發操做的機率很大,若是使用樂觀鎖的話,在每次數據被修改後線程都從頭開始從新處理一遍邏輯,資源的消耗會遠大於互斥鎖的資源消耗,所以加互斥鎖基本上是目前高併發場景的最優方案。

總結

線程的安全問題,從個人理解上,其實很大程度上是在於線程不是持續工做,而是會在工做的途中休眠所形成的,可是這個可能並無辦法解決,由於這是操做系統所決定的,也能夠說是CPU的運行機制所決定的,咱們只能從另外的入口去想辦法解決問題。

 

"今天起了風,你站在風口,個人整個世界都是你的味道。"

相關文章
相關標籤/搜索