自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

面試突然問Java多線程原理,我哭了!

原創(chuàng)
開發(fā) 后端 開發(fā)工具
應(yīng)用開發(fā)隨著業(yè)務(wù)量的增加,數(shù)據(jù)量也在不斷增加,為了應(yīng)對(duì)海量的數(shù)據(jù),通常會(huì)采用多線程的方式處理數(shù)據(jù)。

【51CTO.com原創(chuàng)稿件】應(yīng)用開發(fā)隨著業(yè)務(wù)量的增加,數(shù)據(jù)量也在不斷增加,為了應(yīng)對(duì)海量的數(shù)據(jù),通常會(huì)采用多線程的方式處理數(shù)據(jù)。

[[285995]] 

圖片來自 Pexels

談到 Java 的多線程編程,一定繞不開線程的安全性,線程安全又包括原子性,可見性和有序性等特性。今天,我們就來看看他們之間的關(guān)聯(lián)和實(shí)現(xiàn)原理。

線程與競態(tài)

開發(fā)的應(yīng)用程序會(huì)在一個(gè)進(jìn)程中運(yùn)行,換句話說進(jìn)程就是程序的運(yùn)行實(shí)例。運(yùn)行一個(gè) Java 程序的實(shí)質(zhì)就是運(yùn)行了一個(gè) Java 虛擬機(jī)進(jìn)程。

如果說一個(gè)進(jìn)程可以包括多個(gè)線程,并且這些線程會(huì)共享進(jìn)程中的資源。任何一段代碼會(huì)運(yùn)行在一個(gè)線程中,也運(yùn)行在多個(gè)線程中。線程所要完成的計(jì)算被稱為任務(wù)。

為了提高程序的效率,我們會(huì)生成多個(gè)任務(wù)一起工作,這種工作模式有可能是并行的,A 任務(wù)在執(zhí)行的時(shí)候,B 任務(wù)也在執(zhí)行。

如果多個(gè)任務(wù)(線程)在執(zhí)行過程中,操作相同的資源(變量),這個(gè)資源(變量)被稱為共享資源(變量)。

當(dāng)多個(gè)線程同時(shí)對(duì)共享資源進(jìn)行操作時(shí),例如:寫資源,就會(huì)出現(xiàn)競態(tài),它會(huì)導(dǎo)致被操作的資源在不同時(shí)間看到的結(jié)果不同。

來看看多個(gè)線程訪問相同的資源/變量的例子如下:

 

當(dāng)線程 A 和 B 同時(shí)執(zhí)行 Counter 對(duì)象中的 add() 方法時(shí),在無法知道兩個(gè)線程如何切換的情況下,JVM 會(huì)按照下面的順序來執(zhí)行代碼:

  • 從內(nèi)存獲取 this.count 的值放到寄存器。
  • 將寄存器中的值增加 value。
  • 將寄存器中的值寫回內(nèi)存。

上面操作在線程 A 和 B 交錯(cuò)執(zhí)行時(shí),會(huì)出現(xiàn)以下情況:

 

兩個(gè)線程分別加 2 和 3 到 count 變量上,我們希望的結(jié)果是,兩個(gè)線程執(zhí)行后 count 的值等于 5。

但是,兩個(gè)線程交叉執(zhí)行,即使兩個(gè)線程從內(nèi)存中讀出的初始值都是 0,之后各自加了 2 和 3,并分別寫回內(nèi)存。

然而,最終的值并不是期望的 5,而是最后寫回內(nèi)存的那個(gè)線程(A 線程)的值(3)。

最后寫回內(nèi)存的是線程 A 所以結(jié)果是 3,但也有可能是線程 B 最后寫回內(nèi)存,所以結(jié)果是不可知的。

因此,如果沒有采用同步機(jī)制,線程間的交叉寫資源/變量,結(jié)果是不可控的。

我們把這種一個(gè)計(jì)算結(jié)果的正確性與時(shí)間有關(guān)的現(xiàn)象稱作競態(tài)(Race Condition)。

線程安全

前面我們談到,當(dāng)多線程同時(shí)寫一個(gè)資源/變量的時(shí)候會(huì)出現(xiàn)競態(tài)的情況。這種情況的發(fā)生會(huì)造成,最終結(jié)果的不確定性。

如果把這個(gè)被寫的資源看成 Java 中的一個(gè)類的話,這個(gè)類不是線程安全的。

即便這個(gè)類在單線程環(huán)境下運(yùn)作正常,但在多線程環(huán)境下就無法正常運(yùn)行。例如:ArrayList,HashMap,SimpledateFormat。

那么,為了做到線程安全,需要從以下三個(gè)方面考慮,分別是:

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

原子性

原子性是指某個(gè)操作或者多個(gè)操作,要么全部執(zhí)行,要么就都不執(zhí)行,不會(huì)出現(xiàn)中間過程。換成線程執(zhí)行也一樣,線程中執(zhí)行的操作要么不執(zhí)行,要么全部執(zhí)行。

例如,在 Java 中,基本數(shù)據(jù)類型讀取操作就是原子性操作,來看下面的語句:

  • x = 10
  • x = x + 1

第一句是原子性操作,因?yàn)槠渲苯訉?shù)值 10 賦值給 x,線程執(zhí)行這個(gè)語句時(shí)直接將 10 寫到內(nèi)存中。

第二句包含三個(gè)操作,讀取 x 的值,進(jìn)行加 1 操作,寫入新的值。這三個(gè)操作合起來就不是原子性操作了。

Java 中的原子包括:

  • lock(鎖定)
  • unlock(解鎖)
  • read(讀取)
  • load(載入)
  • use(使用)
  • assign(賦值)
  • store(存儲(chǔ))
  • write(寫入)

假設(shè) A,B 兩個(gè)線程一起執(zhí)行語句 2,同時(shí)對(duì) x 變量進(jìn)行寫操作,由于不滿足原子性操作,因此得到的結(jié)果也是不確定的。在 Java 中有兩種方式來實(shí)現(xiàn)原子性。一種是使用鎖(Lock)。

鎖具有排他性,在多線程訪問時(shí),能夠保障一個(gè)共享變量在任意時(shí)刻都能夠被一個(gè)線程訪問,也就是排除了競態(tài)的可能。

另一種是利用處理器提供的 CAS(Compare-and-Swap)指令實(shí)現(xiàn),它是直接在硬件(處理器和內(nèi)存)這一層實(shí)現(xiàn)的,被稱為“硬件鎖”。

可見性

說完了原子性,再來談?wù)効梢娦浴C缙湟?,在多線程訪問中,一個(gè)線程對(duì)某個(gè)共享變量進(jìn)行更新以后,后續(xù)訪問的線程可以立即讀取更新的結(jié)果。

這種情況就稱作可見,對(duì)共享變量的更新對(duì)其他線程是可見的,否則就稱不可見。

來看看 Java 是如何實(shí)現(xiàn)可見性的。首先,假設(shè)線程 A 執(zhí)行了一段指令,這個(gè)指令在 CPU A 中運(yùn)行。

這個(gè)指令寫的共享變量會(huì)存放在 CPU A 的寄存器中。當(dāng)指令執(zhí)行完畢以后,會(huì)將共享變量從寄存器中寫入到內(nèi)存中。

PS:實(shí)際上是通過寄存器到高速緩存,再到寫緩沖器和無序化隊(duì)列,最后寫到內(nèi)存的。

這里為了舉例,采用了簡化的說法。這樣做的目的是,讓共享變量能夠被另外一個(gè) CPU 中的線程訪問到。

因此,共享變量寫入到內(nèi)存的行為稱為“沖刷處理器緩存”。也就是把共享變量從處理器緩存,沖刷到內(nèi)存中。

 

沖刷處理器緩存

此時(shí),線程 B 剛好運(yùn)行在 CPU B 上,指令為了獲取共享變量,需要從內(nèi)存中的共享變量進(jìn)行同步。

這個(gè)緩存同步的過程被稱為,“刷新處理器緩存”。也就是從內(nèi)存中刷新緩存到處理器的寄存器中。

經(jīng)過這兩個(gè)步驟以后,運(yùn)行在 CPU B 上的線程就能夠同步到,CPU A 上線程處理的共享變量來。也保證了共享變量的可見性。

 

刷新處理器緩存

有序性

說完了可見性,再來聊聊有序性。Java 編譯器針對(duì)代碼執(zhí)行的順序會(huì)有調(diào)整。

它有可能改變兩個(gè)操作執(zhí)行的先后順序,另外一個(gè)處理器上執(zhí)行的多個(gè)操作,從其他處理器的角度來看,指令的執(zhí)行順序有可能也是不一致的。

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

究其原因,編譯器出于性能的考慮,在不影響程序(單線程程序)正確性的情況下,對(duì)源代碼順序進(jìn)行調(diào)整。

在 Java 中,可以通過 Volatile 關(guān)鍵字來保證“有序性”。還可以通過 Synchronized 和 Lock 來保證有序性。后面還會(huì)介紹通過內(nèi)存屏障來實(shí)現(xiàn)有序性。

多線程同步與鎖

前面講到了線程競態(tài)和線程安全,都是圍繞多線程訪問共享變量來討論的。正因?yàn)橛羞@種情況出現(xiàn),在進(jìn)行多線程開發(fā)的時(shí)候需要解決這個(gè)問題。

為了保障線程安全,會(huì)將多線程的并發(fā)訪問轉(zhuǎn)化成串行的訪問。鎖(Lock)就是利用這種思路來保證多線程同步的。

鎖就好像是共享數(shù)據(jù)訪問的許可證,任何線程需要訪問共享數(shù)據(jù)之前都需要獲得這個(gè)鎖。

當(dāng)一個(gè)線程獲取鎖的時(shí)候,其他申請(qǐng)鎖的線程需要等待。獲取鎖的線程會(huì)根據(jù)線程上的任務(wù)執(zhí)行代碼,執(zhí)行代碼以后才會(huì)釋放掉鎖。

在獲得鎖與釋放鎖之間執(zhí)行的代碼區(qū)域被稱為臨界區(qū),在臨界區(qū)中訪問的數(shù)據(jù),被稱為共享數(shù)據(jù)。

在該線程釋放鎖以后,其他的線程才能獲得該鎖,再對(duì)共享數(shù)據(jù)進(jìn)行操作。

 

多線程訪問臨界區(qū),訪問共享數(shù)據(jù)

上面描述的操作也被稱為互斥操作,鎖通過這種互斥操作來保障競態(tài)的原子性。

記得前面談到的原子性嗎?一個(gè)或者多個(gè)對(duì)共享數(shù)據(jù)的操作,要么完成,要么不完成,不能出現(xiàn)中間狀態(tài)。

假設(shè)臨界區(qū)中的代碼,并不是原子性的。例如前文提到的“x=x+1”,其中就包括了三個(gè)操作,讀取 x 的值,進(jìn)行加 1 操作,寫入新的值。

如果在多線程訪問的時(shí)候,隨著運(yùn)行時(shí)間的不同會(huì)得到不同的結(jié)果。如果對(duì)這個(gè)操作加上鎖,就可以使之具有“原子性”,也就是在一個(gè)線程訪問的時(shí)候其他線程無法訪問。

說完了多線程開發(fā)中鎖的重要性,再來看看 Java 有那幾種鎖。

內(nèi)部鎖

內(nèi)部鎖也稱作監(jiān)視器(Monitor),它是通過 Synchronized 關(guān)鍵字來修飾方法及代碼塊,來制造臨界區(qū)的。

Synchronized 關(guān)鍵字可以用來修飾同步方法,同步靜態(tài)方法,同步實(shí)例方法,同步代碼塊。

 

Synchronized 引導(dǎo)的代碼塊就是上面提到的臨界區(qū)。鎖句柄就是一個(gè)對(duì)象的引用,例如:它可以寫成 this 關(guān)鍵字,就表示當(dāng)前對(duì)象。

鎖句柄對(duì)應(yīng)的監(jiān)視器被稱為相應(yīng)同步塊的引導(dǎo)鎖,相應(yīng)的我們稱呼相應(yīng)的同步塊為該鎖引導(dǎo)的同步塊。

 

內(nèi)部鎖示意圖

鎖句柄通常采用 final 修飾(private final)。因?yàn)殒i句柄一旦改變,會(huì)導(dǎo)致同一個(gè)代碼塊的多個(gè)線程使用不同的鎖,而導(dǎo)致競態(tài)。

同步靜態(tài)方法相當(dāng)于當(dāng)前類為引導(dǎo)鎖的同步塊。線程在執(zhí)行臨界區(qū)代碼的時(shí)候,必須持有該臨界區(qū)的引導(dǎo)鎖。

一旦執(zhí)行完臨界區(qū)代碼,引導(dǎo)該臨界區(qū)的鎖就會(huì)被釋放。內(nèi)部鎖申請(qǐng)和釋放的過程由 Java 虛擬機(jī)負(fù)責(zé)完成。

所以 Synchronized 實(shí)現(xiàn)的鎖被稱為內(nèi)部鎖。因此不會(huì)導(dǎo)致鎖泄露,Java 編譯器在將代碼塊編譯成字節(jié)碼的時(shí)候,對(duì)臨界區(qū)拋出的異常進(jìn)行了處理。

Java 虛擬機(jī)會(huì)給每個(gè)內(nèi)部鎖分配一個(gè)入口集(Entry Set),用于記錄等待獲取鎖的線程。申請(qǐng)鎖失敗的線程會(huì)在入口集中等待再次申請(qǐng)鎖的機(jī)會(huì)。

當(dāng)?shù)却逆i被其他線程釋放時(shí),入口集中的等待線程會(huì)被喚醒,獲得申請(qǐng)鎖的機(jī)會(huì)。

內(nèi)部鎖的機(jī)制會(huì)在等待的線程中進(jìn)行選擇,選擇的規(guī)則會(huì)根據(jù)線程活躍度和優(yōu)先級(jí)來進(jìn)行,被選中的線程會(huì)持有鎖進(jìn)行后續(xù)操作。

顯示鎖

顯示鎖是 JDK 1.5 開始引入的排他鎖,作為一種線程同步機(jī)制存在,其作用與內(nèi)部鎖相同,但它提供了一些內(nèi)部鎖不具備的特性。顯示鎖是 java.util.concurrent.locks.Lock 接口的實(shí)例。

顯示鎖實(shí)現(xiàn)的幾個(gè)步驟分別是:

  • 創(chuàng)建 Lock 接口實(shí)例
  • 申請(qǐng)顯示鎖 Lock
  • 對(duì)共享數(shù)據(jù)進(jìn)行訪問
  • 在 finally 中釋放鎖,避免鎖泄漏

 

顯示鎖使用實(shí)例圖

顯示鎖支持非公平鎖也支持公平鎖。公平鎖中, 線程嚴(yán)格先進(jìn)先出(FIFO)的順序,獲取鎖資源。

如果有“當(dāng)前線程”需要獲取共享變量,需要進(jìn)行排隊(duì)。當(dāng)鎖被釋放,由隊(duì)列中排第一個(gè)的線程(Node1)獲取,依次類推。

 

公平鎖示意圖

非公平鎖中,在線程釋放鎖的時(shí)候, “當(dāng)前線程“和等待隊(duì)列中的第一個(gè)線程(Node1)競爭鎖資源。通過線程活躍度和優(yōu)先級(jí)來確定那個(gè)線程持有鎖資源。

 

非公平鎖示意圖

公平鎖保證鎖調(diào)度的公平性,但是增加了線程暫停和喚醒的可能性,即增加了上下文切換的代價(jià)。非公平鎖加入了競爭機(jī)制,會(huì)有更好的性能,能夠承載更大的吞吐量。

當(dāng)然,非公平鎖讓獲取鎖的時(shí)間變得更加不確定,可能會(huì)導(dǎo)致在阻塞隊(duì)列中的線程長期處于饑餓狀態(tài)。

線程同步機(jī)制:內(nèi)存屏障

說了多線程訪問共享變量,存在的競態(tài)問題,然后引入鎖的機(jī)制來解決這個(gè)問題。

上文提到內(nèi)部鎖和顯示鎖來解決線程同步的問題,而且提到了解決了競態(tài)中“原子性”的問題。

那么接下來,通過介紹內(nèi)存屏障機(jī)制,來理解如何實(shí)現(xiàn)“可見性”和“有序性”的。

這里就引出了內(nèi)存屏障的概念,內(nèi)存屏障是被插入兩個(gè) CPU 指令之間執(zhí)行的,它是用來禁止編譯器,處理器重排序從而保證有序性和可見性的。

對(duì)于可見性來說,我們提到了線程獲得和釋放鎖時(shí)分別執(zhí)行的兩個(gè)動(dòng)作:“刷新處理器緩存”和“沖刷處理器緩存”。

前一個(gè)動(dòng)作保證了,持有鎖的線程讀取共享變量,后一個(gè)動(dòng)作保證了,持有鎖的線程對(duì)共享變量更新之后,對(duì)于后續(xù)線程可見。

另外,為了達(dá)到屏障的效果,它也會(huì)使處理器寫入、讀取值之前,將主內(nèi)存的值寫入高速緩存,清空無效隊(duì)列,從而保障可見性。

對(duì)于有序性來說。下面來舉個(gè)例子說明,假設(shè)有一組 CPU 指令:

  • Store 表示“存儲(chǔ)指令”
  • Load 表示“讀取指令”
  • StoreLoad 代表“寫讀內(nèi)存屏障”

 

StoreLoad 內(nèi)存屏障示意圖

StoreLoad 屏障之前的 Store 指令,無法與 StoreLoad 屏障之后的 Load 指令進(jìn)行交換位置,即重排序。

但是 StoreLoad 屏障之前和之后的指令是可以互換位置的,即 Store1 可以和 Store2 互換,Load2 可以和 Load3 互換。

常見有 4 種屏障:

  • LoadLoad 屏障:指令順序如:Load1→LoadLoad→Load2。 需要保證 Load1 指令先完成,才能執(zhí)行 Load2 及后續(xù)指令。
  • StoreStore 屏障:指令順序如:Store1→StoreStore→Store2。需要保證 Store1 指令對(duì)其他處理器可見,才能執(zhí)行 Store2 及后續(xù)指令。
  • LoadStore 屏障:指令順序如:Load1→LoadStore→Store2。需要保證 Load1 指令執(zhí)行完成,才能執(zhí)行 Store2 及后續(xù)指令。
  • StoreLoad 屏障:指令順序如:Store1→StoreLoad→Load2。需要保證 Store1 指令對(duì)所有處理器可見,才能執(zhí)行 Load2 及后續(xù)指令。

這種內(nèi)存屏障的開銷是四種中最大的(沖刷寫緩沖器,刷新處理器緩存)。它也是個(gè)萬能屏障,兼具其他三種內(nèi)存屏障的功能。

一般在 Java 常用 Volatile 和 Synchronized 關(guān)鍵字實(shí)現(xiàn)內(nèi)存屏障,也可以通過 Unsafe 來實(shí)現(xiàn)。

總結(jié)

從線程的定義和多線程訪問共享變量開始,出現(xiàn)了線程對(duì)資源的競態(tài)現(xiàn)象。競態(tài)會(huì)使多線程訪問資源的時(shí)候,隨著時(shí)間的推移,資源結(jié)果不可控制。

這個(gè)給我們的多線程編程提出了挑戰(zhàn)。于是,我們需要通過原子性,可見性,有序性來解決線程安全的問題。

對(duì)于共享資源的同步可以解決這些問題,Java 提供內(nèi)部鎖和顯示鎖作為解決方案的最佳實(shí)踐。

在最后,又介紹了線程同步的底層機(jī)制:內(nèi)存屏障。它通過組織 CPU 指令的重排序解決了可見性和有序性的問題。

作者:崔皓

簡介:十六年開發(fā)和架構(gòu)經(jīng)驗(yàn),曾擔(dān)任過惠普武漢交付中心技術(shù)專家,需求分析師,項(xiàng)目經(jīng)理,后在創(chuàng)業(yè)公司擔(dān)任技術(shù)/產(chǎn)品經(jīng)理。善于學(xué)習(xí),樂于分享。目前專注于技術(shù)架構(gòu)與研發(fā)管理。

【51CTO原創(chuàng)稿件,合作站點(diǎn)轉(zhuǎn)載請(qǐng)注明原文作者和出處為51CTO.com】

 

責(zé)任編輯:武曉燕 來源: 51CTO技術(shù)棧
相關(guān)推薦

2013-05-29 10:47:50

Android開發(fā)Java多線程java面試題

2021-02-05 12:34:33

線程池系統(tǒng)

2021-06-07 08:19:27

Java多線程進(jìn)程

2020-02-18 14:25:51

Java線程池拒絕策略

2009-12-08 10:07:29

2020-05-14 17:41:40

Redis 6.0多線程數(shù)據(jù)庫

2021-11-09 09:30:52

OkHttp面試Android

2022-01-05 09:55:26

asynawait前端

2022-10-31 17:29:11

Java多線程

2009-03-12 10:52:43

Java線程多線程

2021-04-26 17:23:21

JavaCAS原理

2024-04-10 09:47:59

Java調(diào)度虛擬線程

2018-04-23 09:50:54

2021-08-04 07:57:17

C++多線程算法

2021-12-26 18:22:30

Java線程多線程

2009-06-29 17:49:47

Java多線程

2024-04-09 08:32:58

Java面試題線程

2019-10-30 21:27:51

Java中央處理器電腦

2021-01-26 05:07:53

WindowViewWMS

2013-03-27 10:32:53

iOS多線程原理runloop介紹GCD
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)