請(qǐng)點(diǎn)贊,你的點(diǎn)贊對(duì)我意義重大,滿足下我的虛榮心。
??常在河邊走,哪有不濕鞋?;蛟S面試過(guò)程中你遇到的問題就在這呢?
??關(guān)注個(gè)人簡(jiǎn)介,面試不迷路~
一、假如只有一個(gè)cpu,單核,多線程還有用嗎 ?
這道題想考察什么?
是否了解并發(fā)相關(guān)的理論知識(shí)
考察的知識(shí)點(diǎn)
- cpu多線程的基本概念
- 操作系統(tǒng)的調(diào)度任務(wù)機(jī)制
- CPU密集型和IO密集型理論
考生應(yīng)該如何回答
CPU的執(zhí)行速度要遠(yuǎn)大于IO的過(guò)程,因此在大多數(shù)情況下增加一些復(fù)雜的CPU計(jì)算都比增加一次IO要快。單核CPU可以通過(guò)給每個(gè)線程分配CPU時(shí)間片(時(shí)間單元)來(lái)實(shí)現(xiàn)多線程機(jī)制。由于CPU頻率很高,故時(shí)間單元非常短。所以單核也可以實(shí)現(xiàn)多線程機(jī)制。
從用戶體現(xiàn)上說(shuō),單核多線程也能夠減少用戶響應(yīng)時(shí)間,例如web頁(yè)面,也是防止IO阻塞。處理器的數(shù)量和并不影響程序結(jié)構(gòu), 所以不管處理器的個(gè)數(shù)多少, 程序都可以通過(guò)使用多線程得到簡(jiǎn)化。
二、sychronied修飾普通方法和靜態(tài)方法的區(qū)別?什么是可見性?(小米)
這道題想考察什么?
是否了解Java并發(fā)編程的相關(guān)知識(shí)
考察的知識(shí)點(diǎn)
- sychronied的原理
- 并發(fā)的特性
考生應(yīng)該如何回答
sychronied是Java中并發(fā)編程的重要關(guān)鍵字之一。在并發(fā)編程中synchronized一直是解決線程安全問題,它可以保證原子性,可見性,以及有序性。
- 原子性:原子是構(gòu)成物質(zhì)的基本單位,所以原子的意思代表著—“不可分”。由不可分可知,具有原子性的操作也就是拒絕線程調(diào)度器中斷。
- 可見性:一個(gè)線程對(duì)共享變量的修改,另一個(gè)線程能夠立刻看到,稱為可見性。
- 有序性:程序按照代碼的先后順序執(zhí)行。編譯器為了優(yōu)化性能,有時(shí)會(huì)改變程序中語(yǔ)句的順序,但是不會(huì)影響最終的結(jié)果。有序性經(jīng)典的例子就是利用DCL雙重檢查創(chuàng)建單例對(duì)象。
synchronized可以修飾方法,也能夠使用synchronized(obj){}定義同步代碼塊。
-
修飾方法:
- 實(shí)例方法,作用于當(dāng)前實(shí)例加鎖,進(jìn)入方法前需要獲取當(dāng)前實(shí)例的鎖;
- 靜態(tài)方法,作用于當(dāng)前類對(duì)象加鎖,進(jìn)入方法前需要獲取當(dāng)前類對(duì)象的鎖;
修飾代碼塊,指定加鎖對(duì)象,對(duì)給定對(duì)象加鎖,進(jìn)入代碼塊前要獲得給定對(duì)象的鎖。
使用sychronied修飾普通方法和靜態(tài)方法,其實(shí)也等價(jià)于synchronized(this){}與synchronized(class){}。
三、Synchronized在JDK1.6之后做了哪些優(yōu)化 (京東)
這道題想考察什么?
對(duì)并發(fā)編程的掌握
考察的知識(shí)點(diǎn)
并發(fā)與synchronized原理
考生如何回答
synchronized是Java中非常重要的一個(gè)關(guān)鍵字,對(duì)于Android開發(fā)同學(xué)來(lái)說(shuō),考慮到多線程的情況,一般都直接使用到synchronized關(guān)鍵字對(duì)方法或者對(duì)象上鎖。但是問題是為什么加上synchronized關(guān)鍵字就能實(shí)現(xiàn)鎖,它的原理是怎么回事呢?
字節(jié)碼
如果我們使用javap -v xxx.class 反編譯這樣一個(gè)class文件
public static void main(String[] args) {
synchronized (InjectTest.class) {
System.out.println("hello!");
}
}
此時(shí)我們獲得到結(jié)果為:
可以看到j(luò)avap的輸出信息中存在:monitorenter與monitorexit指令。這就代表了同步代碼塊的入口與出口。
這里的monitor是:對(duì)象監(jiān)視器。在JVM中,每個(gè)對(duì)象都會(huì)和一個(gè)對(duì)象監(jiān)視器(monitor)相關(guān)聯(lián)。monitorenter指令插入到同步代碼塊開始的位置、monitorexit指令插入到同步代碼塊結(jié)束位置,jvm需要保證每個(gè)monitorenter都有一個(gè)monitorexit對(duì)應(yīng)。這兩個(gè)指令,本質(zhì)上都是對(duì) 對(duì)象監(jiān)視器(monitor)進(jìn)行獲取,這個(gè)過(guò)程是排他的,也就是說(shuō)同一時(shí)刻只能有一個(gè)線程獲取到由synchronized所保護(hù)對(duì)象的監(jiān)視器。線程執(zhí)行到monitorenter指令時(shí),會(huì)嘗試獲取對(duì)象所對(duì)應(yīng)的monitor所有權(quán),也就是嘗試獲取對(duì)象的鎖;而執(zhí)行monitorexit,就是釋放monitor的所有權(quán)。 如果其他線程已經(jīng)占用了monitor,則當(dāng)前線程進(jìn)入阻塞狀態(tài)。
當(dāng)然這是jdk1.6之前的行為,而jdk1.6以后為了減少獲得鎖和釋放鎖帶來(lái)的性能消耗,對(duì)synchronized鎖進(jìn)行了優(yōu)化,包含偏向鎖、輕量級(jí)鎖、重量級(jí)鎖;
關(guān)于鎖類型與相關(guān)信息的信息都是存放在鎖對(duì)象的對(duì)象頭中 ,在了解偏向鎖、輕量級(jí)鎖、重量級(jí)鎖之前,我們必須先認(rèn)識(shí)一下什么是對(duì)象頭!
Java對(duì)象頭
對(duì)象在虛擬機(jī)內(nèi)存中的布局分為三塊區(qū)域:對(duì)象頭、實(shí)例數(shù)據(jù)和對(duì)齊填充;Java對(duì)象頭是實(shí)現(xiàn)synchronized的鎖對(duì)象的基礎(chǔ),一般而言,synchronized使用的鎖對(duì)象是存儲(chǔ)在Java對(duì)象頭里。
對(duì)象頭由:存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù)的Mark Word 32位系統(tǒng)中4 + 指向類的指針 kClass pointer ,如果是數(shù)組對(duì)象還會(huì)有數(shù)組長(zhǎng)度 Array Length。
其中mark word中就存儲(chǔ)了鎖狀態(tài):

在無(wú)鎖狀態(tài)下,mark word中的數(shù)據(jù)為:

包含對(duì)象hashcode,gc年齡,是否偏向鎖與鎖標(biāo)志信息。
偏向鎖
而偏向鎖下,數(shù)據(jù)則為:

擁有鎖的線程ID, epoch 大家可以先理解為校驗(yàn)位,同時(shí) 是否偏向鎖標(biāo)記由相對(duì)無(wú)鎖狀態(tài)下的0變?yōu)?。
首先之所以會(huì)引入偏向鎖是因?yàn)椋捍蠖鄶?shù)情況下鎖不僅不存在多線程競(jìng)爭(zhēng),而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價(jià)更低而引入了偏向鎖,減少不必要的操作。
在程序進(jìn)入同步代碼塊時(shí),會(huì)訪問Mark Word中偏向鎖的標(biāo)識(shí)是否設(shè)置成1,鎖標(biāo)志位是否為01,若為偏向鎖狀態(tài),則查看偏向鎖狀態(tài)下線程ID是否指向當(dāng)前線程。如果是則直接執(zhí)行同步代碼。但是mark word中記錄的線程ID如果不是當(dāng)前線程,則通過(guò)CAS比較與交換嘗試修改對(duì)象頭獲得鎖。CAS操作成功則可以直接執(zhí)行同步代碼,否則表示有其他線程競(jìng)爭(zhēng),此時(shí)獲得偏向鎖的線程被掛起,偏向鎖升級(jí)為輕量級(jí)鎖 ,然后被阻塞的線程繼續(xù)往下執(zhí)行同步代碼。
輕量級(jí)鎖

輕量級(jí)鎖狀態(tài)下,代碼進(jìn)入同步塊時(shí),如果同步對(duì)象鎖狀態(tài)為無(wú)鎖狀態(tài), 虛擬機(jī)首先將在當(dāng)前線程的棧幀中建立一個(gè)名為鎖記錄(Lock Record)的空間,用于存儲(chǔ)鎖對(duì)象目前的Mark Word的拷貝 ,接著虛擬機(jī)將使用CAS操作嘗試將對(duì)象的Mark Word更新為指向Lock Record的指針 。這個(gè)操作如果成功則代表獲取到了鎖,但是如果失敗,則會(huì)檢查對(duì)象Mark Word是不是指向當(dāng)前線程棧幀中的鎖記錄,如果是,則說(shuō)明本身當(dāng)前線程就擁有此對(duì)象的鎖,就可以直接執(zhí)行同步代碼。否則說(shuō)明鎖對(duì)象被其他線程獲取,當(dāng)前線程是競(jìng)爭(zhēng)者,那么當(dāng)前線程會(huì)自旋等待鎖,也就是不斷重試,當(dāng)重試一定次數(shù)后,總不能一直重試下去吧,太耗CPU了。所以這時(shí)候就要升級(jí)為重量級(jí)鎖。
重量級(jí)鎖
重量級(jí)鎖就是通過(guò)對(duì)象監(jiān)視器(monitor)實(shí)現(xiàn),其中monitor的本質(zhì)是依賴于底層操作系統(tǒng)的Mutex Lock實(shí)現(xiàn),操作系統(tǒng)實(shí)現(xiàn)線程之間的切換需要從用戶態(tài)到內(nèi)核態(tài)的切換,切換成本非常高。主要是,當(dāng)系統(tǒng)檢查到鎖是重量級(jí)鎖之后,會(huì)把等待想要獲得鎖的線程進(jìn)行阻塞,被阻塞的線程不會(huì)消耗cup。但是阻塞或者喚醒一個(gè)線程時(shí),都需要操作系統(tǒng)來(lái)幫忙,這就需要從用戶態(tài)轉(zhuǎn)換到內(nèi)核態(tài),而轉(zhuǎn)換狀態(tài)是需要消耗很多時(shí)間的,有可能比用戶執(zhí)行代碼的時(shí)間還要長(zhǎng)。
四、CAS無(wú)鎖編程的原理(字節(jié)跳動(dòng))
這道題想考察什么?
并發(fā)相關(guān)問題,原子操作
考察的知識(shí)點(diǎn)
Java并發(fā)編程,樂觀鎖機(jī)制
考生如何回答
Jdk中java.util.concurrent.atomic包下的類都是采用CAS來(lái)實(shí)現(xiàn)的。
CAS原理分析
CAS(比較與交換,Compare and swap) 是一種有名的無(wú)鎖算法。無(wú)鎖編程,即不使用鎖的情況下實(shí)現(xiàn)多線程之間的變量同步,也就是在沒有線程被阻塞的情況下實(shí)現(xiàn)變量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。實(shí)現(xiàn)非阻塞同步的方案稱為“無(wú)鎖編程算法”( Non-blocking algorithm)。
CAS機(jī)制當(dāng)中使用了3個(gè)基本操作數(shù):內(nèi)存地址V,舊的預(yù)期值A(chǔ),要修改的新值B。更新一個(gè)變量的時(shí)候,只有當(dāng)變量的預(yù)期值A(chǔ)和內(nèi)存地址V當(dāng)中的實(shí)際值相同時(shí),才會(huì)將內(nèi)存地址V對(duì)應(yīng)的值修改為B。
1.在內(nèi)存地址V當(dāng)中,存儲(chǔ)著值為10的變量。

2.此時(shí)線程1想要把變量的值增加1。對(duì)線程1來(lái)說(shuō),舊的預(yù)期值A(chǔ)=10,要修改的新值B=11。

3.在線程1要提交更新之前,另一個(gè)線程2搶先一步,把內(nèi)存地址V中的變量值率先更新成了11。

4.線程1開始提交更新,首先進(jìn)行A和地址V的實(shí)際值比較(Compare) ,發(fā)現(xiàn)A不等于V的實(shí)際值,提交失敗。

5.線程1重新獲取內(nèi)存地址V的當(dāng)前值,并重新計(jì)算想要修改的新值。此時(shí)對(duì)線程1來(lái)說(shuō),A=11,B=12。這個(gè)重新嘗試的過(guò)程被稱為自旋。

6.這一次比較幸運(yùn),沒有其他線程改變地址V的值。線程1進(jìn)行Compare,發(fā)現(xiàn)A和地址V的實(shí)際值是相等的。

7.線程1進(jìn)行SWAP,把地址V的值替換為B,也就是12。

從思想上來(lái)說(shuō),Synchronized屬于悲觀鎖,悲觀地認(rèn)為程序中的并發(fā)情況嚴(yán)重,所以嚴(yán)防死守。CAS屬于樂觀鎖,樂觀地認(rèn)為程序中的并發(fā)情況不那么嚴(yán)重,所以讓線程不斷去嘗試更新。
CAS的缺點(diǎn)
ABA 問題
由于 CAS 設(shè)計(jì)機(jī)制就是獲取某兩個(gè)時(shí)刻(初始預(yù)期值和當(dāng)前內(nèi)存值)變量值,并進(jìn)行比較更新,所以說(shuō)如果在獲取初始預(yù)期值和當(dāng)前內(nèi)存值這段時(shí)間間隔內(nèi),變量值由 A 變?yōu)?B 再變?yōu)?A,那么對(duì)于 CAS 來(lái)說(shuō)是不可感知的,但實(shí)際上變量已經(jīng)發(fā)生了變化;解決辦法是在每次獲取時(shí)加版本號(hào),并且每次更新對(duì)版本號(hào) +1,這樣當(dāng)發(fā)生 ABA 問題時(shí)通過(guò)版本號(hào)可以得知變量被改動(dòng)過(guò)。JDK 1.5 以后的 AtomicStampedReference 類就提供了此種能力,其中的 compareAndSet 方法就是首先檢查當(dāng)前引用是否等于預(yù)期引用,并且當(dāng)前標(biāo)志是否等于預(yù)期標(biāo)志,如果全部相等,則以原子方式將該引用和該標(biāo)志的值設(shè)置為給定的更新值。
循環(huán)時(shí)間長(zhǎng)開銷大
所謂循環(huán)時(shí)間長(zhǎng)開銷大問題就是當(dāng) CAS 判定變量被修改了以后則放棄本次修改,但往往為了保證數(shù)據(jù)正確性該計(jì)算會(huì)以循環(huán)的方式再次發(fā)起 CAS,如果多次 CAS 判定失敗,則會(huì)產(chǎn)生大量的時(shí)間消耗和性能浪費(fèi);如果JVM能支持處理器提供的pause指令那么效率會(huì)有一定的提升,pause指令有兩個(gè)作用,第一它可以延遲流水線執(zhí)行指令(de-pipeline),使CPU不會(huì)消耗過(guò)多的執(zhí)行資源,延遲的時(shí)間取決于具體實(shí)現(xiàn)的版本,在一些處理器上延遲時(shí)間是零。第二它可以避免在退出循環(huán)的時(shí)候因內(nèi)存順序沖突(memory order violation)而引起CPU流水線被清空(CPU pipeline flush),從而提高CPU的執(zhí)行效率。
只能保證一個(gè)共享變量的原子操作
- CAS 只對(duì)單個(gè)共享變量有效,當(dāng)操作涉及跨多個(gè)共享變量時(shí) CAS 無(wú)效;從 JDK 1.5開始提供了 AtomicReference 類來(lái)保證引用對(duì)象之間的原子性,你可以把多個(gè)變量放在一個(gè)對(duì)象里來(lái)進(jìn)行 CAS 操作
- Unsafe是CAS的核心類,Java無(wú)法直接訪問底層操作系統(tǒng),而是通過(guò)本地(native)方法來(lái)訪問。不過(guò)盡管如此,JVM還是開了一個(gè)后門,JDK中有一個(gè)類Unsafe,它提供了硬件級(jí)別的原子操作。
- valueOffset表示的是變量值在內(nèi)存中的偏移地址,因?yàn)閁nsafe就是根據(jù)內(nèi)存偏移地址獲取數(shù)據(jù)的原值的。
- value是用volatile修飾的,保證了多線程之間看到的value值是同一份。
今天的面試分享到此結(jié)束拉~下期在見