06. 就該這么學(xué)并發(fā) - 如何保證線程安全

前言

上節(jié),我們對線程安全有了較全面的認(rèn)知.

我們知道, 線程之所以不安全, 主要是多線程下對可變的共享資源的爭用導(dǎo)致的.

衡量線程是否安全, 主要從三個特性入手

  • 原子性
  • 可見效
  • 有序性

只要保證了這三個特性,我們就認(rèn)為線程是安全的, 多線程下執(zhí)行結(jié)果才會和單線程執(zhí)行結(jié)果統(tǒng)一起來.

本章,我們就來聊聊如何保證線程安全的問題.

如何保證原子性

常用的保證Java操作原子性的工具是鎖和同步方法(或者同步代碼塊).

我們舉個例子:

public class Test {
    private static int count = 0;

    public static void addCount() {
         count++;
    }
    public static void main(String[] args) throws Exception {
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 1000; j++) {
                        addCount();
                    }
                }
            });
            thread.start();
        }
        // 主線程睡眠1s,保證子線程都執(zhí)行完畢
        Thread.sleep(1000);
        System.out.println("count=" + count);
    }
}

可以看出,
子線程計數(shù)器累加到1000,
然后主線程創(chuàng)建了10個子線程來跑,
所以,最終結(jié)果是應(yīng)該是10000,
但是大家運(yùn)行代碼看看, 發(fā)現(xiàn)各種錯誤的輸出都有!

原因就是 “count++”這個操作不是我們以為的原子操作, 它其實(shí)是三步操作

  • 從主存中讀取count的值,復(fù)制一份到CPU寄存器

  • CPU寄存器中,CPU執(zhí)行指令對 count 進(jìn)行加1 操作

  • 把count重新刷新到主存

單線程當(dāng)然沒有問題, 但當(dāng)多線程時, 就會存在問題.

所以我們必須解決這個問題!

使用鎖, 可以保證

同一時間只有一個線程能拿到鎖,也就保證了同一時間只有一個線程能執(zhí)行申請鎖和釋放鎖之間的代碼.

使用方式

 // 聲明一個鎖
 private static ReentrantLock lock = new ReentrantLock();

 public static void addCount() {
     lock.lock();
     try {
           count++;
     } finally {
           lock.unlock();
     }
 }

需要強(qiáng)調(diào)的是

try{
  //加鎖代碼
}finally{
  lock.unlock();
}

防止異常導(dǎo)致鎖一直無法釋放!

同步方法

與鎖類似的是同步方法或者同步代碼塊,
Java使用關(guān)鍵字synchronized進(jìn)行同步.
需要注意的是, synchronized是有作用范圍的.

synchronized的作用范圍:

  • 修飾非靜態(tài)方法(或成員變量),鎖的是this對象, 就是類的實(shí)例對象(即: 對象鎖)
public synchronized void addCount() {
    count++;
}
  • 修飾靜態(tài)方法(或成員變量), 鎖的是Class對象本身, 因?yàn)殪o態(tài)成員不專屬于任何一個實(shí)例對象 (即: 類鎖)
public static synchronized void addCount() {
    count++;
}
  • 修飾代碼塊時, 鎖住的是synchronized關(guān)鍵字后面括號內(nèi)的對象.
public class Test{
  private Object object = new Object();
  public void addCount() {
      //此時,鎖住的是object對象變量
      synchronized (object) {
          count++;
      }

      //此時鎖住的是當(dāng)前實(shí)例對象
       synchronized (this) {
          count++;
      }

      //此時鎖住的是當(dāng)前Test類的class對象
       synchronized (Test.class) {
          count++;
      }
  }  
}

無論使用鎖還是synchronized, 本質(zhì)都是一樣

通過鎖或同步來實(shí)現(xiàn)資源的排它性,
從而實(shí)際目標(biāo)代碼段同一時間只會被一個線程執(zhí)行,
進(jìn)而保證了目標(biāo)代碼段的原子性.

悲觀鎖和樂觀鎖

  • 悲觀鎖
處理數(shù)據(jù)時,假設(shè)會有其他外部修改, 所以每次都會鎖住數(shù)據(jù), 防止外部的操作. 
  • 樂觀鎖
處理數(shù)據(jù)時, 不加鎖而是假設(shè)沒有沖突而去完成某項(xiàng)操作,如果因?yàn)闆_突失敗就重試,直到成功為止.

初一看, 大家可能會任務(wù)樂觀鎖好像比悲觀鎖性能高,其實(shí)也要看具體場景! 因?yàn)闃酚^鎖的重試機(jī)制, 所以當(dāng)并發(fā)量很高的時候, 重試的次數(shù)就會劇增, 此時, 顯然性能是不如悲觀鎖的!

顯而易見, 鎖或同步就是悲觀鎖, 它們以"犧牲性能"來保證原子性.

那么, 有沒有無需加鎖也能保證原子性的方式呢?

CAS無鎖

CAS 是英文單詞 Compare And Swap 的縮寫,翻譯過來就是比較并替換.
CAS有3個操作數(shù),內(nèi)存值V, 舊的預(yù)期值A(chǔ),要修改的新值B.
當(dāng)且僅當(dāng)預(yù)期值A(chǔ)和內(nèi)存值V相同時, 將內(nèi)存值V修改為B,否則什么都不做.

我們舉個例子:

假設(shè) V = 10;
線程1想要使得V的值加1, 按CAS, 此時, A=10, B = 11;
線程2突然修改了V=11;
線程1發(fā)現(xiàn), (A=10) != (V=11), 所以, 不允許更新!

image.png

CAS是一種樂觀鎖的機(jī)制,它不會阻塞任何線程. 所以在效率上,它會比 鎖和同步要高.

上文中我們說“count++”自增操作不是原子的, 這導(dǎo)致了并發(fā)問題, 那么如何解決呢?

Java提供了并發(fā)原子類AtomicInteger來解決自增操作原子性的問題,其底層就是使用了CAS原理

 private static AtomicInteger count = new AtomicInteger();
 public static void addCount() {
        count.incrementAndGet();
 }

CAS雖然在普通場景下優(yōu)于鎖和同步, 但是同時引入了一個“ABA”問題!

ABA問題:
我們還舉上一個例子:

假設(shè) V = 10;
線程1想要使得V的值加1, 按CAS, 此時, A=10, B = 11;
線程2突然修改了V=11;
線程3突然修改了V=10;
線程1發(fā)現(xiàn), (A=10) = (V=10), 所以, 允許更新!

雖然數(shù)字結(jié)果上沒有問題, 但是如果需要追溯過程就會存在漏洞!
因?yàn)镃AS把線程3修改的V=10,當(dāng)成了V的初始值10, 認(rèn)為它從未更改過!

針對ABA問題,雖然也能通過增加版本號等等來解決, 不過有句忠告:

使用CAS要考慮清楚“ABA”問題是否會影響程序并發(fā)的正確性,如果需要解決ABA問題,改用鎖或同步可能更高效

可重入鎖

介紹完以上知識,不知道大家關(guān)于“鎖”的使用,有沒有這樣的疑惑

A線程對某個對象加鎖后, 在A線程內(nèi)部如果再次要獲取同一個對象的鎖,會怎樣? 會不會死鎖?

針對這樣的問題, 提出了可重入鎖這個東西!

所謂可重入鎖,指的是以線程為單位,當(dāng)一個線程獲取對象鎖之后,
這個線程可以再次獲取本對象上的鎖,而其他的線程是不可以的.
(同一個加鎖線程自己調(diào)用自己不會發(fā)生死鎖情況)

可重入鎖是為了防止死鎖

它的實(shí)現(xiàn)原理是

通過為每個鎖關(guān)聯(lián)一個請求計數(shù)和一個占有它的線程.
當(dāng)計數(shù)為 0 時,認(rèn)為鎖是未被占有的.
線程請求一個未被占有的鎖時, jvm 將記錄鎖的占有者,并且將請求計數(shù)器置為 1 .
如果同一個線程再次請求這個鎖,計數(shù)將遞增;
每次占用線程退出同步塊,計數(shù)器值將遞減.
直到計數(shù)器為0,鎖被釋放.

synchronized 和 ReentrantLock 都是可重入鎖

  • ReentrantLock 表現(xiàn)為 API 層面的互斥鎖(lock() 和 unlock() 方法配合 try/finally 語句塊來完成);

  • synchronized 表現(xiàn)為原生語法層面的互斥鎖.

如何保證可見性

Java提供了volatile關(guān)鍵字來保證可見性.

當(dāng)使用volatile修飾某個變量時,
它會保證對該變量的修改會立即被更新到內(nèi)存中,
并且將其它線程緩存中對該變量的緩存設(shè)置成無效
因此其它線程需要讀取該值時必須從主內(nèi)存中讀取,
從而得到最新的值.

我們還舉介紹可見性時的例子,

private static volatile boolean isRuning = false;

如果用volatile來修飾isRuning,
再運(yùn)行你會發(fā)現(xiàn), 程序能得到預(yù)期結(jié)果了.

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

關(guān)于“不需要保證原子性”這點(diǎn), 大家可以參考介紹“原子性”的那個案例(多線程count++),
將count定義為volatile修飾的變量

private static volatile int count = 0;

運(yùn)行你會發(fā)現(xiàn)最終結(jié)果并不是預(yù)期值, 原因就在于:

兩個線程A,B同時進(jìn)行count++,
count++是三步操作

  • 從主存中讀取count的值,復(fù)制一份到CPU寄存器

  • CPU寄存器中,CPU執(zhí)行指令對 count 進(jìn)行加1 操作

  • 把count重新刷新到主存

假設(shè)count = 1
A和B讀取count都是1,復(fù)制到各自的緩存中
假設(shè)A先執(zhí)行完了, 將count = 2回寫進(jìn)主存, 因?yàn)関olatile, 所以通知其它線程count值有更新.
B呢,此時正好執(zhí)行到最后一步,于是保存的是2,而不是我們認(rèn)為的3!

如何保證有序性

針對編譯器和處理器對指令進(jìn)行重新排序時,可能影響多線程程序并發(fā)執(zhí)行的正確性問題,

Java中可通過volatile關(guān)鍵字在一定程序上保證順序性, 另外還可以通過鎖和同步(synchronized)來保證順序性.

事實(shí)上, 鎖和synchronized即可以保證原子性,也可以保證可見性以及順序性.因?yàn)樗鼈兪峭ㄟ^保證同一時間只有一個線程執(zhí)行目標(biāo)代碼段來實(shí)現(xiàn)的.

鎖和synchronized可以“勝任”一切,為什么還需要volatile?

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

除了從應(yīng)用層面保證目標(biāo)代碼段執(zhí)行的順序性外,
JVM還通過被稱為happens-before原則隱式地保證順序性.

兩個操作的執(zhí)行順序只要可以通過happens-before推導(dǎo)出來,
則JVM會保證其順序性,
反之JVM對其順序性不作任何保證,可對其進(jìn)行任意必要的重新排序以獲取高效率.

happens-before

在JMM(Java內(nèi)存模型)中,

如果一個操作的執(zhí)行結(jié)果需要對另一個操作可見,
那么這兩個操作之間必須要存在happens-before關(guān)系,
這兩個操作既可以在同一個線程,也可以在不同的兩個線程中.

我們需要關(guān)注的happens-before規(guī)則如下:

  • 傳遞規(guī)則
如果操作A先行發(fā)生于操作B,而操作B又先行發(fā)生于操作C,則可以得出操作A先行發(fā)生于操作C
  • 鎖定規(guī)則
一個unlock操作肯定會在后面對同一個鎖的lock操作前發(fā)生, 鎖只有被釋放了才會被再次獲取
  • volatile變量規(guī)則
對一個volatile修飾的變量的寫操作先行發(fā)生于后面對這個變量的讀操作
  • 程序次序規(guī)則
一個線程內(nèi),按照代碼順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作
  • 線程啟動規(guī)則
Thread對象的start()方法先發(fā)生于此線程的其它動作
  • 線程終結(jié)原則
線程中所有的操作都先行發(fā)生于線程的終止檢測, 我們可以通過Thread.join()方法結(jié)束, Thread.isAlive()的返回值手段檢測到線程已經(jīng)終止執(zhí)行(所有終結(jié)的線程都不可再用)
  • 線程中斷規(guī)則
對線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測到中斷事件的發(fā)生
  • 對象終結(jié)規(guī)則
一個對象的初始化完成先行發(fā)生于他的finalize()方法的開始

歡迎關(guān)注我

技術(shù)公眾號 “CTO技術(shù)”

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市亡脸,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌管呵,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,454評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件姐帚,死亡現(xiàn)場離奇詭異恩沛,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)胚股,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來裙秋,“玉大人琅拌,你說我怎么就攤上這事≌蹋” “怎么了进宝?”我有些...
    開封第一講書人閱讀 157,921評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長枷恕。 經(jīng)常有香客問我党晋,道長,這世上最難降的妖魔是什么徐块? 我笑而不...
    開封第一講書人閱讀 56,648評論 1 284
  • 正文 為了忘掉前任未玻,我火速辦了婚禮,結(jié)果婚禮上胡控,老公的妹妹穿的比我還像新娘扳剿。我一直安慰自己,他們只是感情好昼激,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,770評論 6 386
  • 文/花漫 我一把揭開白布庇绽。 她就那樣靜靜地躺著,像睡著了一般橙困。 火紅的嫁衣襯著肌膚如雪瞧掺。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,950評論 1 291
  • 那天凡傅,我揣著相機(jī)與錄音辟狈,去河邊找鬼。 笑死像捶,一個胖子當(dāng)著我的面吹牛上陕,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播拓春,決...
    沈念sama閱讀 39,090評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼释簿,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了硼莽?” 一聲冷哼從身側(cè)響起庶溶,我...
    開封第一講書人閱讀 37,817評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎懂鸵,沒想到半個月后偏螺,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,275評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡匆光,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,592評論 2 327
  • 正文 我和宋清朗相戀三年套像,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片终息。...
    茶點(diǎn)故事閱讀 38,724評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡夺巩,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出周崭,到底是詐尸還是另有隱情柳譬,我是刑警寧澤,帶...
    沈念sama閱讀 34,409評論 4 333
  • 正文 年R本政府宣布续镇,位于F島的核電站美澳,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏摸航。R本人自食惡果不足惜制跟,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,052評論 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望酱虎。 院中可真熱鬧凫岖,春花似錦、人聲如沸逢净。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽糠悯。三九已至难述,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間胀茵,已是汗流浹背社露。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留琼娘,地道東北人峭弟。 一個月前我還...
    沈念sama閱讀 46,503評論 2 361
  • 正文 我出身青樓附鸽,卻偏偏與公主長得像,于是被迫代替她去往敵國和親瞒瘸。 傳聞我的和親對象是個殘疾皇子坷备,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,627評論 2 350