Android 多線程之線程安全問題

什么是線程安全問題

線程安全問題不是說線程不安全,而是多個線程之間交錯操作有可能導(dǎo)致數(shù)據(jù)異常。就比如兩個線程同時對一個數(shù)據(jù)進行操作,不能保證最后得到是數(shù)據(jù)是正確的,這就出現(xiàn)了線程安全問題。

什么是Java內(nèi)存模型

在這之前,先了解下Java內(nèi)存模型是什么,這能幫助我們更好的理解線程的安全性問題。


Java內(nèi)存模型

其實線程每次對數(shù)據(jù)操作,這些數(shù)據(jù)都是當(dāng)前線程工作內(nèi)存中的共享變量副本,并不是直接在主內(nèi)存操作。每條線程都有自己的工作內(nèi)存。

如果線程對變量的操作沒有刷回主內(nèi)存的話,僅僅改變了自己的工作內(nèi)存的變量副本,那么對其他線程來說是不可見的,不知道這個變量發(fā)生變化。而如果一個變量沒有讀取內(nèi)存中新值,而是使用舊的值去做后續(xù)操作的話,會得到一個錯誤的結(jié)果,這里體現(xiàn)出線程安全問題 -- 可見性

什么是Java線程調(diào)度

在任意時刻,CPU 只能執(zhí)行一條機器指令,每個線程只有獲取到 CPU 的使用權(quán)后,才可以執(zhí)行指令。也就是在任意時刻,只有一個線程占用 CPU,處于運行的狀態(tài)。

多線程并發(fā)運行實際上是指多個線程輪流獲取 CPU 使用權(quán),分別執(zhí)行各自的任務(wù)。線程的調(diào)度由 JVM 負(fù)責(zé),線程的調(diào)度是按照特定的機制為多個線程分配 CPU 的使用權(quán)。線程調(diào)度模型分為兩類:分時調(diào)度模型和搶占式調(diào)度模型。

1、分時調(diào)度模型:讓所有線程輪流獲取 CPU 使用權(quán),并且平均分配每個線程占用 CPU 的時間片。
2、搶占式調(diào)度模型:JVM 采用的是搶占式調(diào)度模型,也就是先讓優(yōu)先級高的線程占用 CPU,如果線程的優(yōu)先級都一樣,那就隨機選擇一個線程,并讓該線程占用 CPU。也就是如果我們同時啟動多個線程,并不能保證它們能輪流獲取到均等的時間片。如果我們的程序想干預(yù)線程的調(diào)度過程,最簡單的辦法就是給每個線程設(shè)定一個優(yōu)先級。

什么是數(shù)據(jù)依賴性

如果兩個操作訪問同一個變量,且這兩個操作中有一個為寫操作,此時這兩個操作之間就存在數(shù)據(jù)依賴。數(shù)據(jù)依賴分下列三種類型:
1、寫后讀(a = 1;b = a;),寫一個變量之后,再讀這個位置。
2、寫后寫 (a = 1;a = 2;),寫一個變量之后,再寫這個變量。
3、讀后寫(a = b;b = 1;),讀一個變量之后,再寫這個變量。

上面三種情況,只要重排序兩個操作的執(zhí)行順序,程序的執(zhí)行結(jié)果將會被改變。所以,編譯器和處理器在重排序時,會遵守數(shù)據(jù)依賴性,編譯器和處理器不會改變存在數(shù)據(jù)依賴關(guān)系的兩個操作的執(zhí)行順序。也就是說:在單線程環(huán)境下,指令執(zhí)行的最終效果應(yīng)當(dāng)與其在順序執(zhí)行下的效果一致,否則這種優(yōu)化便會失去意義。

如何保證線程安全

1、原子性
2、可見性
3、有序性
要實現(xiàn)線程安全就要保證上面說到的原子性、可見性和有序性。

原子性

在講原子性之前先來看看這個例子:

a++,對于共享變量a的操作,實際上會執(zhí)行三個步驟,
1.讀取變量a的值
2.a的值+1
3.將值賦予變量a

這三個操作中任何一個操作過程中,a的值被人篡改,那么都會出現(xiàn)我們不希望出現(xiàn)的結(jié)果。在多線程中,a的值可能被其他線程修改,導(dǎo)致線程不安全。為了保證線程安全,必須把這三個步驟當(dāng)成不可分割的一個整體操作,在其他線程看來,該操作只有未開始和結(jié)束的兩種狀態(tài),不知道中間發(fā)生什么。這就體現(xiàn)了原子性。

在單線程環(huán)境下我們可以認(rèn)為整個步驟都是原子性操作,但是在多線程環(huán)境下則不同,Java只保證了基本數(shù)據(jù)類型的變量和賦值操作才是原子性的。

注:基本數(shù)據(jù)類型的變量和賦值操作類似 i = 0 的操作

可見性

剛才講到Java內(nèi)存模型時,有提及到可見性??梢娦允侵敢粋€線程對共享變量的更新,對于其他讀取該變量的線程是否可見。怎么讓修改完的數(shù)據(jù)可見呢?舉個例子

① 將工作內(nèi)存1中的共享變量的改變更新到主內(nèi)存中
② 將主內(nèi)存中最新的共享變量的變化更新到工作內(nèi)存2中

Java提供了volatile關(guān)鍵字來保證可見性。通過synchronized和Lock也能夠保證可見性,synchronized和Lock能保證同一時刻只有一個線程獲取鎖然后執(zhí)行同步代碼,并且在釋放鎖之前會將對變量的修改刷新到主存當(dāng)中。因此可以保證可見性。

有序性

在Java內(nèi)存模型中,允許編譯器和處理器對指令進行重排序,但是重排序過程不會影響到單線程程序的執(zhí)行,卻會影響到多線程并發(fā)執(zhí)行的正確性。舉個例子

class ReorderExample {
    int a = 0;
    boolean flag = false;
 
    public void writer() {
        a = 1;          // 1
        flag = true;    // 2
    }
 
    public void reader() {
        if (flag) {          // 3
            int i = a * a; // 4
        }
    }
}

思考一下,flag變量是個標(biāo)記,用來標(biāo)識變量a是否已被寫入。這里假設(shè)有兩個線程A和B,A首先執(zhí)行writer()方法,隨后B線程接著執(zhí)行reader()方法。線程B在執(zhí)行操作4時,能否看到線程A在操作1對共享變量a的寫入?

答案是:不一定能看到。

由于操作1和操作2沒有數(shù)據(jù)依賴關(guān)系,編譯器和處理器可以對這兩個操作重排序;同樣,操作3和操作4沒有數(shù)據(jù)依賴關(guān)系,編譯器和處理器也可以對這兩個操作重排序。讓我們先來看看,當(dāng)操作1和操作2重排序時,可能會產(chǎn)生什么效果?

執(zhí)行順序是:2 -> 3 -> 4 -> 1 (這是完全存在并且合理的一種順序,如果你不能理解,請先了解CPU是如何對多個線程進行時間分配的)

操作3和操作4重排序后,因為操作3和操作4存在控制依賴關(guān)系。當(dāng)代碼中存在控制依賴性時,會影響指令序列執(zhí)行的并行度。為此,編譯器和處理器會采用猜測(Speculation)執(zhí)行來克服控制相關(guān)性對并行度的影響。以處理器的猜測執(zhí)行為例,執(zhí)行線程B的處理器可以提前讀取并計算a*a,然后把計算結(jié)果臨時保存到一個名為重排序緩沖(reorder buffer ROB)的硬件緩存中。當(dāng)接下來操作3的條件判斷為真時,就把該計算結(jié)果寫入變量i中。我們可以看出,猜測執(zhí)行實質(zhì)上對操作3和4做了重排序。重排序在這里破壞了多線程程序的語義。

在Java里面,可以通過volatile關(guān)鍵字來保證一定的“有序性”。另外可以通過synchronized和Lock來保證有序性,很顯然,synchronized和Lock保證每個時刻是有一個線程執(zhí)行同步代碼,相當(dāng)于是讓線程順序執(zhí)行同步代碼,自然就保證了有序性。另外,Java內(nèi)存模型具備一些先天的“有序性”,即不需要通過任何手段就能夠得到保證的有序性,這個通常也稱為 happens-before 原則。

happens-before原則(先行發(fā)生原則)

程序次序規(guī)則:一個線程內(nèi),按照代碼順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作。

鎖定規(guī)則:一個unLock操作先行發(fā)生于后面對同一個鎖額lock操作。

volatile變量規(guī)則:對一個變量的寫操作先行發(fā)生于后面對這個變量的讀操作。

傳遞規(guī)則:如果操作A先行發(fā)生于操作B,而操作B又先行發(fā)生于操作C,則可以得出操作A先行發(fā)生于操作C。

線程啟動規(guī)則:Thread對象的start()方法先行發(fā)生于此線程的每個一個動作。

線程中斷規(guī)則:對線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測到中斷事件的發(fā)生。

線程終結(jié)規(guī)則:線程中所有的操作都先行發(fā)生于線程的終止檢測,我們可以通過Thread.join()方法結(jié)束、Thread.isAlive()的返回值手段檢測到線程已經(jīng)終止執(zhí)行。

對象終結(jié)規(guī)則:一個對象的初始化完成先行發(fā)生于他的finalize()方法的開始。

總結(jié)

要實現(xiàn)線程安全就要保證上面說到的原子性、可見性和有序性。

注:提及到的volatile、synchronized和Lock后續(xù)寫一篇關(guān)于鎖的文章Android多線程之鎖

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容