面試必看!花了三天整理出來(lái)的并發(fā)編程的鎖及內(nèi)存模型或悲,看完你就明白了!

前言

最近看到有不少粉絲私信我說(shuō)孙咪,能不能給整理出一份面試的要點(diǎn)出來(lái),說(shuō)自己復(fù)習(xí)的時(shí)候思緒很亂巡语,老是找不到重點(diǎn)翎蹈。那么今天就先給大家分享一個(gè)面試幾乎必問(wèn)的點(diǎn),并發(fā)男公!在面試中問(wèn)的頻率很高的一個(gè)是分布式荤堪,一個(gè)就是并發(fā),具體干貨都在下方了枢赔。

面試環(huán)節(jié)

1. 面試官:你先說(shuō)下你對(duì)synchronized的了解澄阳。

  • 我:synchronized可以保證方法或者代碼在運(yùn)行時(shí),同一時(shí)刻只有一個(gè)方法可以進(jìn)入到臨界區(qū)踏拜,同時(shí)還可以保證共享變量的內(nèi)存可見(jiàn)性碎赢。

  • 我:Java中每個(gè)對(duì)象都可以作為鎖,這是synchronized實(shí)現(xiàn)同步的基礎(chǔ): 1速梗、普通同步方法:鎖是當(dāng)前實(shí)例對(duì)象肮塞。
    2襟齿、靜態(tài)同步方法,鎖是當(dāng)前類的class對(duì)象峦嗤。
    3蕊唐、同步代碼塊:鎖是括號(hào)里的對(duì)象屋摔。

2. 面試官:當(dāng)線程訪問(wèn)同步代碼塊時(shí)烁设,它首先要得到鎖才能執(zhí)行代碼,退出或者拋異常要釋放鎖钓试,這是怎么實(shí)現(xiàn)的呢装黑?

  • 我:同步代碼塊是使用monitorenter和monitorexit指令實(shí)現(xiàn)的,同步方法依靠的是方法修飾符上的ACCSYNCHRONIZED實(shí)現(xiàn)的弓熏。
    1恋谭、同步代碼塊:monitorenter指令插入到同步代碼快的開(kāi)始位置,monitorexit指令插入到同步代碼塊的結(jié)束位置挽鞠,jVM保證每一個(gè)monitorexist都有 一個(gè)monitorenter與之相對(duì)應(yīng)疚颊。任何對(duì)應(yīng)都有一個(gè)monitor與之相關(guān)聯(lián),當(dāng)且一個(gè)monitor被持有之后信认,他將處于鎖定狀態(tài)材义。線程執(zhí)行到monitorenter指令 時(shí),將會(huì)嘗試獲取對(duì)象對(duì)應(yīng)的monitor所有權(quán)嫁赏,即嘗試獲取對(duì)象的鎖其掂。
    2、同步方法:synchronized方法是在Class文件的方法表中將該方法的accessflags字段中的synchronized標(biāo)志位置為1潦蝇,表示該方法是同步方法 并使用調(diào)用該方法的對(duì)象或該方法所屬的Class在JVM的內(nèi)部對(duì)象表示Klass作為鎖對(duì)象款熬。

3. 面試官:你剛提到了每個(gè)對(duì)象都有一個(gè)monitor與之對(duì)應(yīng),那什么是Monitor呢攘乒?

  • 我:我們可以把它理解為一個(gè)同步工具贤牛,也可以描述為一種同步機(jī)制,它通常被描述為一個(gè)對(duì)象则酝。與一切皆對(duì)象一樣殉簸,所有的java對(duì)象是天生的Monitor, 每一個(gè)java對(duì)象都有成為Monitor的潛質(zhì)堤魁,因?yàn)樵贘ava的設(shè)計(jì)中喂链,每一個(gè)java對(duì)象自打娘胎出來(lái)就帶了一把看不見(jiàn)的鎖,它被叫做內(nèi)部鎖或者M(jìn)onitor鎖妥泉。

  • 我:(接著說(shuō))Monitor是線程私有的數(shù)據(jù)結(jié)構(gòu)椭微,每一個(gè)線程都有一個(gè)可用monitor record列表,同時(shí)還有一個(gè)全局的可用列表盲链。每一個(gè)被鎖住的對(duì)象 都會(huì)和一個(gè)monitor關(guān)聯(lián)(對(duì)象頭的MarkWord中的LockWord指向monitor的起始地址)蝇率,同時(shí)monitor中由一個(gè)Owner字段存放擁有該鎖的線程的唯一標(biāo)識(shí)迟杂, 表示該鎖被這個(gè)線程占用。

4. 面試官:很好本慕。我們知道synchronized是悲觀鎖排拷,一直以來(lái)被當(dāng)做重量級(jí)鎖。但是jdk1.6對(duì)鎖進(jìn)行了優(yōu)化锅尘,比如自旋鎖监氢、適應(yīng)性自旋鎖、鎖消除藤违、偏向鎖以及 輕量級(jí)鎖等技術(shù)來(lái)減少鎖操作的開(kāi)銷浪腐,這些你都了解嗎?

  • 我:知道一些顿乒。鎖主要存在四種狀態(tài):無(wú)鎖狀態(tài)议街、偏向鎖狀態(tài)、輕量級(jí)鎖狀態(tài)璧榄、重量級(jí)鎖狀態(tài)特漩。他們會(huì)隨著競(jìng)爭(zhēng)的激烈而逐漸升級(jí)。注意鎖可以升級(jí)不可降級(jí)骨杂, 這種策略是為了提高獲得鎖和釋放鎖的效率涂身。

5. 面試官:那你先來(lái)說(shuō)下自旋鎖

  • 我:線程的阻塞和喚醒需要CPU從用戶態(tài)轉(zhuǎn)為核心態(tài),頻繁的阻塞和喚醒對(duì)CPU來(lái)說(shuō)是一個(gè)負(fù)擔(dān)很重的工作腊脱,同時(shí)影響系統(tǒng)的并發(fā)能力访得,同時(shí)我們發(fā)現(xiàn)很多應(yīng)用上 對(duì)象鎖的鎖狀態(tài)只會(huì)持續(xù)很短的一段時(shí)間,為了這一段很短的時(shí)間頻繁的阻塞和喚醒線程是不值得的陕凹,所以引入自旋鎖悍抑。何謂自旋鎖呢-就是讓線程等待一段時(shí)間,不會(huì)被 立即掛起杜耙,看持有鎖的線程是否會(huì)很快釋放鎖搜骡。那么問(wèn)題來(lái)了,等多長(zhǎng)時(shí)間呢佑女?時(shí)間短了等不到持有鎖的線程釋放鎖记靡,時(shí)間長(zhǎng)了占用了處理器的時(shí)間,典型的“占著茅坑不拉屎”团驱, 反而帶來(lái)性能上的浪費(fèi)摸吠。所以,自旋等待的時(shí)間(自旋)的次數(shù)必須有一個(gè)限度嚎花,如果自旋超過(guò)了定義的時(shí)間仍沒(méi)有獲得鎖則要被掛起寸痢。

6. 面試官:我記得有個(gè)適應(yīng)性自旋鎖,更加智能紊选。你能說(shuō)下么啼止?

  • 我:所謂自適應(yīng)就意味著自旋的次數(shù)不再是固定的道逗,它是由上一次在同一個(gè)鎖上的自旋時(shí)間以及鎖的擁有者的狀態(tài)來(lái)決定。線程如果自旋成功了献烦,那么下次自旋的次數(shù) 會(huì)更加多滓窍,因?yàn)樘摂M機(jī)認(rèn)為既然上次成功了,那么此次自旋也可能成功巩那。反之吏夯,如果對(duì)于某個(gè)鎖,很少有自旋能成功的拢操,那么以后等待這個(gè)鎖的時(shí)候自選的次數(shù)會(huì)減少甚至不自旋锦亦。 有了自適應(yīng)自旋鎖,虛擬機(jī)對(duì)程序鎖的狀況預(yù)測(cè)越來(lái)越準(zhǔn)確令境,虛擬機(jī)會(huì)越來(lái)越聰明。

7. 面試官:給你看下面一段代碼顾瞪,你說(shuō)下會(huì)存在加鎖的操作嗎舔庶?

public static void main(String [] args) {
        Vector<String> vector = new Vector<>();
        for (int i=0; i<10; i++) {
            vector.add(i+"");
        }
        System.out.println(vector);
    }
  • 我:不會(huì)。這種情況下陈醒,JVM檢測(cè)到不可能存在共享數(shù)據(jù)競(jìng)爭(zhēng)惕橙,這時(shí)JVM會(huì)對(duì)這些同步鎖進(jìn)行鎖消除。鎖消除的基礎(chǔ)是逃逸分析的數(shù)據(jù)支持钉跷。

8. 面試官:再看一段代碼弥鹦,分析一下是在什么地方加鎖的?

public static void test() {
        List<String> list = new ArrayList<>();
        for (int i=0; i<10; i++) {
            synchronized (Demo.class) {
                list.add(i + "");
            }
        }
        System.out.println(list);
    }
  • 我:雖然synchronized是在循環(huán)里面爷辙,但實(shí)際上加鎖的范圍會(huì)擴(kuò)大到循環(huán)外彬坏,這是鎖粗化。鎖粗化就是將多個(gè)連續(xù)的加鎖膝晾、解鎖操作連接在一起栓始,擴(kuò)展 成一個(gè)范圍更大的鎖。

8. 面試官:你能說(shuō)下輕量級(jí)鎖嗎血当?

  • 我:輕量級(jí)鎖提升程序同步性能的依據(jù)是:對(duì)于絕大部分的鎖缚柏,在整個(gè)同步周期內(nèi)是不存在競(jìng)爭(zhēng)的(區(qū)別于偏向鎖)症汹,這是一個(gè)經(jīng)驗(yàn)數(shù)據(jù)。如果沒(méi)有競(jìng)爭(zhēng),輕量級(jí)鎖使用CAS操作避免了使用互斥 量的開(kāi)銷枉长,但如果存在競(jìng)爭(zhēng),除了互斥量的開(kāi)銷荚板,還額外發(fā)生了CAS操作址遇,因此在有競(jìng)爭(zhēng)的情況下,輕量級(jí)鎖比傳統(tǒng)的重量級(jí)鎖更慢撤奸。

  • 我接著說(shuō):輕量級(jí)鎖的加鎖過(guò)程是: 1吠昭、在代碼進(jìn)入同步塊的時(shí)候喊括,如果同步對(duì)象鎖狀態(tài)為無(wú)鎖狀態(tài)(鎖標(biāo)志位為“01”,是否為偏向鎖為“0”)矢棚,虛擬機(jī)首先將在當(dāng)前線程的棧幀中建立一個(gè)名為鎖記錄(Lock Record)的空間郑什,用于存儲(chǔ)對(duì)象目前的Mark Word的拷貝,官方稱之為Displaced Mark Word蒲肋,這時(shí)候線程堆棧與對(duì)象頭的狀態(tài)如圖:
    輕量級(jí)鎖的加鎖過(guò)程

2蘑拯、拷貝對(duì)象頭中的Mark Word復(fù)制到鎖記錄(Lock Record)中。
3兜粘、拷貝成功后申窘,虛擬機(jī)將使用CAS操作嘗試將鎖對(duì)象的Mark Word更新為指向Lock Record的指針,并將線程棧幀中的Lock Record里的owner指針指向Object的Mark Word孔轴。如果這個(gè)更新 動(dòng)作成功了剃法,那么這個(gè)線程就擁有了該對(duì)象的鎖,并且對(duì)象Mark Word的鎖標(biāo)志位設(shè)置為“00”路鹰,表示此對(duì)象處于輕量級(jí)鎖定狀態(tài)贷洲。

4、如果這個(gè)更新操作失敗了晋柱,虛擬機(jī)首先會(huì)檢查對(duì)象的Mark Word是否指向當(dāng)前線程的棧幀优构,如果是就說(shuō)明當(dāng)前線程已經(jīng)擁有了這個(gè)對(duì)象的鎖,那就可以直接進(jìn)入同步塊繼續(xù)執(zhí)行雁竞。否則說(shuō)明 多個(gè)線程競(jìng)爭(zhēng)鎖钦椭,輕量級(jí)鎖就要膨脹為重量級(jí)鎖,鎖標(biāo)志位的狀態(tài)值變?yōu)椤?0”碑诉,Mark Word中存儲(chǔ)的就是指向重量級(jí)鎖(互斥量)的指針彪腔,后面等待鎖的線程也要進(jìn)入阻塞狀態(tài)。

9. 面試官:很詳細(xì)联贩。那你能再解釋下偏向鎖嗎漫仆?

  • 我:偏向鎖的目的是消除數(shù)據(jù)在無(wú)競(jìng)爭(zhēng)情況下的同步原語(yǔ),進(jìn)一步提高程序的運(yùn)行性能泪幌。偏向鎖會(huì)偏向于第一個(gè)獲得它的線程盲厌,如果在接下來(lái)的執(zhí)行過(guò)程中,該鎖沒(méi)有被其他線程獲取祸泪,那持有 偏向鎖的線程將永遠(yuǎn)不需要同步吗浩。

  • 我頓了下,接著說(shuō):當(dāng)鎖第一次被線程獲取的時(shí)候没隘,線程使用CAS操作把這個(gè)線程的ID記錄在對(duì)象Mark Word中懂扼,同時(shí)置偏向標(biāo)志位1.以后該線程在進(jìn)入和退出代碼塊時(shí)不需要進(jìn)行CAS操作 來(lái)加鎖和解鎖,只需要簡(jiǎn)單測(cè)試一下對(duì)象頭的Mark Word里是否存儲(chǔ)著指向當(dāng)前線程的ID。如果測(cè)試成功阀湿,表示線程已經(jīng)獲得了鎖赶熟。當(dāng)有另外一個(gè)線程去嘗試獲取這個(gè)鎖時(shí),偏向模式就宣告結(jié)束陷嘴。 根據(jù)鎖對(duì)象目前是否處于被鎖定的狀態(tài)映砖,撤銷偏向后恢復(fù)到未鎖定或輕量級(jí)鎖定狀態(tài)。

10. 面試官:那偏向鎖灾挨、輕量級(jí)鎖和重量級(jí)鎖有什么區(qū)別呢邑退?

  • 我:偏向鎖、輕量級(jí)鎖都是樂(lè)觀鎖劳澄,重量級(jí)鎖是悲觀鎖地技。一個(gè)對(duì)象剛開(kāi)始實(shí)例化的時(shí)候,沒(méi)有任何線程來(lái)訪問(wèn)它時(shí)秒拔,它是可偏向的莫矗,意味著它認(rèn)為只可能有一個(gè)線程來(lái)訪問(wèn)它,所以當(dāng)?shù)谝粋€(gè)線程 訪問(wèn)它的時(shí)候溯警,它會(huì)偏向這個(gè)線程趣苏,此時(shí),對(duì)象持有偏向鎖梯轻。偏向第一個(gè)線程,這個(gè)線程在修改對(duì)象頭成為偏向鎖的時(shí)候使用CAS操作尽棕,并將對(duì)象頭中的ThreadID改成自己的Id喳挑,之后再訪問(wèn)這個(gè)對(duì)象只需要對(duì)比ID。一旦有第二個(gè)線程訪問(wèn)這個(gè)對(duì)象滔悉,因?yàn)槠蜴i不會(huì)釋放伊诵,所以第二個(gè)線程看到對(duì)象是偏向狀態(tài),表明在這個(gè)對(duì)象上存在競(jìng)爭(zhēng)了回官,檢查原來(lái)持有該對(duì)象的線程是否依然存活曹宴,如果掛了,則可以將對(duì)象變?yōu)闊o(wú)鎖狀態(tài)歉提,然后重新偏向新的線程笛坦。如果原來(lái)的線程依然存活,則馬上執(zhí)行那個(gè)線程的操作棧苔巨,檢查該對(duì)象的使用情況版扩,如果仍然需要持有偏向鎖,則偏向鎖升級(jí)為輕量級(jí)鎖(偏向鎖就是此時(shí)升級(jí)為輕量級(jí)鎖)侄泽。如果不存在使用了礁芦,則可以將對(duì)象恢復(fù)成無(wú)鎖狀態(tài),然后重新偏向。

  • 我:(接著說(shuō))輕量級(jí)鎖認(rèn)為競(jìng)爭(zhēng)存在柿扣,但是競(jìng)爭(zhēng)的程度很輕肖方,一般兩個(gè)線程對(duì)于同一個(gè)鎖的操作都會(huì)錯(cuò)開(kāi),或者說(shuō)自旋一下未状,另一個(gè)線程就會(huì)釋放鎖俯画。但是當(dāng)自旋超過(guò)一定次數(shù),或者一個(gè)線程持有鎖娩践, 一個(gè)線程在自旋活翩,又有第三個(gè)來(lái)訪,輕量級(jí)鎖膨脹為重量級(jí)鎖翻伺,重量級(jí)鎖使除了擁有鎖的線程以外的線程都阻塞材泄,防止CPU空轉(zhuǎn)。簡(jiǎn)單的說(shuō)就是:有競(jìng)爭(zhēng)吨岭,偏向鎖升級(jí)為輕量級(jí)鎖拉宗,競(jìng)爭(zhēng)逐漸激烈, 輕量級(jí)鎖升級(jí)為重量級(jí)鎖辣辫。

11.面試官:你了解java的內(nèi)存模型嗎旦事?能說(shuō)下對(duì)JMM的理解嗎?

  • 我:在JSR113標(biāo)準(zhǔn)中有有一段對(duì)JMM的簡(jiǎn)單介紹:Java虛擬機(jī)支持多線程執(zhí)行急灭。在Java中Thread類代表線程姐浮,創(chuàng)建一個(gè)線程的唯一方法就是創(chuàng)建一個(gè)Thread類的實(shí)例對(duì)象,當(dāng)調(diào)用了對(duì)象的start方法后葬馋,相應(yīng)的線程將會(huì)執(zhí)行卖鲤。線程的行為有時(shí)會(huì)與我們的直覺(jué)相左,特別是在線程沒(méi)有正確同步的情況下畴嘶。本規(guī)范描述了JMM平臺(tái)上多線程程序的語(yǔ)義蛋逾,具體包含一個(gè)線程對(duì)共享變量的寫(xiě)入何時(shí)能被其他線程看到。這是官方的接單介紹窗悯。

  • 我:Java內(nèi)存模型是內(nèi)存模型在JVM中的體現(xiàn)区匣。這個(gè)模型的主要目標(biāo)是定義程序中各個(gè)共享變量的訪問(wèn)規(guī)則,也就是在虛擬機(jī)中將變量存儲(chǔ)到內(nèi)存以及從內(nèi)存中取出變量這類的底層細(xì)節(jié)蒋院。通過(guò)這些規(guī)則來(lái)規(guī)范對(duì)內(nèi)存的讀寫(xiě)操作亏钩,保證了并發(fā)場(chǎng)景下的可見(jiàn)性、原子性和有序性悦污。 JMM規(guī)定了多有的變量都存儲(chǔ)在主內(nèi)存中铸屉,每條線程都有自己的工作內(nèi)存,線程的工作內(nèi)存保存了該線程中用到的主內(nèi)存副本拷貝切端,線程對(duì)變量的所有操作都必須在工作內(nèi)存中進(jìn)行彻坛,而不是直接讀寫(xiě)主內(nèi)存。不同線程之間也無(wú)法直接訪問(wèn)對(duì)方工作內(nèi)存中的變量,線程間變量的傳遞均需要自己的工作內(nèi)存和主存之間 進(jìn)行數(shù)據(jù)同步昌屉。而JMM就作用于工作內(nèi)存和主存之間數(shù)據(jù)同步過(guò)程钙蒙。他規(guī)定了如何做數(shù)據(jù)同步以及什么時(shí)候做數(shù)據(jù)同步。也就是說(shuō)Java線程之間的通信由Java內(nèi)存模型控制,JMM決定一個(gè)線程對(duì)共享變量的寫(xiě)入何時(shí)對(duì)另一個(gè)線程可見(jiàn)间驮。

  • 我:簡(jiǎn)單的說(shuō):Java的多線程之間是通過(guò)共享內(nèi)存進(jìn)行通信的躬厌,而由于采用共享內(nèi)存進(jìn)行通信,在通信過(guò)程中會(huì)存在一系列如原子性竞帽、可見(jiàn)性和有序性的問(wèn)題扛施。JMM就是為了解決這些問(wèn)題出現(xiàn)的, 這個(gè)模型建立了一些規(guī)范屹篓,可以保證在多核CPU多線程編程的環(huán)境下疙渣,對(duì)共享變量的讀寫(xiě)的原子性、可見(jiàn)性和有序性堆巧。

12. 面試官:那你說(shuō)下Java內(nèi)存模型的happens-before規(guī)則妄荔?

  • 我:在JMM中,如果一個(gè)操作執(zhí)行的結(jié)果需要對(duì)另一個(gè)操作可見(jiàn)谍肤,那么這兩個(gè)操作之間必須存在happens-before關(guān)系啦租。happens-before原則是JMM中非常重要的原則,它是判斷數(shù)據(jù)是否存在競(jìng)爭(zhēng)荒揣、線程是否安全的主要依據(jù)篷角,保證了多線程環(huán)境下的可見(jiàn)性。下面我說(shuō)下happens-before的內(nèi)容: happens-before的原則定義如下:
    1系任、如果一個(gè)操作happens-before另一個(gè)操作内地,那么第一個(gè)操作的執(zhí)行結(jié)果將對(duì)第二個(gè)操作可見(jiàn),而且第一個(gè)操作的執(zhí)行順序排在第二個(gè)操作之前赋除。
    2、兩個(gè)操作之間存在happens-before關(guān)系非凌,并不一定意味著一定要按照happens-before原則制定的順序來(lái)執(zhí)行举农。如果重排序之后的執(zhí)行結(jié)果與按照happens-before關(guān)系來(lái)執(zhí)行的結(jié)果一致,那么這種重排序并不非法敞嗡。
    下面是happens-before的原則規(guī)則:
    1颁糟、程序次序規(guī)則:一個(gè)線程內(nèi),按照代碼書(shū)寫(xiě)順序喉悴,書(shū)寫(xiě)在前面的操作先行發(fā)生于書(shū)寫(xiě)在后面的操作棱貌。
    2、鎖定規(guī)則:一個(gè)unLock操作先行發(fā)生于后面對(duì)同一個(gè)鎖的lock操作箕肃。
    3婚脱、volatile變量規(guī)則:對(duì)一個(gè)變量的寫(xiě)操作先行發(fā)生于后面對(duì)這個(gè)變量的讀操作。
    4、傳遞規(guī)則:如果操作A先行發(fā)生于操作B障贸,而操作B又先行發(fā)生于操作C错森,則可以得出操作A先行發(fā)生于操作C。
    5篮洁、線程啟動(dòng)規(guī)則:Thread對(duì)象的start()方法先行發(fā)生于此線程的每個(gè)動(dòng)作涩维。
    6、線程中斷規(guī)則:對(duì)線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測(cè)到中斷事件的發(fā)生袁波。
    7瓦阐、線程終結(jié)規(guī)則:線程中所有的操作都先行發(fā)生于線程的終止檢測(cè)。
    8篷牌、對(duì)象終結(jié)規(guī)則:一個(gè)對(duì)象的初始化完成先行發(fā)生于它的finalize()方法的開(kāi)始睡蟋。

13 面試官:你剛才提到了JVM會(huì)對(duì)我們的程序進(jìn)行重排序,那是隨便重排序嗎娃磺?

  • 我:不是的薄湿,它需要滿足以下兩個(gè)條件:
    1、在單線程環(huán)境下不能改變程序運(yùn)行的結(jié)果偷卧。
    2豺瘤、存在數(shù)據(jù)依賴關(guān)系的不允許重排序。
    其實(shí)這兩點(diǎn)可以歸結(jié)為一點(diǎn):無(wú)法通過(guò)happens-before原則推導(dǎo)出來(lái)的听诸,JMM允許任意的排序坐求。

  • 我:這里有必要提到as-if-serial語(yǔ)義:所有的操作都可以為了優(yōu)化而被重排序,但是你必須保證重排序后執(zhí)行的結(jié)果不能被改變晌梨,編譯器桥嗤、runtime和處理器都必須遵守 as-if-serial語(yǔ)義。注意as-if-serial只保證單線程環(huán)境仔蝌,多線程環(huán)境下無(wú)效泛领。舉個(gè)栗子:

int a=1; //A
int b=2; //B
int c=a+b; //C

A,B,C三個(gè)操作存在如下關(guān)系:A和B不存在數(shù)據(jù)依賴,A和C敛惊,B和C存在數(shù)據(jù)依賴渊鞋,因此在重排序的時(shí)候:A和B可以隨意排序,但是必須位于C的前面瞧挤,但無(wú)論何種順序锡宋,最終結(jié)果C都是3.

  • 我接著說(shuō):下面舉個(gè)重排序?qū)Χ嗑€程影響的栗子:
public class RecordExample2 {
    int a = 0;
    boolean flag = false;

    /**
     * A線程執(zhí)行
     */
    public void writer(){
        a = 1;                  // 1
        flag = true;            // 2
    }

    /**
     * B線程執(zhí)行
     */
    public void read(){
        if(flag){                  // 3
           int i = a + a;          // 4
        }
    }}

假如操作1和操作2之間重排序,可能會(huì)變成下面這種執(zhí)行順序:
1特恬、線程A執(zhí)行flag=true执俩;
2、線程B執(zhí)行if(flag)癌刽;
3役首、線程B執(zhí)行int i = a+a尝丐;
4、線程A執(zhí)行a=1宋税。
按照這種執(zhí)行順序線程B肯定讀不到線程A設(shè)置的a值摊崭,在這里多線程的語(yǔ)義就已經(jīng)被重排序破壞了。操作3和操作4之間也可以重排序杰赛,這里就不闡述了呢簸。但是他們之間存在一個(gè)控制依賴的關(guān)系,因?yàn)橹挥胁僮?成立操作4才會(huì)執(zhí)行乏屯。當(dāng)代碼中存在控制依賴性時(shí)根时,會(huì)影響指令序列的執(zhí)行的并行度,所以編譯器和處理器會(huì)采用猜測(cè)執(zhí)行來(lái)克服控制依賴對(duì)并行度的影響辰晕。假如操作3和操作4重排序了蛤迎,操作4先執(zhí)行,則先會(huì)把計(jì)算結(jié)果臨時(shí)保存到重排序緩沖中含友,當(dāng)操作3為真時(shí)才會(huì)將計(jì)算結(jié)果寫(xiě)入變量i中替裆。

14. 面試官:你能給我講下對(duì)volatile的理解嗎?

  • 我:講volatile之前窘问,先補(bǔ)充說(shuō)明下Java內(nèi)存模型中的三個(gè)概念:原子性辆童、可見(jiàn)性和有序性

1、可見(jiàn)性:可見(jiàn)性是指線程之間的可見(jiàn)性惠赫,一個(gè)線程修改的狀態(tài)對(duì)另一個(gè)線程是可見(jiàn)的把鉴。也就是一個(gè)線程的修改的結(jié)果,另一個(gè)線程能夠馬上看到儿咱。比如:用volatile修飾的變量庭砍,就會(huì)具有可見(jiàn)性,volatile修飾的變量不允許線程內(nèi)部緩存和重排序混埠,即直接修改內(nèi)存怠缸,所以對(duì)其他線程是可見(jiàn)的。但這里要注意一個(gè)問(wèn)題钳宪,volatile只能讓被他修飾的內(nèi)容具有可見(jiàn)性凯旭,不能保證它具有原子性。比如 volatile int a=0; ++a使套;這個(gè)變量a具有可見(jiàn)性,但是a++是一個(gè)非原子操作鞠柄,也就是這個(gè)操作同樣存在線程安全問(wèn)題侦高。在Java中,volatile/synchronized/final實(shí)現(xiàn)了可見(jiàn)性厌杜。

2奉呛、原子性:即一個(gè)操作或者多個(gè)操作要么全部執(zhí)行并且執(zhí)行的過(guò)程不會(huì)被任何因素打斷计螺,要么都不執(zhí)行。原子就像數(shù)據(jù)庫(kù)里的事務(wù)一樣瞧壮,他們是一個(gè)團(tuán)隊(duì)登馒,同生共死∨夭郏看下面一個(gè)簡(jiǎn)單的栗子:

i=0;  //1
j=i;  //2
i++; //3
i=j+1; //4

上面的四個(gè)操作陈轿,只有1是原子操作,其他都不是原子操作秦忿。比如2包含了兩個(gè)操作:讀取i,將i值賦給j麦射。在Java中synchronized/lock操作中保證原子性。

3灯谣、有序性:程序執(zhí)行的順序按照代碼的先后順序執(zhí)行潜秋。 前面JMM中提到了重排序,在java內(nèi)存模型中胎许,為了效率是允許編譯器和處理器對(duì)指令進(jìn)行重排序峻呛,而且重排序不會(huì)影響單線程的運(yùn)行結(jié)果,但是對(duì)多線程有影響辜窑。Java中提供了volatile和synchronized保證有序性钩述。

  • 我:volatile的原理是volatile可以保證線程可見(jiàn)性且提供了一定的有序性,但是無(wú)法保證原子性谬擦,在JVM底層volatile是采用“內(nèi)存屏障”來(lái)實(shí)現(xiàn)的切距。總結(jié)起來(lái)就是: 1惨远、保證可見(jiàn)性谜悟,不保證原子性。 2北秽、禁止指令重排序葡幸。
  • 我:下面我來(lái)分析下volatile的這兩條性質(zhì)。 volatile的內(nèi)存語(yǔ)義是: 1贺氓、當(dāng)寫(xiě)一個(gè)volatile變量時(shí)蔚叨,JMM會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存中的共享變量值立即刷新到主內(nèi)存中。
    2辙培、當(dāng)讀一個(gè)volatile變量時(shí)蔑水,JMM會(huì)把線程的本地內(nèi)存置為無(wú)效,直接從主內(nèi)存中讀取共享變量扬蕊。 所以volatile的寫(xiě)內(nèi)存語(yǔ)義是直接刷新到主內(nèi)存中搀别,讀內(nèi)存語(yǔ)義是直接從主內(nèi)存中讀取---所以才能實(shí)現(xiàn)線程可見(jiàn)性。

那么volatile的內(nèi)存語(yǔ)義是如何實(shí)現(xiàn)的呢尾抑?對(duì)于一般的變量會(huì)被重排序歇父,而對(duì)于volatile則不能蒂培,這樣會(huì)影響其內(nèi)存語(yǔ)義,所以為了實(shí)現(xiàn)volatile的內(nèi)存語(yǔ)義JMM會(huì)限制重排序榜苫。
volatile的重排序規(guī)則
1护戳、如果第一個(gè)操作為volatile讀,則不管第二個(gè)操作是啥垂睬,都不能重排序媳荒。這個(gè)操作確保volatile讀之后的操作不會(huì)被編譯器重排序到volatile讀之前。
2羔飞、當(dāng)?shù)诙€(gè)操作為volatile寫(xiě)肺樟,則不管第一個(gè)操作是啥,都不能重排序逻淌。這個(gè)操作確保了volatile寫(xiě)之前的操作不會(huì)被編譯器重排序到volatile寫(xiě)之后么伯。
3、當(dāng)?shù)谝粋€(gè)操作為volatile寫(xiě)卡儒,第二個(gè)操作為volatile讀田柔,不能重排序。

volatile的底層實(shí)現(xiàn)是通過(guò)插入內(nèi)存屏障骨望,但是對(duì)于編譯器來(lái)說(shuō)硬爆,發(fā)現(xiàn)一個(gè)最優(yōu)布置來(lái)最小化插入內(nèi)存屏障的總數(shù)幾乎是不可能的,所以JMM采用了保守策略擎鸠。 如下:
1缀磕、在每一個(gè)volatile寫(xiě)操作前插入一個(gè)StoreStore屏障。
2劣光、在每一個(gè)volatile寫(xiě)操作后插入一個(gè)StoreLoad屏障袜蚕。
3、在每一個(gè)volatile讀操作后插入一個(gè)LoadLoad屏障绢涡。
4牲剃、在每一個(gè)volatile讀操作后插入一個(gè)LoadStore屏障。
總結(jié):StoreStore屏障->寫(xiě)操作->StoreLoad屏障->讀操作->LoadLoad屏障->LoadStore屏障雄可。 下面通過(guò)一個(gè)例子簡(jiǎn)單分析下: volatile原理分析

15. 面試官:很好凿傅,看來(lái)你對(duì)volatile理解的挺深入的了。我們換個(gè)話題数苫,你知道CAS嗎聪舒,能跟我講講嗎?

  • 我:CAS(Compare And Swap)虐急,比較并交換过椎。整個(gè)AQS同步組件,Atomic原子類操作等等都是基于CAS實(shí)現(xiàn)的戏仓,甚至ConcurrentHashMap在JDK1.8版本中疚宇,也調(diào)整為CAS+synchronized。 可以說(shuō)赏殃,CAS是整個(gè)JUC的基石敷待。如下圖:
  • 我:CAS的實(shí)現(xiàn)方式其實(shí)不難。在CAS中有三個(gè)參數(shù):內(nèi)存值V仁热、舊的預(yù)期值A(chǔ)榜揖、要更新的值B,當(dāng)且僅當(dāng)內(nèi)存值V的值等于舊的預(yù)期值A(chǔ)時(shí)抗蠢,才會(huì)將內(nèi)存值V的值修改為B举哟,否則什么也不干,是一種樂(lè)觀鎖迅矛。 其偽代碼如下:
if (this.value == A) {
    this.value = B
    return true;
} else {
    return false;
}
  • 我:接著我舉了個(gè)AtomicInteger的例子妨猩,來(lái)給面試官闡述CAS的實(shí)現(xiàn)。
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;

static {
    try {
        valueOffset = unsafe.objectFieldOffset
            (AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
}

private volatile int value;

如上是AtomicInteger的源碼: 1秽褒、Unsafe是CAS的核心類壶硅,Java無(wú)法直接訪問(wèn)底層操作系統(tǒng),而是通過(guò)本地native方法訪問(wèn)销斟。不過(guò)盡管如此庐椒,JVM還是開(kāi)了個(gè)后門(mén):Unsafe,它提供了 硬件級(jí)別的原子操作蚂踊。
2约谈、valueOffset:為變量值在內(nèi)存中的偏移地址,Unsafe就是通過(guò)偏移地址來(lái)得到數(shù)據(jù)的原值的犁钟。
3棱诱、value:當(dāng)前值,使用volatile修飾特纤,保證多線程環(huán)境下看見(jiàn)的是同一個(gè)军俊。

// AtomicInteger.java
public final int addAndGet(int delta) {
    return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}

// Unsafe.java
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

在方法compareAndSwapInt(var1, var2, var5, var5 + var4)中,有四個(gè)參數(shù)捧存,分別代表:對(duì)象粪躬,對(duì)象的地址,預(yù)期值昔穴,修改值镰官。

  • 我:CAS可以保證一次的讀-改-寫(xiě)操作是原子操作,在單處理器上該操作容易實(shí)現(xiàn)吗货,但是在多處理器上實(shí)現(xiàn)就有點(diǎn)復(fù)雜泳唠。CPU提供了兩種方法來(lái)實(shí)現(xiàn)多處理器的原子操作:總線加鎖或者緩存加鎖。
    1宙搬、總線加鎖:總線加鎖就是使用處理器提供的一個(gè)LOCK#信號(hào)笨腥,當(dāng)一個(gè)處理器在總線上輸出此信號(hào)時(shí)拓哺,其他處理器的請(qǐng)求將被阻塞住,那么該處理器可以獨(dú)占使用共享內(nèi)存脖母。但是這種處理方式顯然 有點(diǎn)霸道士鸥。
    2、緩存加鎖:其實(shí)針對(duì)上面的情況谆级,我們只需要保證在同一時(shí)刻烤礁,對(duì)某個(gè)內(nèi)存地址的操作是原子性的即可。緩存加鎖肥照,就是緩存在內(nèi)存區(qū)域的數(shù)據(jù)如果在加鎖期間脚仔,當(dāng)它執(zhí)行鎖操作寫(xiě)回內(nèi)存時(shí), 處理器不再輸出#LOCK信號(hào)舆绎,而是修改內(nèi)部的內(nèi)存地址鲤脏,利用緩存一致性協(xié)議來(lái)保證原子性。緩存一致性機(jī)制可以保證同一個(gè)內(nèi)存區(qū)域的數(shù)據(jù)僅能被一個(gè)處理器修改亿蒸,也就是說(shuō)當(dāng)CPU1修改緩存行 中的i時(shí)使用緩存鎖定凑兰,那么CPU2就不能同時(shí)緩存了i的緩存行。

16. 面試官:那CAS有什么缺陷嗎边锁?

  • 我:CAS雖然高效的解決了原子問(wèn)題姑食,但是還是存在一些缺陷的,主要體現(xiàn)在三個(gè)方面:
    1茅坛、循環(huán)時(shí)間太長(zhǎng):如果自旋CAS長(zhǎng)時(shí)間不成功音半,則會(huì)給CPU帶來(lái)非常大的開(kāi)銷,在JUC中贡蓖,有些地方就會(huì)限制CAS自旋的次數(shù)曹鸠。
    2、只能保證一個(gè)共享變量原子操作:看了CAS的實(shí)現(xiàn)就知道這只能針對(duì)一個(gè)共享變量斥铺,如果是多個(gè)共享變量就只能使用鎖了彻桃。或者把多個(gè)變量整成一個(gè)變量也可以用CAS晾蜘。
    3邻眷、ABA問(wèn)題:CAS需要檢查操作值有沒(méi)有發(fā)生改變,如果沒(méi)有發(fā)生改變則更新剔交,但是存在這樣一種情況:如果一個(gè)值原來(lái)是A肆饶,變成了B,然后又變成了A岖常,那么在CAS檢查的時(shí)候會(huì)發(fā)現(xiàn)沒(méi)有改變驯镊,但是實(shí)質(zhì)上它已經(jīng)發(fā)生了改變,這就是所謂的ABA問(wèn)題。對(duì)于ABA問(wèn)題的解決方案是加上版本號(hào)板惑,即在每個(gè)變量都加上一個(gè)版本號(hào)橄镜,每次改變時(shí)加1,即A->B->A冯乘,變成1A->2B->3A蛉鹿。例如原子類中AtomicInteger會(huì)發(fā)生ABA問(wèn)題,使用AtomicStampedReference可以解決ABA問(wèn)題往湿。

結(jié)語(yǔ)

其實(shí)在面試?yán)铮嗑€程惋戏,并發(fā)這塊問(wèn)的還是非常頻繁的领追,大家看完之后有什么不懂的歡迎在評(píng)論區(qū)討論,也可以私信問(wèn)我响逢,一般我看到之后都會(huì)回的绒窑!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市舔亭,隨后出現(xiàn)的幾起案子些膨,更是在濱河造成了極大的恐慌,老刑警劉巖钦铺,帶你破解...
    沈念sama閱讀 210,978評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件订雾,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡矛洞,警方通過(guò)查閱死者的電腦和手機(jī)洼哎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)沼本,“玉大人噩峦,你說(shuō)我怎么就攤上這事〕檎祝” “怎么了识补?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,623評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)辫红。 經(jīng)常有香客問(wèn)我凭涂,道長(zhǎng),這世上最難降的妖魔是什么厉熟? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,324評(píng)論 1 282
  • 正文 為了忘掉前任导盅,我火速辦了婚禮,結(jié)果婚禮上揍瑟,老公的妹妹穿的比我還像新娘白翻。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,390評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布滤馍。 她就那樣靜靜地躺著岛琼,像睡著了一般。 火紅的嫁衣襯著肌膚如雪巢株。 梳的紋絲不亂的頭發(fā)上槐瑞,一...
    開(kāi)封第一講書(shū)人閱讀 49,741評(píng)論 1 289
  • 那天,我揣著相機(jī)與錄音阁苞,去河邊找鬼困檩。 笑死,一個(gè)胖子當(dāng)著我的面吹牛那槽,可吹牛的內(nèi)容都是我干的悼沿。 我是一名探鬼主播,決...
    沈念sama閱讀 38,892評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼骚灸,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼糟趾!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起甚牲,我...
    開(kāi)封第一講書(shū)人閱讀 37,655評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤义郑,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后丈钙,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體非驮,經(jīng)...
    沈念sama閱讀 44,104評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年著恩,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了院尔。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,569評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡喉誊,死狀恐怖邀摆,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情伍茄,我是刑警寧澤栋盹,帶...
    沈念sama閱讀 34,254評(píng)論 4 328
  • 正文 年R本政府宣布,位于F島的核電站敷矫,受9級(jí)特大地震影響例获,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜曹仗,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,834評(píng)論 3 312
  • 文/蒙蒙 一榨汤、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧怎茫,春花似錦收壕、人聲如沸妓灌。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,725評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)虫埂。三九已至,卻和暖如春圃验,著一層夾襖步出監(jiān)牢的瞬間掉伏,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,950評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工澳窑, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留斧散,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,260評(píng)論 2 360
  • 正文 我出身青樓摊聋,卻偏偏與公主長(zhǎng)得像颅湘,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子栗精,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,446評(píng)論 2 348