Java進(jìn)階系列(二)當(dāng)我們說(shuō)線程安全時(shí),到底在說(shuō)什么

原創(chuàng)文章,轉(zhuǎn)載請(qǐng)務(wù)必將下面這段話置于文章開(kāi)頭處。
  本文轉(zhuǎn)發(fā)自Jason's Blog,
  原文鏈接 http://www.jasongj.com/java/thread_safe/

多線程編程中的三個(gè)核心概念

原子性

這一點(diǎn),跟數(shù)據(jù)庫(kù)事務(wù)的原子性概念差不多,即一個(gè)操作(有可能包含有多個(gè)子操作)要么全部執(zhí)行(生效),要么全部都不執(zhí)行(都不生效)。

關(guān)于原子性,一個(gè)非常經(jīng)典的例子就是銀行轉(zhuǎn)賬問(wèn)題:比如A和B同時(shí)向C轉(zhuǎn)賬10萬(wàn)元。如果轉(zhuǎn)賬操作不具有原子性,A在向C轉(zhuǎn)賬時(shí),讀取了C的余額為20萬(wàn),然后加上轉(zhuǎn)賬的10萬(wàn),計(jì)算出此時(shí)應(yīng)該有30萬(wàn),但還未來(lái)及將30萬(wàn)寫回C的賬戶,此時(shí)B的轉(zhuǎn)賬請(qǐng)求過(guò)來(lái)了,B發(fā)現(xiàn)C的余額為20萬(wàn),然后將其加10萬(wàn)并寫回。然后A的轉(zhuǎn)賬操作技術(shù)——將30萬(wàn)寫回C的余額。這種情況下C的最終余額為30萬(wàn),而非預(yù)期的40萬(wàn)。

可見(jiàn)性

可見(jiàn)性是指,當(dāng)多個(gè)線程并發(fā)訪問(wèn)共享變量時(shí),一個(gè)線程共享變量的修改,其它線程能夠立即看到??梢?jiàn)性問(wèn)題是好多個(gè)忽略或者理解錯(cuò)誤的一點(diǎn)。

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

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

順序性

順序性指的是,程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。

以下面這段代碼為例

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

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

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

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

Java如何解決多線程并發(fā)問(wèn)題

Java如何保證原子性

鎖和同步

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

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

與鎖類似的是同步方法或者同步代碼塊。使用非靜態(tài)同步方法時(shí),鎖住的是當(dāng)前實(shí)例;使用靜態(tài)同步方法時(shí),鎖住的是該類的Class對(duì)象;使用靜態(tài)代碼塊時(shí),鎖住的是synchronized關(guān)鍵字后面括號(hào)內(nèi)的對(duì)象。下面是同步代碼塊示例

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

無(wú)論使用鎖還是synchronized,本質(zhì)都是一樣,通過(guò)鎖來(lái)實(shí)現(xiàn)資源的排它性,從而實(shí)際目標(biāo)代碼段同一時(shí)間只會(huì)被一個(gè)線程執(zhí)行,進(jìn)而保證了目標(biāo)代碼段的原子性。這是一種以犧牲性能為代價(jià)的方法。

CAS(compare and swap)

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

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

Java如何保證可見(jiàn)性

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

Java如何保證順序性

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

Java中可通過(guò)volatile在一定程序上保證順序性,另外還可以通過(guò)synchronized和鎖來(lái)保證順序性。

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

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

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

  • 傳遞規(guī)則:如果操作1在操作2前面,而操作2在操作3前面,則操作1肯定會(huì)在操作3前發(fā)生。該規(guī)則說(shuō)明了happens-before原則具有傳遞性
  • 鎖定規(guī)則:一個(gè)unlock操作肯定會(huì)在后面對(duì)同一個(gè)鎖的lock操作前發(fā)生。這個(gè)很好理解,鎖只有被釋放了才會(huì)被再次獲取
  • volatile變量規(guī)則:對(duì)一個(gè)被volatile修飾的寫操作先發(fā)生于后面對(duì)該變量的讀操作
  • 程序次序規(guī)則:一個(gè)線程內(nèi),按照代碼順序執(zhí)行
  • 線程啟動(dòng)規(guī)則:Thread對(duì)象的start()方法先發(fā)生于此線程的其它動(dòng)作
  • 線程終結(jié)原則:線程的終止檢測(cè)后發(fā)生于線程中其它的所有操作
  • 線程中斷規(guī)則: 對(duì)線程interrupt()方法的調(diào)用先發(fā)生于對(duì)該中斷異常的獲取
  • 對(duì)象終結(jié)規(guī)則:一個(gè)對(duì)象構(gòu)造先于它的finalize發(fā)生

volatile適用場(chǎng)景

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

boolean isRunning = false;

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

public void stop () {
  isRunning = false;
}

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

線程安全十萬(wàn)個(gè)為什么

問(wèn):平時(shí)項(xiàng)目中使用鎖和synchronized比較多,而很少使用volatile,難道就沒(méi)有保證可見(jiàn)性?
答:鎖和synchronized即可以保證原子性,也可以保證可見(jiàn)性。都是通過(guò)保證同一時(shí)間只有一個(gè)線程執(zhí)行目標(biāo)代碼段來(lái)實(shí)現(xiàn)的。

問(wèn):既然鎖和synchronized即可保證原子性也可保證可見(jiàn)性,為何還需要volatile?
答:synchronized和鎖需要通過(guò)操作系統(tǒng)來(lái)仲裁誰(shuí)獲得鎖,開(kāi)銷比較高,而volatile開(kāi)銷小很多。因此在只需要保證可見(jiàn)性的條件下,使用volatile的性能要比使用鎖和synchronized高得多。

問(wèn):既然鎖和synchronized可以保證原子性,為什么還需要AtomicInteger這種的類來(lái)保證原子操作?
答:鎖和synchronized需要通過(guò)操作系統(tǒng)來(lái)仲裁誰(shuí)獲得鎖,開(kāi)銷比較高,而AtomicInteger是通過(guò)CPU級(jí)的CAS操作來(lái)保證原子性,開(kāi)銷比較小。所以使用AtomicInteger的目的還是為了提高性能。

問(wèn):還有沒(méi)有別的辦法保證線程安全
答:有。盡可能避免引起非線程安全的條件——共享變量。如果能從設(shè)計(jì)上避免共享變量的使用,即可避免非線程安全的發(fā)生,也就無(wú)須通過(guò)鎖或者synchronized以及volatile解決原子性、可見(jiàn)性和順序性的問(wèn)題。

問(wèn):synchronized即可修飾非靜態(tài)方式,也可修飾靜態(tài)方法,還可修飾代碼塊,有何區(qū)別
答:synchronized修飾非靜態(tài)同步方法時(shí),鎖住的是當(dāng)前實(shí)例;synchronized修飾靜態(tài)同步方法時(shí),鎖住的是該類的Class對(duì)象;synchronized修飾靜態(tài)代碼塊時(shí),鎖住的是synchronized關(guān)鍵字后面括號(hào)內(nèi)的對(duì)象。

Java進(jìn)階系列

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

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

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