☆啃碎并發(fā)(五):Java線程安全特性與問題

0 前言

在單線程中不會出現(xiàn)線程安全問題路呜,而在多線程編程中迷捧,有可能會出現(xiàn)同時訪問同一個 共享、可變資源 的情況胀葱,這種資源可以是:一個變量漠秋、一個對象、一個文件等抵屿。特別注意兩點:

  1. 共享: 意味著該資源可以由多個線程同時訪問庆锦;
  2. 可變: 意味著該資源可以在其生命周期內(nèi)被修改;

簡單的說轧葛,如果你的代碼在單線程下執(zhí)行和在多線程下執(zhí)行永遠(yuǎn)都能獲得一樣的結(jié)果搂抒,那么你的代碼就是線程安全的。那么尿扯,當(dāng)進(jìn)行多線程編程時求晶,我們又會面臨哪些線程安全的要求呢?又是要如何去解決的呢衷笋?

1 線程安全特性

1.1 原子性

跟數(shù)據(jù)庫事務(wù)的原子性概念差不多芳杏,即一個操作(有可能包含有多個子操作)要么全部執(zhí)行(生效),要么全部都不執(zhí)行(都不生效)辟宗。

關(guān)于原子性爵赵,一個非常經(jīng)典的例子就是銀行轉(zhuǎn)賬問題:

比如:A和B同時向C轉(zhuǎn)賬10萬元。如果轉(zhuǎn)賬操作不具有原子性慢蜓,A在向C轉(zhuǎn)賬時亚再,讀取了C的余額為20萬,然后加上轉(zhuǎn)賬的10萬晨抡,計算出此時應(yīng)該有30萬氛悬,但還未來及將30萬寫回C的賬戶则剃,此時B的轉(zhuǎn)賬請求過來了,B發(fā)現(xiàn)C的余額為20萬如捅,然后將其加10萬并寫回棍现。然后A的轉(zhuǎn)賬操作繼續(xù)——將30萬寫回C的余額。這種情況下C的最終余額為30萬镜遣,而非預(yù)期的40萬己肮。

1.2 可見性

可見性是指,當(dāng)多個線程并發(fā)訪問共享變量時悲关,一個線程對共享變量的修改谎僻,其它線程能夠立即看到≡⑷瑁可見性問題是好多人忽略或者理解錯誤的一點艘绍。

CPU從主內(nèi)存中讀數(shù)據(jù)的效率相對來說不高,現(xiàn)在主流的計算機(jī)中秫筏,都有幾級緩存诱鞠。每個線程讀取共享變量時,都會將該變量加載進(jìn)其對應(yīng)CPU的高速緩存里这敬,修改該變量后航夺,CPU會立即更新該緩存,但并不一定會立即將其寫回主內(nèi)存(實際上寫回主內(nèi)存的時間不可預(yù)期)崔涂。此時其它線程(尤其是不在同一個CPU上執(zhí)行的線程)訪問該變量時阳掐,從主內(nèi)存中讀到的就是舊的數(shù)據(jù),而非第一個線程更新后的數(shù)據(jù)冷蚂。

這一點是操作系統(tǒng)或者說是硬件層面的機(jī)制锚烦,所以很多應(yīng)用開發(fā)人員經(jīng)常會忽略。

1.3 有序性

有序性指的是帝雇,程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。以下面這段代碼為例:

boolean started = false; // 語句1
long counter = 0L; // 語句2
counter = 1; // 語句3
started = true; // 語句4

從代碼順序上看蛉拙,上面四條語句應(yīng)該依次執(zhí)行尸闸,但實際上JVM真正在執(zhí)行這段代碼時,并不保證它們一定完全按照此順序執(zhí)行孕锄。

處理器為了提高程序整體的執(zhí)行效率遏片,可能會對代碼進(jìn)行優(yōu)化氓皱,其中的一項優(yōu)化方式就是調(diào)整代碼順序,按照更高效的順序執(zhí)行代碼

講到這里缩膝,有人要著急了——什么,CPU不按照我的代碼順序執(zhí)行代碼膜楷,那怎么保證得到我們想要的效果呢?實際上抡砂,大家大可放心,CPU雖然并不保證完全按照代碼順序執(zhí)行恬涧,但它會保證程序最終的執(zhí)行結(jié)果和代碼順序執(zhí)行時的結(jié)果一致注益。

2 線程安全問題

2.1 競態(tài)條件與臨界區(qū)

線程之間共享堆空間,在編程的時候就要格外注意避免競態(tài)條件溯捆。危險在于多個線程同時訪問相同的資源并進(jìn)行讀寫操作丑搔。當(dāng)其中一個線程需要根據(jù)某個變量的狀態(tài)來相應(yīng)執(zhí)行某個操作的之前,該變量很可能已經(jīng)被其它線程修改提揍。

也就是說啤月,當(dāng)兩個線程競爭同一資源時,如果對資源的訪問順序敏感劳跃,就稱存在 競態(tài)條件谎仲。導(dǎo)致竟態(tài)條件發(fā)生的代碼稱作 臨界區(qū)

/**
 * 以下這段代碼就存在競態(tài)條件售碳,其中return ++count就是臨界區(qū)强重。
 */
public class Obj
{

    private int count;

    public int incr()
    {
        return ++count;
    }

}

2.2 死鎖

死鎖:指兩個或兩個以上的進(jìn)程(或線程)在執(zhí)行過程中,因爭奪資源而造成的一種互相等待的現(xiàn)象贸人,若無外力作用间景,它們都將無法推進(jìn)下去。此時稱系統(tǒng)處于死鎖狀態(tài)或系統(tǒng)產(chǎn)生了死鎖艺智,這些永遠(yuǎn)在互相等待的進(jìn)程稱為死鎖進(jìn)程倘要。

關(guān)于死鎖發(fā)生的條件:

  1. 互斥條件:線程對資源的訪問是排他性的,如果一個線程對占用了某資源十拣,那么其他線程必須處于等待狀態(tài)封拧,直到資源被釋放。
  2. 請求和保持條件:線程T1至少已經(jīng)保持了一個資源R1占用夭问,但又提出對另一個資源R2請求泽西,而此時,資源R2被其他線程T2占用缰趋,于是該線程T1也必須等待捧杉,但又對自己保持的資源R1不釋放。
  3. 不剝奪條件:線程已獲得的資源秘血,在未使用完之前味抖,不能被其他線程剝奪,只能在使用完以后由自己釋放灰粮。
  4. 環(huán)路等待條件:在死鎖發(fā)生時仔涩,必然存在一個“進(jìn)程-資源環(huán)形鏈”,即:{p0,p1,p2,...pn}粘舟,進(jìn)程p0(或線程)等待p1占用的資源熔脂,p1等待p2占用的資源佩研,pn等待p0占用的資源。(最直觀的理解是锤悄,p0等待p1占用的資源韧骗,而p1而在等待p0占用的資源,于是兩個進(jìn)程就相互等待)零聚。

2.3 活鎖

活鎖:是指線程1可以使用資源袍暴,但它很禮貌,讓其他線程先使用資源隶症,線程2也可以使用資源政模,但它很紳士,也讓其他線程先使用資源蚂会。這樣你讓我淋样,我讓你,最后兩個線程都無法使用資源胁住。

關(guān)于“死鎖與活鎖”的比喻

死鎖:迎面開來的汽車A和汽車B過馬路趁猴,汽車A得到了半條路的資源(滿足死鎖發(fā)生條件1:資源訪問是排他性的,我占了路你就不能上來彪见,除非你爬我頭上去)儡司,汽車B占了汽車A的另外半條路的資源,A想過去必須請求另一半被B占用的道路(死鎖發(fā)生條件2:必須整條車身的空間才能開過去余指,我已經(jīng)占了一半捕犬,尼瑪另一半的路被B占用了),B若想過去也必須等待A讓路酵镜,A是輛蘭博基尼碉碉,B是開奇瑞QQ的屌絲,A素質(zhì)比較低開窗對B狂罵:快給老子讓開淮韭,B很生氣垢粮,你媽逼的,老子就不讓(死鎖發(fā)生條件3:在未使用完資源前靠粪,不能被其他線程剝奪)足丢,于是兩者相互僵持一個都走不了(死鎖發(fā)生條件4:環(huán)路等待條件),而且導(dǎo)致整條道上的后續(xù)車輛也走不了庇配。

活鎖:馬路中間有條小橋,只能容納一輛車經(jīng)過绍些,橋兩頭開來兩輛車A和B捞慌,A比較禮貌,示意B先過柬批,B也比較禮貌啸澡,示意A先過袖订,結(jié)果兩人一直謙讓誰也過不去。

2.4 饑餓

饑餓:是指如果線程T1占用了資源R嗅虏,線程T2又請求封鎖R洛姑,于是T2等待。T3也請求資源R皮服,當(dāng)T1釋放了R上的封鎖后楞艾,系統(tǒng)首先批準(zhǔn)了T3的請求,T2仍然等待龄广。然后T4又請求封鎖R硫眯,當(dāng)T3釋放了R上的封鎖之后,系統(tǒng)又批準(zhǔn)了T4的請求......择同,T2可能永遠(yuǎn)等待两入。

也就是,如果一個線程因為CPU時間全部被其他線程搶走而得不到CPU運行時間敲才,這種狀態(tài)被稱之為“饑餓”裹纳。而該線程被“饑餓致死”正是因為它得不到CPU運行時間的機(jī)會

關(guān)于“饑餓”的比喻

在“首堵”北京的某一天紧武,天氣陰沉剃氧,空氣中充斥著霧霾和地溝油的味道,某個苦逼的臨時工交警正在處理塞車脏里,有兩條道A和B上都堵滿了車輛她我,其中A道堵的時間最長,B相對堵的時間較短迫横,這時番舆,前面道路已疏通,交警按照最佳分配原則矾踱,示意B道上車輛先過恨狈,B道路上過了一輛又一輛,A道上排隊時間最長的卻沒法通過呛讲,只能等B道上沒有車輛通過的時候再等交警發(fā)指令讓A道依次通過禾怠,這也就是ReentrantLock顯示鎖里提供的不公平鎖機(jī)制(當(dāng)然了,ReentrantLock也提供了公平鎖的機(jī)制贝搁,由用戶根據(jù)具體的使用場景而決定到底使用哪種鎖策略)吗氏,不公平鎖能夠提高吞吐量但不可避免的會造成某些線程的饑餓

在Java中雷逆,下面三個常見的原因會導(dǎo)致線程饑餓弦讽,如下:

  1. 高優(yōu)先級線程吞噬所有的低優(yōu)先級線程的CPU時間

    你能為每個線程設(shè)置獨自的線程優(yōu)先級,優(yōu)先級越高的線程獲得的CPU時間越多,線程優(yōu)先級值設(shè)置在1到10之間往产,而這些優(yōu)先級值所表示行為的準(zhǔn)確解釋則依賴于你的應(yīng)用運行平臺被碗。對大多數(shù)應(yīng)用來說,你最好是不要改變其優(yōu)先級值仿村。

  2. 線程被永久堵塞在一個等待進(jìn)入同步塊的狀態(tài)锐朴,因為其他線程總是能在它之前持續(xù)地對該同步塊進(jìn)行訪問

    Java的同步代碼區(qū)也是一個導(dǎo)致饑餓的因素。Java的同步代碼區(qū)對哪個線程允許進(jìn)入的次序沒有任何保障蔼囊。這就意味著理論上存在一個試圖進(jìn)入該同步區(qū)的線程處于被永久堵塞的風(fēng)險焚志,因為其他線程總是能持續(xù)地先于它獲得訪問,這即是“饑餓”問題压真,而一個線程被“饑餓致死”正是因為它得不到CPU運行時間的機(jī)會娩嚼。

  3. 線程在等待一個本身(在其上調(diào)用wait())也處于永久等待完成的對象,因為其他線程總是被持續(xù)地獲得喚醒

    如果多個線程處在wait()方法執(zhí)行上滴肿,而對其調(diào)用notify()不會保證哪一個線程會獲得喚醒岳悟,任何線程都有可能處于繼續(xù)等待的狀態(tài)。因此存在這樣一個風(fēng)險:一個等待線程從來得不到喚醒泼差,因為其他等待線程總是能被獲得喚醒贵少。

2.5 公平

解決饑餓的方案被稱之為“公平性” – 即所有線程均能公平地獲得運行機(jī)會。在Java中實現(xiàn)公平性方案堆缘,需要:

  1. 使用鎖滔灶,而不是同步塊;
  2. 使用公平鎖吼肥;
  3. 注意性能方面录平;

在Java中實現(xiàn)公平性,雖Java不可能實現(xiàn)100%的公平性缀皱,依然可以通過同步結(jié)構(gòu)在線程間實現(xiàn)公平性的提高斗这。

首先來學(xué)習(xí)一段簡單的同步態(tài)代碼:

public class Synchronizer{
    public synchronized void doSynchronized () {
        // do a lot of work which takes a long time
    }
}

如果有多個線程調(diào)用doSynchronized()方法,在第一個獲得訪問的線程未完成前啤斗,其他線程將一直處于阻塞狀態(tài)表箭,而且在這種多線程被阻塞的場景下,接下來將是哪個線程獲得訪問是沒有保障的钮莲。

改為 使用鎖方式替代同步塊免钻,為了提高等待線程的公平性,我們使用鎖方式來替代同步塊:

public class Synchronizer{
    Lock lock = new Lock();
    public void doSynchronized() throws InterruptedException{
        this.lock.lock();
        //critical section, do a lot of work which takes a long time
        this.lock.unlock();
    }
}

注意到doSynchronized()不再聲明為synchronized崔拥,而是用lock.lock()和lock.unlock()來替代极舔。下面是用Lock類做的一個實現(xiàn):

public class Lock{

    private boolean isLocked      = false;

    private Thread lockingThread = null;

    public synchronized void lock() throws InterruptedException{
        while(isLocked){
            wait();
        }

        isLocked = true;
        lockingThread = Thread.currentThread();
    }

    public synchronized void unlock(){

        if(this.lockingThread != Thread.currentThread()){
            throw new IllegalMonitorStateException("Calling thread has not locked this lock");
        }

        isLocked = false;
        lockingThread = null;
        notify();
    }
}

注意到上面對Lock的實現(xiàn),如果存在多線程并發(fā)訪問lock()链瓦,這些線程將阻塞在對lock()方法的訪問上姆怪。另外,如果鎖已經(jīng)鎖上(校對注:這里指的是isLocked等于true時),這些線程將阻塞在while(isLocked)循環(huán)的wait()調(diào)用里面稽揭。要記住的是,當(dāng)線程正在等待進(jìn)入lock() 時肥卡,可以調(diào)用wait()釋放其鎖實例對應(yīng)的同步鎖溪掀,使得其他多個線程可以進(jìn)入lock()方法,并調(diào)用wait()方法步鉴。

這回看下doSynchronized()揪胃,你會注意到在lock()和unlock()之間的注釋:在這兩個調(diào)用之間的代碼將運行很長一段時間。進(jìn)一步設(shè)想氛琢,這段代碼將長時間運行喊递,和進(jìn)入lock()并調(diào)用wait()來比較的話。這意味著大部分時間用在等待進(jìn)入鎖和進(jìn)入臨界區(qū)的過程是用在wait()的等待中阳似,而不是被阻塞在試圖進(jìn)入lock()方法中骚勘。

在早些時候提到過,同步塊不會對等待進(jìn)入的多個線程誰能獲得訪問做任何保障撮奏,同樣當(dāng)調(diào)用notify()時俏讹,wait()也不會做保障一定能喚醒線程。因此這個版本的Lock類和doSynchronized()那個版本就保障公平性而言畜吊,沒有任何區(qū)別泽疆。

但我們能夠改變這種情況,如下:

當(dāng)前的Lock類版本調(diào)用自己的wait()方法玲献,如果每個線程在不同的對象上調(diào)用wait()殉疼,那么只有一個線程會在該對象上調(diào)用wait(),Lock類可以決定哪個對象能對其調(diào)用notify()捌年,因此能做到有效的選擇喚醒哪個線程瓢娜。

下面將上面Lock類轉(zhuǎn)變?yōu)楣芥iFairLock。你會注意到新的實現(xiàn)和之前的Lock類中的同步和wait()/notify()稍有不同延窜。重點是恋腕,每一個調(diào)用lock()的線程都會進(jìn)入一個隊列,當(dāng)解鎖時逆瑞,只有隊列里的第一個線程被允許鎖住FairLock實例荠藤,所有其它的線程都將處于等待狀態(tài),直到他們處于隊列頭部获高。如下:

public class FairLock {
    private boolean isLocked = false;
    private Thread lockingThread = null;
    private List<QueueObject> waitingThreads = new ArrayList<QueueObject>();

    public void lock() throws InterruptedException{
        // 當(dāng)前線程創(chuàng)建“令牌”
        QueueObject queueObject = new QueueObject();
        boolean isLockedForThisThread = true;
        synchronized(this){
            // 所有線程的queueObject令牌哈肖,入隊
            waitingThreads.add(queueObject);
        }

        while(isLockedForThisThread){
            synchronized(this){
                // 1. 判斷是否已被鎖住:是否已有線程獲得鎖念秧,正在執(zhí)行同步代碼塊
                // 2. 判斷頭部令牌與當(dāng)前線程令牌是否一致:也就是只鎖住頭部令牌對應(yīng)的線程淤井;
                isLockedForThisThread = isLocked || waitingThreads.get(0) != queueObject;
                if(!isLockedForThisThread){
                    isLocked = true;
                    // 移除頭部令牌
                    waitingThreads.remove(queueObject);
                    lockingThread = Thread.currentThread();
                    return;
                }
            }
            try{
                // 其他線程執(zhí)行doWait(),進(jìn)行等待
                queueObject.doWait();
            }catch(InterruptedException e){
                synchronized(this) { waitingThreads.remove(queueObject); }
                throw e;
            }
        }
    }

    public synchronized void unlock(){
        if(this.lockingThread != Thread.currentThread()){
            throw new IllegalMonitorStateException("Calling thread has not locked this lock");
        }
        isLocked = false;
        lockingThread = null;
        if(waitingThreads.size() > 0) {
            // 喚醒頭部令牌對應(yīng)的線程,可以執(zhí)行
            waitingThreads.get(0).doNotify();
        }
    }
}

public class QueueObject {
    private boolean isNotified = false;

    public synchronized void doWait() throws InterruptedException {
        while(!isNotified){
            this.wait();
        }
        this.isNotified = false;
    }

    public synchronized void doNotify() {
        this.isNotified = true;
        this.notify();
    }

    public boolean equals(Object o) {
        return this == o;
    }
}

首先注意到lock()方法不在聲明為synchronized币狠,取而代之的是對必需同步的代碼游两,在synchronized中進(jìn)行嵌套

FairLock新創(chuàng)建了一個QueueObject的實例漩绵,并對每個調(diào)用lock()的線程進(jìn)行入隊操作贱案。調(diào)用unlock()的線程將從隊列頭部獲取QueueObject,并對其調(diào)用doNotify()止吐,以喚醒在該對象上等待的線程宝踪。通過這種方式,在同一時間僅有一個等待線程獲得喚醒碍扔,而不是所有的等待線程瘩燥。這也是實現(xiàn)FairLock公平性的核心所在。

還需注意到不同,QueueObject實際是一個semaphore厉膀。doWait()和doNotify()方法在QueueObject中保存著信號。這樣做以避免一個線程在調(diào)用queueObject.doWait()之前被另一個線程調(diào)用unlock()并隨之調(diào)用queueObject.doNotify()的線程重入套鹅,從而導(dǎo)致信號丟失站蝠。queueObject.doWait()調(diào)用放置在synchronized(this)塊之外,以避免被monitor嵌套鎖死卓鹿,所以另外的線程可以解鎖菱魔,只要當(dāng)沒有線程在lock方法的synchronized(this)塊中執(zhí)行即可。

最后吟孙,注意到queueObject.doWait()在try – catch塊中是怎樣調(diào)用的澜倦。在InterruptedException拋出的情況下,線程得以離開lock()杰妓,并需讓它從隊列中移除藻治。

3 如何確保線程安全特性

3.1 如何確保原子性

3.1.1 鎖和同步

常用的保證Java操作原子性的工具是 鎖和同步方法(或者同步代碼塊)。使用鎖巷挥,可以保證同一時間只有一個線程能拿到鎖桩卵,也就保證了同一時間只有一個線程能執(zhí)行申請鎖和釋放鎖之間的代碼。

public void testLock () {
    lock.lock();
    try{
        int j = i;
        i = j + 1;
    } finally {
        lock.unlock();
    }
}

與鎖類似的是同步方法或者同步代碼塊倍宾。使用非靜態(tài)同步方法時雏节,鎖住的是當(dāng)前實例;使用靜態(tài)同步方法時高职,鎖住的是該類的Class對象钩乍;使用靜態(tài)代碼塊時,鎖住的是synchronized關(guān)鍵字后面括號內(nèi)的對象怔锌。下面是同步代碼塊示例:

public void testLock () {
    synchronized (anyObject){
        int j = i;
        i = j + 1;
    }
}

無論使用鎖還是synchronized寥粹,本質(zhì)都是一樣变过,通過鎖或同步來實現(xiàn)資源的排它性,從而實際目標(biāo)代碼段同一時間只會被一個線程執(zhí)行涝涤,進(jìn)而保證了目標(biāo)代碼段的原子性媚狰。這是一種以犧牲性能為代價的方法

3.1.2 CAS(compare and swap)

基礎(chǔ)類型變量自增(i++)是一種常被新手誤以為是原子操作而實際不是的操作阔拳。Java中提供了對應(yīng)的原子操作類來實現(xiàn)該操作哈雏,并保證原子性,其本質(zhì)是利用了CPU級別的CAS指令衫生。由于是CPU級別的指令,其開銷比需要操作系統(tǒng)參與的鎖的開銷小土浸。AtomicInteger使用方法如下:

AtomicInteger atomicInteger = new AtomicInteger();
for(int b = 0; b < numThreads; b++) {
    new Thread(() -> {
        for(int a = 0; a < iteration; a++) {
            atomicInteger.incrementAndGet();
        }
    }).start();
}

3.2 如何確弊镎耄可見性

Java提供了volatile關(guān)鍵字來保證可見性。當(dāng)使用volatile修飾某個變量時黄伊,它會保證對該變量的修改會立即被更新到內(nèi)存中泪酱,并且將其它線程緩存中對該變量的緩存設(shè)置成無效,因此其它線程需要讀取該值時必須從主內(nèi)存中讀取还最,從而得到最新的值墓阀。

volatile適用場景:volatile適用于不需要保證原子性,但卻需要保證可見性的場景拓轻。一種典型的使用場景是用它修飾用于停止線程的狀態(tài)標(biāo)記斯撮。如下所示:

boolean isRunning = false;
public void start () {
    new Thread( () -> {
        while(isRunning) {
            someOperation();
        }
    }).start();
}
public void stop () {
    isRunning = false;
}

在這種實現(xiàn)方式下,即使其它線程通過調(diào)用stop()方法將isRunning設(shè)置為false扶叉,循環(huán)也不一定會立即結(jié)束勿锅。可以通過volatile關(guān)鍵字,保證while循環(huán)及時得到isRunning最新的狀態(tài)從而及時停止循環(huán)枣氧,結(jié)束線程溢十。

3.3 如何確保有序性

上文講過編譯器和處理器對指令進(jìn)行重新排序時,會保證重新排序后的執(zhí)行結(jié)果和代碼順序執(zhí)行的結(jié)果一致达吞,所以重新排序過程并不會影響單線程程序的執(zhí)行张弛,卻可能影響多線程程序并發(fā)執(zhí)行的正確性

Java中可通過volatile在一定程序上保證順序性酪劫,另外還可以通過synchronized和鎖來保證順序性吞鸭。

synchronized和鎖保證順序性的原理和保證原子性一樣,都是通過保證同一時間只會有一個線程執(zhí)行目標(biāo)代碼段來實現(xiàn)的契耿。

除了從應(yīng)用層面保證目標(biāo)代碼段執(zhí)行的順序性外瞒大,JVM還通過被稱為happens-before原則隱式地保證順序性。兩個操作的執(zhí)行順序只要可以通過happens-before推導(dǎo)出來搪桂,則JVM會保證其順序性透敌,反之JVM對其順序性不作任何保證盯滚,可對其進(jìn)行任意必要的重新排序以獲取高效率。

happens-before原則(先行發(fā)生原則)酗电,如下:

  1. 傳遞規(guī)則:如果操作1在操作2前面魄藕,而操作2在操作3前面,則操作1肯定會在操作3前發(fā)生撵术。該規(guī)則說明了happens-before原則具有傳遞性背率。
  2. 鎖定規(guī)則:一個unlock操作肯定會在后面對同一個鎖的lock操作前發(fā)生。這個很好理解嫩与,鎖只有被釋放了才會被再次獲取寝姿。
  3. volatile變量規(guī)則:對一個被volatile修飾的寫操作先發(fā)生于后面對該變量的讀操作。
  4. 程序次序規(guī)則:一個線程內(nèi)划滋,按照代碼順序執(zhí)行饵筑。
  5. 線程啟動規(guī)則:Thread對象的start()方法先發(fā)生于此線程的其它動作。
  6. 線程終結(jié)原則:線程的終止檢測后發(fā)生于線程中其它的所有操作处坪。
  7. 線程中斷規(guī)則: 對線程interrupt()方法的調(diào)用先發(fā)生于對該中斷異常的獲取根资。
  8. 對象終結(jié)規(guī)則:一個對象構(gòu)造先于它的finalize發(fā)生。

4 關(guān)于線程安全的幾個為什么

  1. 平時項目中使用鎖和synchronized比較多同窘,而很少使用volatile玄帕,難道就沒有保證可見性?

    鎖和synchronized即可以保證原子性想邦,也可以保證可見性裤纹。都是通過保證同一時間只有一個線程執(zhí)行目標(biāo)代碼段來實現(xiàn)的。

  2. 鎖和synchronized為何能保證可見性案狠?

    根據(jù)JDK 7的Java doc中對concurrent包的說明服傍,一個線程的寫結(jié)果保證對另外線程的讀操作可見,只要該寫操作可以由happen-before原則推斷出在讀操作之前發(fā)生骂铁。

  3. 既然鎖和synchronized即可保證原子性也可保證可見性吹零,為何還需要volatile?

    synchronized和鎖需要通過操作系統(tǒng)來仲裁誰獲得鎖拉庵,開銷比較高灿椅,而volatile開銷小很多。因此在只需要保證可見性的條件下钞支,使用volatile的性能要比使用鎖和synchronized高得多茫蛹。

  4. 既然鎖和synchronized可以保證原子性,為什么還需要AtomicInteger這種的類來保證原子操作烁挟?

    鎖和synchronized需要通過操作系統(tǒng)來仲裁誰獲得鎖婴洼,開銷比較高,而AtomicInteger是通過CPU級的CAS操作來保證原子性撼嗓,開銷比較小柬采。所以使用AtomicInteger的目的還是為了提高性能欢唾。

  5. 還有沒有別的辦法保證線程安全?

    有粉捻。盡可能避免引起非線程安全的條件——共享變量礁遣。如果能從設(shè)計上避免共享變量的使用,即可避免非線程安全的發(fā)生肩刃,也就無須通過鎖或者synchronized以及volatile解決原子性祟霍、可見性和順序性的問題

  6. synchronized即可修飾非靜態(tài)方式盈包,也可修飾靜態(tài)方法沸呐,還可修飾代碼塊,有何區(qū)別呢燥?

    synchronized修飾非靜態(tài)同步方法時垂谢,鎖住的是當(dāng)前實例;synchronized修飾靜態(tài)同步方法時疮茄,鎖住的是該類的Class對象;synchronized修飾靜態(tài)代碼塊時根暑,鎖住的是synchronized關(guān)鍵字后面括號內(nèi)的對象力试。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市排嫌,隨后出現(xiàn)的幾起案子畸裳,更是在濱河造成了極大的恐慌,老刑警劉巖淳地,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件怖糊,死亡現(xiàn)場離奇詭異,居然都是意外死亡颇象,警方通過查閱死者的電腦和手機(jī)伍伤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來遣钳,“玉大人扰魂,你說我怎么就攤上這事≡誊睿” “怎么了劝评?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長倦淀。 經(jīng)常有香客問我蒋畜,道長,這世上最難降的妖魔是什么撞叽? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任姻成,我火速辦了婚禮插龄,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘佣渴。我一直安慰自己辫狼,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布辛润。 她就那樣靜靜地躺著膨处,像睡著了一般。 火紅的嫁衣襯著肌膚如雪砂竖。 梳的紋絲不亂的頭發(fā)上真椿,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天,我揣著相機(jī)與錄音乎澄,去河邊找鬼突硝。 笑死,一個胖子當(dāng)著我的面吹牛置济,可吹牛的內(nèi)容都是我干的解恰。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼浙于,長吁一口氣:“原來是場噩夢啊……” “哼护盈!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起羞酗,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤腐宋,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后檀轨,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體胸竞,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年参萄,在試婚紗的時候發(fā)現(xiàn)自己被綠了卫枝。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡讹挎,死狀恐怖剃盾,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情淤袜,我是刑警寧澤痒谴,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站铡羡,受9級特大地震影響积蔚,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜烦周,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一尽爆、第九天 我趴在偏房一處隱蔽的房頂上張望怎顾。 院中可真熱鬧,春花似錦漱贱、人聲如沸槐雾。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽募强。三九已至,卻和暖如春崇摄,著一層夾襖步出監(jiān)牢的瞬間擎值,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工逐抑, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留鸠儿,地道東北人。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓厕氨,卻偏偏與公主長得像进每,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子命斧,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,762評論 2 345

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