前面整理了Java基礎(chǔ)、Mysql、Spring的高頻面試題,今天為大家?guī)?lái)Java并發(fā)方面的高頻面試題,因?yàn)椴l(fā)知識(shí)不管在學(xué)習(xí)、面試還是工作過(guò)程中都非常非常重要,看完本文,相信絕對(duì)能助你一臂之力。
1、線程和進(jìn)程有什么區(qū)別?
線程是進(jìn)程的子集,一個(gè)進(jìn)程可以有很多線程。每個(gè)進(jìn)程都有自己的內(nèi)存空間,可執(zhí)行代碼和唯一進(jìn)程標(biāo)識(shí)符(PID)。
每條線程并行執(zhí)行不同的任務(wù)。不同的進(jìn)程使用不同的內(nèi)存空間(線程自己的堆棧),而所有的線程共享一片相同的內(nèi)存空間(進(jìn)程主內(nèi)存)。別把它和棧內(nèi)存搞混,每個(gè)線程都擁有單獨(dú)的棧內(nèi)存用來(lái)存儲(chǔ)本地?cái)?shù)據(jù)。
2、實(shí)現(xiàn)多線程的方式有哪些?
- 繼承Thread類:Java單繼承,不推薦;
- 實(shí)現(xiàn)Runnable接口:Thread類也是繼承Runnable接口,推薦;
- 實(shí)現(xiàn)Callable接口:實(shí)現(xiàn)Callable接口,配合FutureTask使用,有返回值;
- 使用線程池:復(fù)用,節(jié)約資源;
- 更多方式可以參考我的文章使用Java Executor框架實(shí)現(xiàn)多線程
3、用Runnable還是Thread?
這個(gè)問題是上題的后續(xù),大家都知道我們可以通過(guò)繼承Thread類或者調(diào)用Runnable接口來(lái)實(shí)現(xiàn)線程,問題是,那個(gè)方法更好呢?什么情況下使用它?這個(gè)問題很容易回答,如果你知道Java不支持類的多重繼承,但允許你調(diào)用多個(gè)接口。所以如果你要繼承其他類,當(dāng)然是調(diào)用Runnable接口好了。
- Runnable和Thread兩者最大的區(qū)別是Thread是類而Runnable是接口,至于用類還是用接口,取決于繼承上的實(shí)際需要。Java類是單繼承的,實(shí)現(xiàn)多個(gè)接口可以實(shí)現(xiàn)類似多繼承的操作。
- 其次, Runnable就相當(dāng)于一個(gè)作業(yè),而Thread才是真正的處理線程,我們需要的只是定義這個(gè)作業(yè),然后將作業(yè)交給線程去處理,這樣就達(dá)到了松耦合,也符合面向?qū)ο罄锩娼M合的使用,另外也節(jié)省了函數(shù)開銷,繼承Thread的同時(shí),不僅擁有了作業(yè)的方法run(),還繼承了其他所有的方法。
- 當(dāng)需要?jiǎng)?chuàng)建大量線程的時(shí)候,有以下不足:①線程生命周期的開銷非常高;②資源消耗;③穩(wěn)定性。
- 如果二者都可以選擇不用,那就不用。因?yàn)镴ava這門語(yǔ)言發(fā)展到今天,在語(yǔ)言層面提供的多線程機(jī)制已經(jīng)比較豐富且高級(jí),完全不用在線程層面操作。直接使用Thread和Runnable這樣的“裸線程”元素比較容易出錯(cuò),還需要額外關(guān)注線程數(shù)等問題。建議:簡(jiǎn)單的多線程程序,使用Executor。復(fù)雜的多線程程序,使用一個(gè)Actor庫(kù),首推Akka。
- 如果一定要在Runnable和Thread中選擇一個(gè)使用,選擇Runnable。
4、Thread 類中的start() 和 run() 方法有什么區(qū)別?
這個(gè)問題經(jīng)常被問到,但還是能從此區(qū)分出面試者對(duì)Java線程模型的理解程度。start()方法被用來(lái)啟動(dòng)新創(chuàng)建的線程,而且start()內(nèi)部調(diào)用了run()方法,JDK 1.8源碼中start方法的注釋這樣寫到:Causes this thread to begin execution; the Java Virtual Machine calls the <code>run</code> method of this thread.這和直接調(diào)用run()方法的效果不一樣。當(dāng)你調(diào)用run()方法的時(shí)候,只會(huì)是在原來(lái)的線程中調(diào)用,沒有新的線程啟動(dòng),start()方法才會(huì)啟動(dòng)新線程,JDK 1.8源碼中注釋這樣寫:The result is that two threads are running concurrently: the current thread (which returns from the call to the <code>start</code> method) and the other thread (which executes its <code>run</code> method).。
new 一個(gè) Thread,線程進(jìn)入了新建狀態(tài);調(diào)用 start() 方法,會(huì)啟動(dòng)一個(gè)線程并使線程進(jìn)入了就緒狀態(tài),當(dāng)分配到時(shí)間片后就可以開始運(yùn)行了。start() 會(huì)執(zhí)行線程的相應(yīng)準(zhǔn)備工作,然后自動(dòng)執(zhí)行 run() 方法的內(nèi)容,這是真正的多線程工作。而直接執(zhí)行 run() 方法,會(huì)把 run 方法當(dāng)成一個(gè) main 線程下的普通方法去執(zhí)行,并不會(huì)在某個(gè)線程中執(zhí)行它,所以這并不是多線程工作。
總結(jié):調(diào)用 start 方法方可啟動(dòng)線程并使線程進(jìn)入就緒狀態(tài),而 run 方法只是 thread 的一個(gè)普通方法調(diào)用,還是在主線程里執(zhí)行。
5、說(shuō)說(shuō) sleep() 方法和 wait() 方法區(qū)別和共同點(diǎn)?
- 兩者最主要的區(qū)別在于:sleep 方法沒有釋放鎖,而 wait 方法釋放了鎖 。
- 兩者都可以暫停線程的執(zhí)行。
- Wait 通常被用于線程間交互/通信,sleep 通常被用于暫停執(zhí)行。
- wait() 方法被調(diào)用后,線程不會(huì)自動(dòng)蘇醒,需要?jiǎng)e的線程調(diào)用同一個(gè)對(duì)象上的 notify() 或者 notifyAll() 方法。sleep() 方法執(zhí)行完成后,線程會(huì)自動(dòng)蘇醒。
6、說(shuō)說(shuō)并發(fā)與并行的區(qū)別?
- 并發(fā): 同一時(shí)間段,多個(gè)任務(wù)都在執(zhí)行 (單位時(shí)間內(nèi)不一定同時(shí)執(zhí)行);
- 并行: 單位時(shí)間內(nèi),多個(gè)任務(wù)同時(shí)執(zhí)行。
7、說(shuō)說(shuō)線程的生命周期和狀態(tài)?
Java 線程在運(yùn)行的生命周期中的指定時(shí)刻只可能處于下面 6 種不同狀態(tài)的其中一個(gè)狀態(tài)(圖源《Java 并發(fā)編程藝術(shù)》4.1.4 節(jié))。
線程在生命周期中并不是固定處于某一個(gè)狀態(tài)而是隨著代碼的執(zhí)行在不同狀態(tài)之間切換。Java 線程狀態(tài)變遷如下圖所示(圖源《Java 并發(fā)編程藝術(shù)》4.1.4 節(jié)):
由上圖可以看出:線程創(chuàng)建之后它將處于 NEW(新建) 狀態(tài),調(diào)用 start() 方法后開始運(yùn)行,線程這時(shí)候處于 READY(可運(yùn)行) 狀態(tài)??蛇\(yùn)行狀態(tài)的線程獲得了 CPU 時(shí)間片(timeslice)后就處于 RUNNING(運(yùn)行) 狀態(tài)。
操作系統(tǒng)隱藏 Java 虛擬機(jī)(JVM)中的 RUNNABLE 和 RUNNING 狀態(tài),它只能看到 RUNNABLE 狀態(tài)(圖源:HowToDoInJava:Java Thread Life Cycle and Thread States),所以 Java 系統(tǒng)一般將這兩個(gè)狀態(tài)統(tǒng)稱為 RUNNABLE(運(yùn)行中) 狀態(tài) 。
當(dāng)線程執(zhí)行 wait()方法之后,線程進(jìn)入 WAITING(等待)狀態(tài)。進(jìn)入等待狀態(tài)的線程需要依靠其他線程的通知才能夠返回到運(yùn)行狀態(tài),而 TIME_WAITING(超時(shí)等待) 狀態(tài)相當(dāng)于在等待狀態(tài)的基礎(chǔ)上增加了超時(shí)限制,比如通過(guò) sleep(long millis)方法或 wait(long millis)方法可以將 Java 線程置于 TIMED WAITING 狀態(tài)。當(dāng)超時(shí)時(shí)間到達(dá)后 Java 線程將會(huì)返回到 RUNNABLE 狀態(tài)。當(dāng)線程調(diào)用同步方法時(shí),在沒有獲取到鎖的情況下,線程將會(huì)進(jìn)入到 BLOCKED(阻塞) 狀態(tài)。線程在執(zhí)行 Runnable 的run()方法之后將會(huì)進(jìn)入到 TERMINATED(終止) 狀態(tài)。
8、什么是線程死鎖?
多個(gè)線程同時(shí)被阻塞,它們中的一個(gè)或者全部都在等待某個(gè)資源被釋放。由于線程被無(wú)限期地阻塞,因此程序不可能正常終止。
如下圖所示,線程 A 持有資源 2,線程 B 持有資源 1,他們同時(shí)都想申請(qǐng)對(duì)方的資源,所以這兩個(gè)線程就會(huì)互相等待而進(jìn)入死鎖狀態(tài)。
下面通過(guò)一個(gè)例子來(lái)說(shuō)明線程死鎖,代碼模擬了上圖的死鎖的情況 (代碼來(lái)源于《并發(fā)編程之美》):
public class DeadLockDemo {
private static Object resource1 = new Object();//資源 1
private static Object resource2 = new Object();//資源 2
?
public static void main(String[] args) {
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "線程 1").start();
?
new Thread(() -> {
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource1");
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
}
}
}, "線程 2").start();
}
}
輸出:
Thread[線程 1,5,main]get resource1
Thread[線程 2,5,main]get resource2
Thread[線程 1,5,main]waiting get resource2
Thread[線程 2,5,main]waiting get resource1
線程 A 通過(guò) synchronized (resource1) 獲得 resource1 的監(jiān)視器鎖,然后通過(guò) Thread.sleep(1000);讓線程 A 休眠 1s 為的是讓線程 B 得到執(zhí)行然后獲取到 resource2 的監(jiān)視器鎖。線程 A 和線程 B 休眠結(jié)束了都開始企圖請(qǐng)求獲取對(duì)方的資源,然后這兩個(gè)線程就會(huì)陷入互相等待的狀態(tài),這也就產(chǎn)生了死鎖。上面的例子符合產(chǎn)生死鎖的四個(gè)必要條件。
學(xué)過(guò)操作系統(tǒng)的朋友都知道產(chǎn)生死鎖必須具備以下四個(gè)條件:
- 互斥條件:該資源任意一個(gè)時(shí)刻只由一個(gè)線程占用。
- 請(qǐng)求與保持條件:一個(gè)進(jìn)程因請(qǐng)求資源而阻塞時(shí),對(duì)已獲得的資源保持不放。
- 不剝奪條件:線程已獲得的資源在末使用完之前不能被其他線程強(qiáng)行剝奪,只有自己使用完畢后才釋放資源。
- 循環(huán)等待條件:若干進(jìn)程之間形成一種頭尾相接的循環(huán)等待資源關(guān)系。
9、如何避免線程死鎖?
我們只要破壞產(chǎn)生死鎖的四個(gè)條件中的其中一個(gè)就可以了。
- 破壞互斥條件:這個(gè)條件我們沒有辦法破壞,因?yàn)槲覀冇面i本來(lái)就是想讓他們互斥的(臨界資源需要互斥訪問)。
- 破壞請(qǐng)求與保持條件:一次性申請(qǐng)所有的資源。
- 破壞不剝奪條件:占用部分資源的線程進(jìn)一步申請(qǐng)其他資源時(shí),如果申請(qǐng)不到,可以主動(dòng)釋放它占有的資源。
- 破壞循環(huán)等待條件:靠按序申請(qǐng)資源來(lái)預(yù)防。按某一順序申請(qǐng)資源,釋放資源則反序釋放。破壞循環(huán)等待條件。
我們對(duì)線程 2 的代碼修改成下面這樣就不會(huì)產(chǎn)生死鎖了。
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "線程 2").start();
輸出:
Thread[線程 1,5,main]get resource1
Thread[線程 1,5,main]waiting get resource2
Thread[線程 1,5,main]get resource2
Thread[線程 2,5,main]get resource1
Thread[線程 2,5,main]waiting get resource2
Thread[線程 2,5,main]get resource2
?
Process finished with exit code 0
我們分析一下上面的代碼為什么避免了死鎖的發(fā)生?
線程 1 首先獲得到 resource1 的監(jiān)視器鎖,這時(shí)候線程 2 就獲取不到了。然后線程 1 再去獲取 resource2 的監(jiān)視器鎖,可以獲取到。然后線程 1 釋放了對(duì) resource1、resource2 的監(jiān)視器鎖的占用,線程 2 獲取到就可以執(zhí)行了。這樣就破壞了破壞循環(huán)等待條件,因此避免了死鎖。
10、什么是死鎖,活鎖?
- 死鎖:多個(gè)線程都無(wú)法獲得資源繼續(xù)執(zhí)行。可以通過(guò)避免一個(gè)線程獲取多個(gè)鎖;一個(gè)鎖占用一個(gè)資源;使用定時(shí)鎖;數(shù)據(jù)庫(kù)加解鎖在一個(gè)連接中。
- 死鎖的必要條件:環(huán)路等待,不可剝奪,請(qǐng)求保持,互斥條件
- 活鎖:線程之間相互謙讓資源,都無(wú)法獲取所有資源繼續(xù)執(zhí)行。
11、Java中CyclicBarrier 和 CountDownLatch有什么不同?
CyclicBarrier 和 CountDownLatch 都可以用來(lái)讓一組線程等待其它線程。與 CyclicBarrier 不同的是,CountdownLatch 不能重新使用。
- CountDownLatch是一種靈活的閉鎖實(shí)現(xiàn),可以使一個(gè)或者多個(gè)線程等待一組事件發(fā)生。閉鎖狀態(tài)包括一個(gè)計(jì)數(shù)器,改計(jì)數(shù)器初始化為一個(gè)正數(shù),表示需要等待的事件數(shù)量。countDown方法遞減計(jì)數(shù)器,表示有一個(gè)事件發(fā)生了,而await方法等待計(jì)數(shù)器到達(dá)0,表示所有需要等待的事情都已經(jīng)發(fā)生。如果計(jì)數(shù)器的值非零,那么await就會(huì)一直阻塞知道計(jì)數(shù)器的值為0,或者等待的線程中斷,或者等待超時(shí)。
- CyclicBarrier適用于這樣的情況:你希望創(chuàng)建一組任務(wù),他們并行地執(zhí)行工作,然后在進(jìn)行下一個(gè)步驟之前等待,直至所有任務(wù)都完成。它使得所有的并行任務(wù)都將在柵欄出列隊(duì),因此可以一致的向前移動(dòng)。這非常像CountDownLatch,只是CountDownLatch是只觸發(fā)一次的事件,而CyclicBarrier可以多次重用。
12、Java中的同步集合與并發(fā)集合有什么區(qū)別?
- 同步集合與并發(fā)集合都為多線程和并發(fā)提供了合適的線程安全的集合,不過(guò)并發(fā)集合的可擴(kuò)展性更高。在Java1.5之前程序員們只有同步集合來(lái)用且在多線程并發(fā)的時(shí)候會(huì)導(dǎo)致爭(zhēng)用,阻礙了系統(tǒng)的擴(kuò)展性。Java5介紹了并發(fā)集合像ConcurrentHashMap,不僅提供線程安全還用鎖分離和內(nèi)部分區(qū)等現(xiàn)代技術(shù)提高了可擴(kuò)展性。
- 同步容器是線程安全的。同步容器將所有對(duì)容器狀態(tài)的訪問都串行化,以實(shí)現(xiàn)他們的線程安全性。這種方法的代價(jià)是嚴(yán)重降低并發(fā)性,當(dāng)多個(gè)線程競(jìng)爭(zhēng)容器的鎖時(shí),吞吐量將嚴(yán)重降低。并發(fā)容器是針對(duì)多個(gè)線程并發(fā)訪問設(shè)計(jì)的,改進(jìn)了同步容器的性能。通過(guò)并發(fā)容器來(lái)代替同步容器,可以極大地提高伸縮性并降低風(fēng)險(xiǎn)。
13、你如何在Java中獲取線程堆棧?
對(duì)于不同的操作系統(tǒng),有多種方法來(lái)獲得Java進(jìn)程的線程堆棧。當(dāng)你獲取線程堆棧時(shí),JVM會(huì)把所有線程的狀態(tài)存到日志文件或者輸出到控制臺(tái)。在Windows你可以使用Ctrl + Break組合鍵來(lái)獲取線程堆棧,Linux下用kill -3命令。你也可以用jstack這個(gè)工具來(lái)獲取,它對(duì)線程id進(jìn)行操作,你可以用jps這個(gè)工具找到id。
14、Java中ConcurrentHashMap的并發(fā)度是什么?
- ConcurrentHashMap把實(shí)際map劃分成若干部分來(lái)實(shí)現(xiàn)它的可擴(kuò)展性和線程安全。這種劃分是使用并發(fā)度獲得的,它是ConcurrentHashMap類構(gòu)造函數(shù)的一個(gè)可選參數(shù),默認(rèn)值為16,這樣在多線程情況下就能避免爭(zhēng)用。
- 并發(fā)度可以理解為程序運(yùn)行時(shí)能夠同時(shí)更新ConccurentHashMap且不產(chǎn)生鎖競(jìng)爭(zhēng)的最大線程數(shù),實(shí)際上就是ConcurrentHashMap中的分段鎖個(gè)數(shù),即Segment[]的數(shù)組長(zhǎng)度。ConcurrentHashMap默認(rèn)的并發(fā)度為16,但用戶也可以在構(gòu)造函數(shù)中設(shè)置并發(fā)度。當(dāng)用戶設(shè)置并發(fā)度時(shí),ConcurrentHashMap會(huì)使用大于等于該值的最小2冪指數(shù)作為實(shí)際并發(fā)度(假如用戶設(shè)置并發(fā)度為17,實(shí)際并發(fā)度則為32)。運(yùn)行時(shí)通過(guò)將key的高n位(n = 32 – segmentShift)和并發(fā)度減1(segmentMask)做位與運(yùn)算定位到所在的Segment。segmentShift與segmentMask都是在構(gòu)造過(guò)程中根據(jù)concurrency level被相應(yīng)的計(jì)算出來(lái)。
- 如果并發(fā)度設(shè)置的過(guò)小,會(huì)帶來(lái)嚴(yán)重的鎖競(jìng)爭(zhēng)問題;如果并發(fā)度設(shè)置的過(guò)大,原本位于同一個(gè)Segment內(nèi)的訪問會(huì)擴(kuò)散到不同的Segment中,CPU cache命中率會(huì)下降,從而引起程序性能下降。
15、Java中的同步集合與并發(fā)集合有什么區(qū)別?
- 同步集合與并發(fā)集合都為多線程和并發(fā)提供了合適的線程安全的集合,不過(guò)并發(fā)集合的可擴(kuò)展性更高。在Java1.5之前程序員們只有同步集合來(lái)用且在多線程并發(fā)的時(shí)候會(huì)導(dǎo)致爭(zhēng)用,阻礙了系統(tǒng)的擴(kuò)展性。Java5介紹了并發(fā)集合像ConcurrentHashMap,不僅提供線程安全還用鎖分離和內(nèi)部分區(qū)等現(xiàn)代技術(shù)提高了可擴(kuò)展性。
- 同步容器是線程安全的。同步容器將所有對(duì)容器狀態(tài)的訪問都串行化,以實(shí)現(xiàn)他們的線程安全性。這種方法的代價(jià)是嚴(yán)重降低并發(fā)性,當(dāng)多個(gè)線程競(jìng)爭(zhēng)容器的鎖時(shí),吞吐量將嚴(yán)重降低。并發(fā)容器是針對(duì)多個(gè)線程并發(fā)訪問設(shè)計(jì)的,改進(jìn)了同步容器的性能。通過(guò)并發(fā)容器來(lái)代替同步容器,可以極大地提高伸縮性并降低風(fēng)險(xiǎn)。
16、Thread類中的yield方法有什么作用?
- Yield方法可以暫停當(dāng)前正在執(zhí)行的線程對(duì)象,讓其它有相同優(yōu)先級(jí)的線程執(zhí)行。它是一個(gè)靜態(tài)方法而且只保證當(dāng)前線程放棄CPU占用而不能保證使其它線程一定能占用CPU,執(zhí)行yield()的線程有可能在進(jìn)入到暫停狀態(tài)后馬上又被執(zhí)行。
- 線程讓步:如果知道已經(jīng)完成了在run()方法的循環(huán)的一次迭代過(guò)程中所需的工作,就可以給線程調(diào)度機(jī)制一個(gè)暗示:你的工作已經(jīng)做得差不多了,可以讓別的線程使用CPU了。這個(gè)暗示將通過(guò)調(diào)用yield()方法來(lái)做出(不過(guò)這只是一個(gè)暗示,沒有任何機(jī)制保證它將會(huì)被采納)。當(dāng)調(diào)用yield()時(shí),也是在建議具有相同優(yōu)先級(jí)的其他線程可以運(yùn)行。
- yield()的作用是讓步。它能讓當(dāng)前線程由“運(yùn)行狀態(tài)”進(jìn)入到“就緒狀態(tài)”,從而讓其它具有相同優(yōu)先級(jí)的等待線程獲取執(zhí)行權(quán);但是,并不能保證在當(dāng)前線程調(diào)用yield()之后,其它具有相同優(yōu)先級(jí)的線程就一定能獲得執(zhí)行權(quán);也有可能是當(dāng)前線程又進(jìn)入到“運(yùn)行狀態(tài)”繼續(xù)運(yùn)行!
17、什么是ThreadLocal變量?
ThreadLocal是Java里一種特殊的變量。每個(gè)線程都有一個(gè)ThreadLocal就是每個(gè)線程都擁有了自己獨(dú)立的一個(gè)變量,競(jìng)爭(zhēng)條件被徹底消除了。它是為創(chuàng)建代價(jià)高昂的對(duì)象獲取線程安全的好方法,比如你可以用ThreadLocal讓SimpleDateFormat變成線程安全的,因?yàn)槟莻€(gè)類創(chuàng)建代價(jià)高昂且每次調(diào)用都需要?jiǎng)?chuàng)建不同的實(shí)例所以不值得在局部范圍使用它,如果為每個(gè)線程提供一個(gè)自己獨(dú)有的變量拷貝,將大大提高效率。首先,通過(guò)復(fù)用減少了代價(jià)高昂的對(duì)象的創(chuàng)建個(gè)數(shù)。其次,你在沒有使用高代價(jià)的同步或者不變性的情況下獲得了線程安全。線程局部變量的另一個(gè)不錯(cuò)的例子是ThreadLocalRandom類,它在多線程環(huán)境中減少了創(chuàng)建代價(jià)高昂的Random對(duì)象的個(gè)數(shù)。
ThreadLocal是一種線程封閉技術(shù)。ThreadLocal提供了get和set等訪問接口或方法,這些方法為每個(gè)使用該變量的線程都存有一份獨(dú)立的副本,因此get總是返回由當(dāng)前執(zhí)行線程在調(diào)用set時(shí)設(shè)置的最新值。
** 18、Java內(nèi)存模型是什么?**
Java內(nèi)存模型規(guī)定和指引Java程序在不同的內(nèi)存架構(gòu)、CPU和操作系統(tǒng)間有確定性地行為。它在多線程的情況下尤其重要。Java內(nèi)存模型對(duì)一個(gè)線程所做的變動(dòng)能被其它線程可見提供了保證,它們之間是先行發(fā)生關(guān)系。這個(gè)關(guān)系定義了一些規(guī)則讓程序員在并發(fā)編程時(shí)思路更清晰。比如,先行發(fā)生關(guān)系確保了:
- 線程內(nèi)的代碼能夠按先后順序執(zhí)行,這被稱為程序次序規(guī)則。
- 對(duì)于同一個(gè)鎖,一個(gè)解鎖操作一定要發(fā)生在時(shí)間上后發(fā)生的另一個(gè)鎖定操作之前,也叫做管程鎖定規(guī)則。
- 前一個(gè)對(duì)volatile的寫操作在后一個(gè)volatile的讀操作之前,也叫volatile變量規(guī)則。
- 一個(gè)線程內(nèi)的任何操作必需在這個(gè)線程的start()調(diào)用之后,也叫作線程啟動(dòng)規(guī)則。
- 一個(gè)線程的所有操作都會(huì)在線程終止之前,線程終止規(guī)則。
- 一個(gè)對(duì)象的終結(jié)操作必需在這個(gè)對(duì)象構(gòu)造完成之后,也叫對(duì)象終結(jié)規(guī)則。
- 可傳遞性
我強(qiáng)烈建議大家閱讀《Java并發(fā)編程實(shí)踐》第十六章來(lái)加深對(duì)Java內(nèi)存模型的理解。
19、Java中的volatile 變量是什么?
volatile是一個(gè)特殊的修飾符,只有成員變量才能使用它。在Java并發(fā)程序缺少同步類的情況下,多線程對(duì)成員變量的操作對(duì)其它線程是透明的。volatile變量可以保證下一個(gè)讀取操作會(huì)在前一個(gè)寫操作之后發(fā)生,就是上一題的volatile變量規(guī)則。
Java語(yǔ)言提供了一種稍弱的同步機(jī)制,即volatile變量,用來(lái)確保將變量的更新操作通知到其他線程。當(dāng)把變量聲明為volatile類型后,編譯器和運(yùn)行時(shí)都會(huì)注意到這個(gè)變量是共享的,因此不會(huì)將變量上的操作和其他內(nèi)存操作一起重排序。volatile變量不會(huì)被緩存在寄存器或者對(duì)其他處理器不可見的地方,因此在讀取volatile類型的時(shí)候總會(huì)返回最新寫入的值。
在訪問volatile變量時(shí)不會(huì)執(zhí)行加鎖操作,因此也不會(huì)使執(zhí)行線程阻塞,因此volatile變量是一種比synchronized關(guān)鍵字更輕量級(jí)的同步機(jī)制。
加鎖機(jī)制既可以確??梢娦杂挚梢源_保原子性,而volatile變量只能確??梢娦浴?/p>
20、volatile 變量和 atomic 變量有什么不同?
這是個(gè)有趣的問題。首先,volatile 變量和 atomic 變量看起來(lái)很像,但功能卻不一樣。Volatile變量可以確保先行關(guān)系,即寫操作會(huì)發(fā)生在后續(xù)的讀操作之前, 但它并不能保證原子性。例如用volatile修飾count變量那么 count++ 操作就不是原子性的。而AtomicInteger類提供的atomic方法可以讓這種操作具有原子性如getAndIncrement()方法會(huì)原子性的進(jìn)行增量操作把當(dāng)前值加一,其它數(shù)據(jù)類型和引用變量也可以進(jìn)行相似操作。
21、Java中Runnable和Callable有什么不同?
- Runnable和Callable都代表那些要在不同的線程中執(zhí)行的任務(wù)。Runnable從JDK1.0開始就有了,Callable是在JDK1.5增加的。它們的主要區(qū)別是Callable的 call() 方法可以返回值和拋出異常,而Runnable的run()方法沒有這些功能。Callable可以返回裝載有計(jì)算結(jié)果的Future對(duì)象。
- Runnable是執(zhí)行工作的獨(dú)立任務(wù),但是它不返回任何值。如果希望任務(wù)在完成的時(shí)候能夠返回一個(gè)值,那么可以實(shí)現(xiàn)Callable接口而不是Runnable接口。在Java SE5中引入的Callable是一種具有類型參數(shù)的泛型,它的類型參數(shù)表示的是從方法call()(而不是run())中返回的值,并且必須使用ExecutorService.submit()方法調(diào)用它。submit()方法會(huì)產(chǎn)生Future對(duì)象,它用Callable返回結(jié)果的特定類型進(jìn)行了參數(shù)化。
22、哪些操作釋放鎖,哪些不釋放鎖?
- sleep(): 釋放資源,不釋放鎖,進(jìn)入阻塞狀態(tài),喚醒隨機(jī)線程,Thread類方法。
- wait(): 釋放資源,釋放鎖,Object類方法。
- yield(): 不釋放鎖,進(jìn)入可執(zhí)行狀態(tài),選擇優(yōu)先級(jí)高的線程執(zhí)行,Thread類方法。
- 如果線程產(chǎn)生的異常沒有被捕獲,會(huì)釋放鎖。
23、如何正確的終止線程?
- 使用共享變量,要用volatile關(guān)鍵字,保證可見性,能夠及時(shí)終止。
- 使用interrupt()和isInterrupted()配合使用。
24、interrupt(), interrupted(), isInterrupted()的區(qū)別?
- interrupt():設(shè)置中斷標(biāo)志;
- interrupted():響應(yīng)中斷標(biāo)志并復(fù)位中斷標(biāo)志;
- isInterrupted():響應(yīng)中斷標(biāo)志;
25、synchronized的鎖對(duì)象是哪些?
- 普通方法是當(dāng)前實(shí)例對(duì)象;
- 同步方法快是括號(hào)中配置內(nèi)容,可以是類Class對(duì)象,可以是實(shí)例對(duì)象;
- 靜態(tài)方法是當(dāng)前類Class對(duì)象。
- 只要不是同一個(gè)鎖,就可以并行執(zhí)行,同一個(gè)鎖,只能串行執(zhí)行。
- 更多參考我的文章Java中Synchronized關(guān)鍵字簡(jiǎn)介(譯)
26、volatile和synchronized的區(qū)別是什么?
- volatile只能使用在變量上;而synchronized可以在類,變量,方法和代碼塊上。
- volatile至保證可見性;synchronized保證原子性與可見性。
- volatile禁用指令重排序;synchronized不會(huì)。
- volatile不會(huì)造成阻塞;synchronized會(huì)。
27、什么是緩存一致性協(xié)議?
因?yàn)镃PU是運(yùn)算很快,而主存的讀寫很忙,所以在程序運(yùn)行中,會(huì)復(fù)制一份數(shù)據(jù)到高速緩存,處理完成在將結(jié)果保存主存.
這樣存在一些問題,在多核CPU中多個(gè)線程,多個(gè)線程拷貝多份的高速緩存數(shù)據(jù),最后在計(jì)算完成,刷到主存的數(shù)據(jù)就會(huì)出現(xiàn)覆蓋
所以就出現(xiàn)了緩存一致性協(xié)議。最出名的就是Intel 的MESI協(xié)議,MESI協(xié)議保證了每個(gè)緩存中使用的共享變量的副本是一致的。它核心的思想是:當(dāng)CPU寫數(shù)據(jù)時(shí),如果發(fā)現(xiàn)操作的變量是共享變量,即在其他CPU中也存在該變量的副本,會(huì)發(fā)出信號(hào)通知其他CPU將該變量的緩存行置為無(wú)效狀態(tài),因此當(dāng)其他CPU需要讀取這個(gè)變量時(shí),發(fā)現(xiàn)自己緩存中緩存該變量的緩存行是無(wú)效的,那么它就會(huì)從內(nèi)存重新讀取。
28、Synchronized關(guān)鍵字、Lock,并解釋它們之間的區(qū)別?
Synchronized 與Lock都是可重入鎖,同一個(gè)線程再次進(jìn)入同步代碼的時(shí)候.可以使用自己已經(jīng)獲取到的鎖
Synchronized是悲觀鎖機(jī)制,獨(dú)占鎖。而Locks.ReentrantLock是,每次不加鎖而是假設(shè)沒有沖突而去完成某項(xiàng)操作,如果因?yàn)闆_突失敗就重試,直到成功為止。ReentrantLock適用場(chǎng)景
某個(gè)線程在等待一個(gè)鎖的控制權(quán)的這段時(shí)間需要中斷
需要分開處理一些wait-notify,ReentrantLock里面的Condition應(yīng)用,能夠控制notify哪個(gè)線程,鎖可以綁定多個(gè)條件。
具有公平鎖功能,每個(gè)到來(lái)的線程都將排隊(duì)等候。
29、Volatile如何保證內(nèi)存可見性?
- 當(dāng)寫一個(gè)volatile變量時(shí),JMM會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存中的共享變量刷新到主內(nèi)存。
- 當(dāng)讀一個(gè)volatile變量時(shí),JMM會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存置為無(wú)效。線程接下來(lái)將從主內(nèi)存中讀取共享變量。
30、 Java中什么是競(jìng)態(tài)條件?
競(jìng)態(tài)條件會(huì)導(dǎo)致程序在并發(fā)情況下出現(xiàn)一些bugs。多線程對(duì)一些資源的競(jìng)爭(zhēng)的時(shí)候就會(huì)產(chǎn)生競(jìng)態(tài)條件,如果首先要執(zhí)行的程序競(jìng)爭(zhēng)失敗排到后面執(zhí)行了,那么整個(gè)程序就會(huì)出現(xiàn)一些不確定的bugs。這種bugs很難發(fā)現(xiàn)而且會(huì)重復(fù)出現(xiàn),因?yàn)榫€程間的隨機(jī)競(jìng)爭(zhēng)。
31、為什么wait, notify 和 notifyAll這些方法不在thread類里面?
明顯的原因是JAVA提供的鎖是對(duì)象級(jí)的而不是線程級(jí)的,每個(gè)對(duì)象都有鎖,通過(guò)線程獲得。如果線程需要等待某些鎖那么調(diào)用對(duì)象中的wait()方法就有意義了。如果wait()方法定義在Thread類中,線程正在等待的是哪個(gè)鎖就不明顯了。簡(jiǎn)單的說(shuō),由于wait,notify和notifyAll都是鎖級(jí)別的操作,所以把他們定義在Object類中因?yàn)殒i屬于對(duì)象。
32、Java中synchronized 和 ReentrantLock 有什么不同?
相似點(diǎn):
這兩種同步方式有很多相似之處,它們都是加鎖方式同步,而且都是阻塞式的同步,也就是說(shuō)當(dāng)如果一個(gè)線程獲得了對(duì)象鎖,進(jìn)入了同步塊,其他訪問該同步塊的線程都必須阻塞在同步塊外面等待,而進(jìn)行線程阻塞和喚醒的代價(jià)是比較高的.
區(qū)別:
這兩種方式最大區(qū)別就是對(duì)于Synchronized來(lái)說(shuō),它是java語(yǔ)言的關(guān)鍵字,是原生語(yǔ)法層面的互斥,需要jvm實(shí)現(xiàn)。而ReentrantLock它是JDK 1.5之后提供的API層面的互斥鎖,需要lock()和unlock()方法配合try/finally語(yǔ)句塊來(lái)完成。
Synchronized進(jìn)過(guò)編譯,會(huì)在同步塊的前后分別形成monitorenter和monitorexit這個(gè)兩個(gè)字節(jié)碼指令。在執(zhí)行monitorenter指令時(shí),首先要嘗試獲取對(duì)象鎖。如果這個(gè)對(duì)象沒被鎖定,或者當(dāng)前線程已經(jīng)擁有了那個(gè)對(duì)象鎖,把鎖的計(jì)算器加1,相應(yīng)的,在執(zhí)行monitorexit指令時(shí)會(huì)將鎖計(jì)算器就減1,當(dāng)計(jì)算器為0時(shí),鎖就被釋放了。如果獲取對(duì)象鎖失敗,那當(dāng)前線程就要阻塞,直到對(duì)象鎖被另一個(gè)線程釋放為止。
由于ReentrantLock是java.util.concurrent包下提供的一套互斥鎖,相比Synchronized,ReentrantLock類提供了一些高級(jí)功能,主要有以下3項(xiàng):
- 等待可中斷,持有鎖的線程長(zhǎng)期不釋放的時(shí)候,正在等待的線程可以選擇放棄等待,這相當(dāng)于Synchronized來(lái)說(shuō)可以避免出現(xiàn)死鎖的情況。
- 公平鎖,多個(gè)線程等待同一個(gè)鎖時(shí),必須按照申請(qǐng)鎖的時(shí)間順序獲得鎖,Synchronized鎖非公平鎖,ReentrantLock默認(rèn)的構(gòu)造函數(shù)是創(chuàng)建的非公平鎖,可以通過(guò)參數(shù)true設(shè)為公平鎖,但公平鎖表現(xiàn)的性能不是很好。
- 鎖綁定多個(gè)條件,一個(gè)ReentrantLock對(duì)象可以同時(shí)綁定對(duì)個(gè)對(duì)象。
33、Synchronized 用過(guò)嗎,其原理是什么?
這是一道 Java 面試中幾乎百分百會(huì)問到的問題,因?yàn)橹灰浅绦騿T就一定會(huì)通過(guò)或者接觸過(guò)Synchronized。
答:Synchronized 是由 JVM 實(shí)現(xiàn)的一種實(shí)現(xiàn)互斥同步的一種方式,如果 你查看被 Synchronized 修飾過(guò)的程序塊編譯后的字節(jié)碼,會(huì)發(fā)現(xiàn), 被 Synchronized 修飾過(guò)的程序塊,在編譯前后被編譯器生成了monitorenter 和 monitorexit 兩 個(gè) 字 節(jié) 碼 指 令 。
這兩個(gè)指令是什么意思呢?
在虛擬機(jī)執(zhí)行到 monitorenter 指令時(shí),首先要嘗試獲取對(duì)象的鎖: 如果這個(gè)對(duì)象沒有鎖定,或者當(dāng)前線程已經(jīng)擁有了這個(gè)對(duì)象的鎖,把鎖 的計(jì)數(shù)器 +1;當(dāng)執(zhí)行 monitorexit 指令時(shí)將鎖計(jì)數(shù)器 -1;當(dāng)計(jì)數(shù)器 為 0 時(shí),鎖就被釋放了。如果獲取對(duì)象失敗了,那當(dāng)前線程就要阻塞等待,直到對(duì)象鎖被另外一 個(gè)線程釋放為止。
Java 中 Synchronize 通過(guò)在對(duì)象頭設(shè)置標(biāo)記,達(dá)到了獲取鎖和釋放 鎖的目的。
34、上面提到獲取對(duì)象的鎖,這個(gè)“鎖”到底是什么?如何確定對(duì)象的鎖?
答:“鎖”的本質(zhì)其實(shí)是 monitorenter 和 monitorexit 字節(jié)碼指令的一 個(gè) Reference 類型的參數(shù),即要鎖定和解鎖的對(duì)象。我們知道,使用Synchronized 可以修飾不同的對(duì)象,因此,對(duì)應(yīng)的對(duì)象鎖可以這么確 定:
如果 Synchronized 明確指定了鎖對(duì)象,比如 Synchronized(變量 名)、Synchronized(this) 等,說(shuō)明加解鎖對(duì)象為該對(duì)象。
如果沒有明確指定:
- 若 Synchronized 修飾的方法為非靜態(tài)方法,表示此方法對(duì)應(yīng)的對(duì)象為 鎖對(duì)象;
- 若 Synchronized 修飾的方法為靜態(tài)方法,則表示此方法對(duì)應(yīng)的類對(duì)象 為鎖對(duì)象。
注意,當(dāng)一個(gè)對(duì)象被鎖住時(shí),對(duì)象里面所有用 Synchronized 修飾的 方法都將產(chǎn)生堵塞,而對(duì)象里非 Synchronized 修飾的方法可正常被 調(diào)用,不受鎖影響。
35、什么是可重入性,為什么說(shuō) Synchronized 是可重入鎖?
先來(lái)看一下維基百科關(guān)于可重入鎖的定義:
若一個(gè)程序或子程序可以“在任意時(shí)刻被中斷然后操作系統(tǒng)調(diào)度執(zhí)行另外一段代碼,這段代碼又調(diào)用了該子程序不會(huì)出錯(cuò)”,則稱其為可重入(reentrant或re-entrant)的。即當(dāng)該子程序正在運(yùn)行時(shí),執(zhí)行線程可以再次進(jìn)入并執(zhí)行它,仍然獲得符合設(shè)計(jì)時(shí)預(yù)期的結(jié)果。與多線程并發(fā)執(zhí)行的線程安全不同,可重入強(qiáng)調(diào)對(duì)單個(gè)線程執(zhí)行時(shí)重新進(jìn)入同一個(gè)子程序仍然是安全的。
通俗來(lái)說(shuō):當(dāng)線程請(qǐng)求一個(gè)由其它線程持有的對(duì)象鎖時(shí),該線程會(huì)阻塞,而當(dāng)線程請(qǐng)求由自己持有的對(duì)象鎖時(shí),如果該鎖是重入鎖,請(qǐng)求就會(huì)成功,否則阻塞。
要證明synchronized是不是可重入鎖,我們先來(lái)看一段代碼:
package com.mzc.common.concurrent.synchronize;
?
/**
* <p class="detail">
* 功能: 證明synchronized為什么是可重入鎖
* </p>
*
* @author Moore
* @ClassName Super class.
* @Version V1.0.
* @date 2020.02.07 15:34:12
*/
public class SuperClass {
?
public synchronized void doSomething(){
System.out.println("father is doing something,the thread name is:"+Thread.currentThread().getName());
}
}
package com.mzc.common.concurrent.synchronize;
?
/**
* <p class="detail">
* 功能: 證明synchronized為什么是可重入鎖
* </p>
*
* @author Moore
* @ClassName Sub class.
* @Version V1.0.
* @date 2020.02.07 15:34:41
*/
public class SubClass extends SuperClass {
?
public synchronized void doSomething() {
System.out.println("child is doing doSomething,the thread name is:" + Thread.currentThread().getName());
// 調(diào)用自己類中其他的synchronized方法
doAnotherThing();
}
?
private synchronized void doAnotherThing() {
// 調(diào)用父類的synchronized方法
super.doSomething();
System.out.println("child is doing anotherThing,the thread name is:" + Thread.currentThread().getName());
}
?
public static void main(String[] args) {
SubClass child = new SubClass();
child.doSomething();
}
}
通過(guò)運(yùn)行main方法,先一下結(jié)果:
child is doing doSomething,the thread name is:main
father is doing something,the thread name is:main
child is doing anotherThing,the thread name is:main
因?yàn)檫@些方法輸出了相同的線程名稱,表明即使遞歸使用synchronized也沒有發(fā)生死鎖,證明其是可重入的。
還看不懂?那我就再解釋下!
這里的對(duì)象鎖只有一個(gè),就是 child 對(duì)象的鎖,當(dāng)執(zhí)行 child.doSomething 時(shí),該線程獲得 child 對(duì)象的鎖,在 doSomething 方法內(nèi)執(zhí)行 doAnotherThing 時(shí)再次請(qǐng)求child對(duì)象的鎖,因?yàn)閟ynchronized 是重入鎖,所以可以得到該鎖,繼續(xù)在 doAnotherThing 里執(zhí)行父類的 doSomething 方法時(shí)第三次請(qǐng)求 child 對(duì)象的鎖,同樣可得到。如果不是重入鎖的話,那這后面這兩次請(qǐng)求鎖將會(huì)被一直阻塞,從而導(dǎo)致死鎖。
所以在 java 內(nèi)部,同一線程在調(diào)用自己類中其他 synchronized 方法/塊或調(diào)用父類的 synchronized 方法/塊都不會(huì)阻礙該線程的執(zhí)行。就是說(shuō)同一線程對(duì)同一個(gè)對(duì)象鎖是可重入的,而且同一個(gè)線程可以獲取同一把鎖多次,也就是可以多次重入。因?yàn)閖ava線程是基于“每線程(per-thread)”,而不是基于“每調(diào)用(per-invocation)”的(java中線程獲得對(duì)象鎖的操作是以線程為粒度的,per-invocation 互斥體獲得對(duì)象鎖的操作是以每調(diào)用作為粒度的)。
重入鎖實(shí)現(xiàn)可重入性原理或機(jī)制是:每一個(gè)鎖關(guān)聯(lián)一個(gè)線程持有者和計(jì)數(shù)器,當(dāng)計(jì)數(shù)器為 0 時(shí)表示該鎖沒有被任何線程持有,那么任何線程都可能獲得該鎖而調(diào)用相應(yīng)的方法;當(dāng)某一線程請(qǐng)求成功后,JVM會(huì)記下鎖的持有線程,并且將計(jì)數(shù)器置為 1;此時(shí)其它線程請(qǐng)求該鎖,則必須等待;而該持有鎖的線程如果再次請(qǐng)求這個(gè)鎖,就可以再次拿到這個(gè)鎖,同時(shí)計(jì)數(shù)器會(huì)遞增;當(dāng)線程退出同步代碼塊時(shí),計(jì)數(shù)器會(huì)遞減,如果計(jì)數(shù)器為 0,則釋放該鎖。
36、JVM 對(duì) Java 的原生鎖做了哪些優(yōu)化?
在 Java 6 之前,Monitor 的實(shí)現(xiàn)完全依賴底層操作系統(tǒng)的互斥鎖來(lái) 實(shí)現(xiàn),也就是我們剛才在問題二中所闡述的獲取/釋放鎖的邏輯。
由于 Java 層面的線程與操作系統(tǒng)的原生線程有映射關(guān)系,如果要將一 個(gè)線程進(jìn)行阻塞或喚起都需要操作系統(tǒng)的協(xié)助,這就需要從用戶態(tài)切換 到內(nèi)核態(tài)來(lái)執(zhí)行,這種切換代價(jià)十分昂貴,很耗處理器時(shí)間,現(xiàn)代 JDK中做了大量的優(yōu)化。一種優(yōu)化是使用自旋鎖,即在把線程進(jìn)行阻塞操作之前先讓線程自旋等待一段時(shí)間,可能在等待期間其他線程已經(jīng)解鎖,這時(shí)就無(wú)需再讓線程 執(zhí)行阻塞操作,避免了用戶態(tài)到內(nèi)核態(tài)的切換。
現(xiàn)代 JDK 中還提供了三種不同的 Monitor 實(shí)現(xiàn),也就是三種不同的鎖:
- 偏向鎖(Biased Locking)
- 輕量級(jí)鎖
- 重量級(jí)鎖
這三種鎖使得 JDK 得以優(yōu)化 Synchronized 的運(yùn)行,當(dāng) JVM 檢測(cè) 到不同的競(jìng)爭(zhēng)狀況時(shí),會(huì)自動(dòng)切換到適合的鎖實(shí)現(xiàn),這就是鎖的升級(jí)、 降級(jí)。
- 當(dāng)沒有競(jìng)爭(zhēng)出現(xiàn)時(shí),默認(rèn)會(huì)使用偏向鎖。JVM 會(huì)利用 CAS 操作,在對(duì)象頭上的 Mark Word 部分設(shè)置線程ID,以表示這個(gè)對(duì)象偏向于當(dāng)前線程,所以并不涉及真正的互斥鎖,因 為在很多應(yīng)用場(chǎng)景中,大部分對(duì)象生命周期中最多會(huì)被一個(gè)線程鎖定, 使用偏斜鎖可以降低無(wú)競(jìng)爭(zhēng)開銷。
- 如果有另一線程試圖鎖定某個(gè)被偏斜過(guò)的對(duì)象,JVM 就撤銷偏斜鎖, 切換到輕量級(jí)鎖實(shí)現(xiàn)。
- 輕量級(jí)鎖依賴 CAS 操作 Mark Word 來(lái)試圖獲取鎖,如果重試成功, 就使用普通的輕量級(jí)鎖;否則,進(jìn)一步升級(jí)為重量級(jí)鎖。
37、為什么說(shuō) Synchronized 是非公平鎖?
答:非公平主要表現(xiàn)在獲取鎖的行為上,并非是按照申請(qǐng)鎖的時(shí)間前后給等 待線程分配鎖的,每當(dāng)鎖被釋放后,任何一個(gè)線程都有機(jī)會(huì)競(jìng)爭(zhēng)到鎖, 這樣做的目的是為了提高執(zhí)行性能,缺點(diǎn)是可能會(huì)產(chǎn)生線程饑餓現(xiàn)象。
38、為什么說(shuō) Synchronized 是一個(gè)悲觀鎖?樂觀鎖的實(shí)現(xiàn)原理 又是什么?什么是 CAS,它有什么特性?
答:Synchronized 顯然是一個(gè)悲觀鎖,因?yàn)樗牟l(fā)策略是悲觀的:不管是否會(huì)產(chǎn)生競(jìng)爭(zhēng),任何的數(shù)據(jù)操作都必須要加鎖、用戶態(tài)核心態(tài)轉(zhuǎn) 換、維護(hù)鎖計(jì)數(shù)器和檢查是否有被阻塞的線程需要被喚醒等操作。
隨著硬件指令集的發(fā)展,我們可以使用基于沖突檢測(cè)的樂觀并發(fā)策略。先進(jìn)行操作,如果沒有其他線程征用數(shù)據(jù),那操作就成功了; 如果共享數(shù)據(jù)有征用,產(chǎn)生了沖突,那就再進(jìn)行其他的補(bǔ)償措施。這種 樂觀的并發(fā)策略的許多實(shí)現(xiàn)不需要線程掛起,所以被稱為非阻塞同步。
樂觀鎖的核心算法是 CAS(Compareand Swap,比較并交換),它涉 及到三個(gè)操作數(shù):內(nèi)存值、預(yù)期值、新值。當(dāng)且僅當(dāng)預(yù)期值和內(nèi)存值相 等時(shí)才將內(nèi)存值修改為新值。這樣處理的邏輯是,首先檢查某塊內(nèi)存的值是否跟之前我讀取時(shí)的一 樣,如不一樣則表示期間此內(nèi)存值已經(jīng)被別的線程更改過(guò),舍棄本次操 作,否則說(shuō)明期間沒有其他線程對(duì)此內(nèi)存值操作,可以把新值設(shè)置給此 塊內(nèi)存。
CAS 具有原子性,它的原子性由CPU 硬件指令實(shí)現(xiàn)保證,即使用JNI 調(diào)用 Native 方法調(diào)用由 C++ 編寫的硬件級(jí)別指令,JDK 中提 供了 Unsafe 類執(zhí)行這些操作。
39、樂觀鎖一定就是好的嗎?
答:樂觀鎖避免了悲觀鎖獨(dú)占對(duì)象的現(xiàn)象,同時(shí)也提高了并發(fā)性能,但它也 有缺點(diǎn):
- 樂觀鎖只能保證一個(gè)共享變量的原子操作。如果多一個(gè)或幾個(gè)變量,樂 觀鎖將變得力不從心,但互斥鎖能輕易解決,不管對(duì)象數(shù)量多少及對(duì)象 顆粒度大小。
- 長(zhǎng)時(shí)間自旋可能導(dǎo)致開銷大。假如 CAS 長(zhǎng)時(shí)間不成功而一直自旋,會(huì) 給 CPU 帶來(lái)很大的開銷。
- ABA 問題。CAS 的核心思想是通過(guò)比對(duì)內(nèi)存值與預(yù)期值是否一樣而判 斷內(nèi)存值是否被改過(guò),但這個(gè)判斷邏輯不嚴(yán)謹(jǐn),假如內(nèi)存值原來(lái)是 A, 后來(lái)被一條線程改為 B,最后又被改成了 A,則 CAS 認(rèn)為此內(nèi)存值并 沒有發(fā)生改變,但實(shí)際上是有被其他線程改過(guò)的,這種情況對(duì)依賴過(guò)程 值的情景的運(yùn)算結(jié)果影響很大。解決的思路是引入版本號(hào),每次變量更新都把版本號(hào)加一。
40、談一談AQS框架。
AQS(AbstractQueuedSynchronizer 類)是一個(gè)用來(lái)構(gòu)建鎖和同步器 的框架,各種Lock 包中的鎖(常用的有 ReentrantLock、 ReadWriteLock) , 以 及 其 他 如 Semaphore、 CountDownLatch, 甚 至是早期的 FutureTask 等,都是基于 AQS 來(lái)構(gòu)建。
- AQS 在內(nèi)部定義了一個(gè) volatile int state 變量,表示同步狀態(tài):當(dāng)線 程調(diào)用 lock 方法時(shí) ,如果 state=0,說(shuō)明沒有任何線程占有共享資源 的鎖,可以獲得鎖并將 state=1;如果 state=1,則說(shuō)明有線程目前正在 使用共享變量,其他線程必須加入同步隊(duì)列進(jìn)行等待。
- AQS 通過(guò) Node 內(nèi)部類構(gòu)成的一個(gè)雙向鏈表結(jié)構(gòu)的同步隊(duì)列,來(lái)完成線 程獲取鎖的排隊(duì)工作,當(dāng)有線程獲取鎖失敗后,就被添加到隊(duì)列末尾。Node 類是對(duì)要訪問同步代碼的線程的封裝,包含了線程本身及其狀態(tài)叫waitStatus(有五種不同 取值,分別表示是否被阻塞,是否等待喚醒, 是否已經(jīng)被取消等),每個(gè) Node 結(jié)點(diǎn)關(guān)聯(lián)其 prev 結(jié)點(diǎn)和 next 結(jié) 點(diǎn),方便線程釋放鎖后快速喚醒下一個(gè)在等待的線程,是一個(gè) FIFO 的過(guò) 程。Node 類有兩個(gè)常量,SHARED 和 EXCLUSIVE,分別代表共享模式和獨(dú) 占模式。所謂共享模式是一個(gè)鎖允許多條線程同時(shí)操作(信號(hào)量Semaphore 就是基于 AQS 的共享模式實(shí)現(xiàn)的),獨(dú)占模式是同一個(gè)時(shí) 間段只能有一個(gè)線程對(duì)共享資源進(jìn)行操作,多余的請(qǐng)求線程需要排隊(duì)等待 ( 如 ReentranLock) 。
- AQS 通過(guò)內(nèi)部類 ConditionObject 構(gòu)建等待隊(duì)列(可有多個(gè)),當(dāng)Condition 調(diào)用 wait() 方法后,線程將會(huì)加入等待隊(duì)列中,而當(dāng)Condition 調(diào)用 signal() 方法后,線程將從等待隊(duì)列轉(zhuǎn)移動(dòng)同步隊(duì)列中進(jìn)行鎖競(jìng)爭(zhēng)。
- AQS 和 Condition 各自維護(hù)了不同的隊(duì)列,在使用 Lock 和Condition 的時(shí)候,其實(shí)就是兩個(gè)隊(duì)列的互相移動(dòng)。
41、ReentrantLock 是如何實(shí)現(xiàn)可重入性的?
答:ReentrantLock 內(nèi)部自定義了同步器 Sync(Sync 既實(shí)現(xiàn)了 AQS, 又實(shí)現(xiàn)了 AOS,而 AOS 提供了一種互斥鎖持有的方式),其實(shí)就是 加鎖的時(shí)候通過(guò) CAS 算法,將線程對(duì)象放到一個(gè)雙向鏈表中,每次獲 取鎖的時(shí)候,看下當(dāng)前維護(hù)的那個(gè)線程 ID 和當(dāng)前請(qǐng)求的線程 ID 是否 一樣,一樣就可重入了。
42、Java中Semaphore是什么?
Java中的Semaphore是一種新的同步類,它是一個(gè)計(jì)數(shù)信號(hào)。從概念上講,從概念上講,信號(hào)量維護(hù)了一個(gè)許可集合。如有必要,在許可可用前會(huì)阻塞每一個(gè) acquire(),然后再獲取該許可。每個(gè) release()添加一個(gè)許可,從而可能釋放一個(gè)正在阻塞的獲取者。但是,不使用實(shí)際的許可對(duì)象,Semaphore只對(duì)可用許可的號(hào)碼進(jìn)行計(jì)數(shù),并采取相應(yīng)的行動(dòng)。信號(hào)量常常用于多線程的代碼中,比如數(shù)據(jù)庫(kù)連接池。
package com.mzc.common.concurrent;
?
import java.util.concurrent.Semaphore;
?
/**
* <p class="detail">
* 功能: Semaphore Test
* </p>
*
* @author Moore
* @ClassName Test semaphore.
* @Version V1.0.
* @date 2020.02.07 20:11:00
*/
public class TestSemaphore {
?
static class Worker extends Thread{
private int num;
private Semaphore semaphore;
public Worker(int num,Semaphore semaphore){
this.num = num;
this.semaphore = semaphore;
}
@Override
public void run() {
try {
// 搶許可
semaphore.acquire();
Thread.sleep(2000);
// 釋放許可
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
?
public static void main(String[] args) {
// 機(jī)器數(shù)目,即5個(gè)許可
Semaphore semaphore = new Semaphore(5);
// 8個(gè)線程去搶許可
for (int i = 0; i < 8; i++){
new Worker(i,semaphore).start();
}
}
}
43、Java 中的線程池是如何實(shí)現(xiàn)的?
- 在 Java 中,所謂的線程池中的“線程”,其實(shí)是被抽象為了一個(gè)靜態(tài) 內(nèi)部類 Worker,它基于 AQS 實(shí)現(xiàn),存放在線程池的HashSet<Worker> workers 成員變量中;
- 而需要執(zhí)行的任務(wù)則存放在成員變量 workQueue(BlockingQueue<Runnable> workQueue)中。這樣,整個(gè)線程池實(shí)現(xiàn)的基本思想就是:從 workQueue 中不斷取出 需要執(zhí)行的任務(wù),放在 Workers 中進(jìn)行處理。
44、線程池中的線程是怎么創(chuàng)建的?是一開始就隨著線程池的啟動(dòng)創(chuàng)建好的嗎?
答:顯然不是的。線程池默認(rèn)初始化后不啟動(dòng) Worker,等待有請(qǐng)求時(shí)才啟動(dòng)。每當(dāng)我們調(diào)用 execute() 方法添加一個(gè)任務(wù)時(shí),線程池會(huì)做如下判 斷:
- 如果正在運(yùn)行的線程數(shù)量小于 corePoolSize,那么馬上創(chuàng)建線程運(yùn)行這個(gè)任務(wù);
- 如果正在運(yùn)行的線程數(shù)量大于或等于 corePoolSize,那么將這個(gè)任務(wù)放入隊(duì)列;
- 如果這時(shí)候隊(duì)列滿了,而且正在運(yùn)行的線程數(shù)量小于maximumPoolSize,那么還是要?jiǎng)?chuàng)建非核心線程立刻運(yùn)行這個(gè)任務(wù);
- 如果隊(duì)列滿了,而且正在運(yùn)行的線程數(shù)量大于或等于maximumPoolSize,那么線程池會(huì)拋出異常RejectExecutionException。
當(dāng)一個(gè)線程完成任務(wù)時(shí),它會(huì)從隊(duì)列中取下一個(gè)任務(wù)來(lái)執(zhí)行。當(dāng)一個(gè)線程無(wú)事可做,超過(guò)一定的時(shí)間(keepAliveTime)時(shí),線程池會(huì)判斷。
如果當(dāng)前運(yùn)行的線程數(shù)大于 corePoolSize,那么這個(gè)線程就被停掉。所以線程池的所有任務(wù)完成后,它最終會(huì)收縮到 corePoolSize 的大小。
45、什么是競(jìng)爭(zhēng)條件?如何發(fā)現(xiàn)和解決競(jìng)爭(zhēng)?
兩個(gè)線程同步操作同一個(gè)對(duì)象,使這個(gè)對(duì)象的最終狀態(tài)不明——叫做競(jìng)爭(zhēng)條件。競(jìng)爭(zhēng)條件可以在任何應(yīng)該由程序員保證原子操作的,而又忘記使用synchronized的地方。
唯一的解決方案就是加鎖。
Java有兩種鎖可供選擇:
- 對(duì)象或者類(class)的鎖。每一個(gè)對(duì)象或者類都有一個(gè)鎖。使用synchronized關(guān)鍵字獲取。 synchronized加到static方法上面就使用類鎖,加到普通方法上面就用對(duì)象鎖。除此之外synchronized還可以用于鎖定關(guān)鍵區(qū)域塊(Critical Section)。 synchronized之后要制定一個(gè)對(duì)象(鎖的攜帶者),并把關(guān)鍵區(qū)域用大括號(hào)包裹起來(lái)。synchronized(this){// critical code}。
- 顯示構(gòu)建的鎖(java.util.concurrent.locks.Lock),調(diào)用lock的lock方法鎖定關(guān)鍵代碼。
46、很多人都說(shuō)要慎用 ThreadLocal,談?wù)勀愕睦斫?,使用ThreadLocal 需要注意些什么?
答:使 用 ThreadLocal 要 注 意 remove!
ThreadLocal 的實(shí)現(xiàn)是基于一個(gè)所謂的 ThreadLocalMap,在ThreadLocalMap 中,它的 key 是一個(gè)弱引用。通常弱引用都會(huì)和引用隊(duì)列配合清理機(jī)制使用,但是 ThreadLocal 是 個(gè)例外,它并沒有這么做。這意味著,廢棄項(xiàng)目的回收依賴于顯式地觸發(fā),否則就要等待線程結(jié) 束,進(jìn)而回收相應(yīng) ThreadLocalMap!這就是很多 OOM 的來(lái)源,所 以通常都會(huì)建議,應(yīng)用一定要自己負(fù)責(zé) remove,并且不要和線程池配 合,因?yàn)?worker 線程往往是不會(huì)退出的。
參考資料:https://www.cnblogs.com/jxldjsn/p/10872154.html
參考資料:https://www.cnblogs.com/sgh1023/p/10297322.html
參考資料:https://blog.csdn.net/u011780616/article/details/95339236
更多面試資料請(qǐng)關(guān)注我的公眾號(hào)“碼之初”或者“ma_zhichu”,希望所有鄉(xiāng)親們面試無(wú)憂,前程似錦。