一、現(xiàn)在有 T1、T2、T3 三個(gè)線程,你怎樣保證 T2 在 T1 執(zhí)行完后執(zhí)行,T3 在 T2 執(zhí)行完后執(zhí)行?
這個(gè)線程問題通常會(huì)在第一輪或電話面試階段被問到,目的是檢測(cè)你對(duì)”join”方法是否熟悉。這個(gè)多線程問題比較簡單,可以用 join 方法實(shí)現(xiàn)。
線程按順序執(zhí)行-方式一(這種方式更好,不會(huì)阻塞主線程)
/**
* @author Alan Chen
* @description 線程按順序執(zhí)行-方式一
* @date 2021/2/10
*/
public class ThreadOrder {
public static void main(String[] args) {
System.out.println("子線程調(diào)用前代碼邏輯......");
Thread t1 = new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
Thread.sleep(40);
System.out.println("t1 run:");
}
});
Thread t2 = new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
Thread.sleep(40);
t1.join();//表明當(dāng)前線程需要在t1線程上等待
System.out.println("t2 run:");
}
});
Thread t3 = new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
Thread.sleep(40);
t2.join();//表明當(dāng)前線程需要在t2線程上等待
System.out.println("t3 run:");
}
});
t1.start();
t2.start();
t3.start();
System.out.println("子線程調(diào)用后代碼邏輯......");
}
}
執(zhí)行結(jié)果
子線程調(diào)用前代碼邏輯......
子線程調(diào)用后代碼邏輯......
t1 run:
t2 run:
t3 run:
線程按順序執(zhí)行-方式二
/**
* @author Alan Chen
* @description 線程按順序執(zhí)行-方式一
* @date 2020-01-13DateTool
*/
public class TestClient {
public static void main(String[] args)throws InterruptedException{
System.out.println("子線程調(diào)用前代碼邏輯......");
Thread t1 = new Thread(new ThreadTest("t1"));
Thread t2 = new Thread(new ThreadTest("t2"));
Thread t3 = new Thread(new ThreadTest("t3"));
t1.start();
t1.join();
t2.start();
t2.join();
t3.start();
System.out.println("子線程調(diào)用后代碼邏輯......");
}
}
class ThreadTest extends Thread {
private String threadName;
public ThreadTest(String name) {
threadName = name;
}
@SneakyThrows
@Override
public void run() {
Thread.sleep(40);
System.out.println(threadName+" run:");
}
}
執(zhí)行結(jié)果
子線程調(diào)用前代碼邏輯......
t1 run:
t2 run:
子線程調(diào)用后代碼邏輯......
t3 run:
join方法是synchronized,所以需要獲取Thread的對(duì)象鎖才能進(jìn)入,只有獲得了鎖才能調(diào)用wait放棄對(duì)鎖的獨(dú)占并等待再次獲取鎖。
join方法用線程對(duì)象調(diào)用,如果在一個(gè)線程A中調(diào)用另一個(gè)線程B的join方法,線程A將會(huì)等待線程B執(zhí)行完畢后再執(zhí)行。
join 方法是一個(gè)阻塞方法,用來進(jìn)行線程之間的交流。線程 A 調(diào)用 線程 B 的 join 方法,則線程 A 將阻塞,線程 B 執(zhí)行結(jié)束后 線程 A 開始執(zhí)行。
問:join方法的作用?
答:Thread類中的join方法的主要作用就是同步,它可以使得線程之間的并行執(zhí)行變?yōu)榇袌?zhí)行。當(dāng)我們調(diào)用某個(gè)線程的這個(gè)方法時(shí),這個(gè)方法會(huì)掛起調(diào)用線程,直到被調(diào)用線程結(jié)束執(zhí)行,調(diào)用線程才會(huì)繼續(xù)執(zhí)行。
問:join方法傳參和不傳參的區(qū)別?
答:join方法中如果傳入?yún)?shù),則表示這樣的意思:如果A線程中調(diào)用B線程的join(10),則表示A線程會(huì)等待B線程執(zhí)行10毫秒,10毫秒過后,A、B線程并行執(zhí)行。需要注意的是,jdk規(guī)定,join(0)的意思不是A線程等待B線程0秒,而是A線程等待B線程無限時(shí)間,直到B線程執(zhí)行完畢,即join(0)等價(jià)于join()。
問:join與start調(diào)用順序問題
答:join方法必須在線程start方法調(diào)用之后調(diào)用才有意義。這個(gè)也很容易理解:如果一個(gè)線程都沒有start,那它也就無法同步了。因?yàn)閳?zhí)行完start方法才會(huì)創(chuàng)建線程。
問:join方法實(shí)現(xiàn)原理
答:join方法是通過調(diào)用線程的wait方法來達(dá)到同步的目的的。例如A線程中調(diào)用了B線程的join方法,則相當(dāng)于在A線程中調(diào)用了B線程的wait方法,當(dāng)B線程執(zhí)行完(或者到達(dá)等待時(shí)間),B線程會(huì)自動(dòng)調(diào)用自身的notifyAll方法喚醒A線程,從而達(dá)到同步的目的。
二、在 Java 中 Lock 接口比 synchronized 塊的優(yōu)勢(shì)是什么?你需要實(shí)現(xiàn)一個(gè)高效的緩存,它允許多個(gè)用戶讀,但只允許一個(gè)用戶寫,以此來保持它的完整性,你會(huì)怎樣去實(shí)現(xiàn)它?
lock 接口在多線程和并發(fā)編程中最大的優(yōu)勢(shì)是它們?yōu)樽x和寫分別提供了鎖,它能滿足你寫像ConcurrentHashMap 這樣的高性能數(shù)據(jù)結(jié)構(gòu)和有條件的阻塞。Java 線程面試的問題越來越會(huì)根據(jù)面試者的回答來提問。我強(qiáng)烈建議在你去參加多線程的面試之前認(rèn)真讀一下 Locks,因?yàn)楫?dāng)前其大量用于構(gòu)建電子交易終統(tǒng)的客戶端緩存和交易連接空間。
三、在 Java 中 wait 和 sleep 方法的不同?
通常會(huì)在電話面試中經(jīng)常被問到的 Java 線程面試問題。最大的不同是在等待時(shí) wait 會(huì)釋放鎖,而 sleep 一直持有鎖。Wait 通常被用于線程間交互,sleep 通常被用于暫停執(zhí)行。
四、用 Java 實(shí)現(xiàn)阻塞隊(duì)列。
這是一個(gè)相對(duì)艱難的多線程面試問題,它能達(dá)到很多的目的。第一,它可以檢測(cè)侯選者是否能實(shí)際的用 Java 線程寫程序;第二,可以檢測(cè)侯選者對(duì)并發(fā)場(chǎng)景的理解,并且你可以根據(jù)這個(gè)問很多問題。如果他用wait()和 notify()方法來實(shí)現(xiàn)阻塞隊(duì)列,你可以要求他用最新的Java5 中的并發(fā)類來再寫一次。
五、用 Java 寫代碼來解決生產(chǎn)者——消費(fèi)者問題
與上面的問題很類似,但這個(gè)問題更經(jīng)典,有些時(shí)候面試都會(huì)問下面的問題。在 Java 中怎么解決生產(chǎn)者——消費(fèi)者問題,當(dāng)然有很多解決方法,我已經(jīng)分享了一種用阻塞隊(duì)列實(shí)現(xiàn)的方法。有些時(shí)候他們甚至?xí)栐趺磳?shí)現(xiàn)哲學(xué)家進(jìn)餐問題。
六、什么是原子操作,Java 中的原子操作是什么?
6.1 原子操作
原子操作是無法被別的線程打斷的操作。要么不執(zhí)行,要么就執(zhí)行成功。
例如:x=3是原子操作。過程就是先把工作內(nèi)存的x賦成3,再把主存的x賦成3。y=x不是原子操作,它涉及在工作內(nèi)存先把x值讀出來,再把這個(gè)值賦給y。x++或x=x+1也不是原子操作,它涉及取值,自加和賦值。
6.2 Java 中的原子操作
在Java中,我們可以通過同步鎖或者CAS操作來實(shí)現(xiàn)原子操作。
CAS是Compare and swap的簡稱,這個(gè)操作是硬件級(jí)別的操作,在硬件層面保證了操作的原子性。CAS有3個(gè)操作數(shù),內(nèi)存值V,舊的預(yù)期值A(chǔ),要修改的新值B。當(dāng)且僅當(dāng)預(yù)期值A(chǔ)和內(nèi)存值V相同時(shí),將內(nèi)存值V修改為B,否則什么都不做。Java中的sun.misc.Unsafe類提供了compareAndSwapInt和compareAndSwapLong等幾個(gè)方法實(shí)現(xiàn)CAS。
七、Java 中的 volatile 關(guān)鍵是什么作用?怎樣使用它?在 Java 中它跟 synchronized 方法有什么不同?
自從 Java 5 和 Java 內(nèi)存模型改變以后,基于 volatile 關(guān)鍵字的線程問題越來越流行。應(yīng)該準(zhǔn)備好回答關(guān)于 volatile 變量怎樣在并發(fā)環(huán)境中確保可見性。
八、多線程有什么用?
8.1 發(fā)揮多核 CPU 的優(yōu)勢(shì)
隨著工業(yè)的進(jìn)步,現(xiàn)在的筆記本、臺(tái)式機(jī)乃至商用的應(yīng)用服務(wù)器至少也都是雙核的,4 核、8 核甚至 16 核的也都不少見,如果是單線程的程序,那么在雙核 CPU 上就浪費(fèi)了 50%,在 4 核 CPU 上就浪費(fèi)了 75%。單核 CPU 上所謂的"多線程"那是假的多線程,同一時(shí)間處理器只會(huì)處理一段邏輯,只不過線程之間切換得比較快,看著像多個(gè)線程"同時(shí)"運(yùn)行罷了。多核 CPU 上的多線程才是真正的多線程,它能讓你的多段邏輯同時(shí)工作,多線程,可以真正發(fā)揮出多核 CPU 的優(yōu)勢(shì)來,達(dá)到充分利用 CPU 的目的。
8.2 防止阻塞
從程序運(yùn)行效率的角度來看,單核 CPU 不但不會(huì)發(fā)揮出多線程的優(yōu)勢(shì),反而會(huì)因?yàn)樵趩魏?CPU 上運(yùn)行多線程導(dǎo)致線程上下文的切換,而降低程序整體的效率。但是單核 CPU 我們還是要應(yīng)用多線程,就是為了防止阻塞。試想,如果單核 CPU 使用單線程,那么只要這個(gè)線程阻塞了,比方說遠(yuǎn)程讀取某個(gè)數(shù)據(jù)吧,對(duì)端遲遲未返回又沒有設(shè)置超時(shí)時(shí)間,那么你的整個(gè)程序在數(shù)據(jù)返回回來之前就停止運(yùn)行了。多線程可以防止這個(gè)問題,多條線程同時(shí)運(yùn)行,哪怕一條線程的代碼執(zhí)行讀取數(shù)據(jù)阻塞,也不會(huì)影響其它任務(wù)的執(zhí)行。
8.3 便于建模
這是另外一個(gè)沒有這么明顯的優(yōu)點(diǎn)了。假設(shè)有一個(gè)大的任務(wù) A,單線程編程,那么就要考慮很多,建立整個(gè)程序模型比較麻煩。但是如果把這個(gè)大的任務(wù) A分解成幾個(gè)小任務(wù),任務(wù) B、任務(wù) C、任務(wù) D,分別建立程序模型,并通過多線程分別運(yùn)行這幾個(gè)任務(wù),那就簡單很多了。
九、創(chuàng)建線程的方式
9.1 繼承Thread類
看jdk源碼可以發(fā)現(xiàn),Thread類其實(shí)是實(shí)現(xiàn)了Runnable接口的一個(gè)實(shí)例,繼承Thread類后需要重寫run方法并通過start方法啟動(dòng)線程。繼承Thread類耦合性太強(qiáng)了,因?yàn)閖ava只能單繼承,所以不利于擴(kuò)展。
9.2 實(shí)現(xiàn)Runnable接口
通過實(shí)現(xiàn)Runnable接口并重寫run方法,并把Runnable實(shí)例傳給Thread對(duì)象,Thread的start方法調(diào)用run方法再通過調(diào)用Runnable實(shí)例的run方法啟動(dòng)線程。所以如果一個(gè)類繼承了另外一個(gè)父類,此時(shí)要實(shí)現(xiàn)多線程就不能通過繼承Thread的類實(shí)現(xiàn)。
9.3 實(shí)現(xiàn)Callable接口
通過實(shí)現(xiàn)Callable接口并重寫call方法,并把Callable實(shí)例傳給FutureTask對(duì)象,再把FutureTask對(duì)象傳給Thread對(duì)象。它與Thread、Runnable最大的不同是Callable能返回一個(gè)異步處理的結(jié)果Future對(duì)象并能拋出異常,而其他兩種不能。
十、start()方法和 run()方法的區(qū)別
只有調(diào)用了 start()方法,才會(huì)表現(xiàn)出多線程的特性,不同線程的 run()方法里面的代碼交替執(zhí)行。如果只是調(diào)用 run()方法,那么代碼還是同步執(zhí)行的,必須等待一個(gè)線程的 run()方法里面的代碼全部執(zhí)行完畢之后,另外一個(gè)線程才可以執(zhí)行其 run()方法里面的代碼
十一、Runnable 接口和 Callable 接口的區(qū)別
有點(diǎn)深的問題了,也看出一個(gè) Java 程序員學(xué)習(xí)知識(shí)的廣度。Runnable 接口中的 run()方法的返回值是 void,它做的事情只是純粹地去執(zhí)行 run()方法中的代碼而已;Callable 接口中的 call()方法是有返回值的,是一個(gè)泛型,和 Future、FutureTask 配合可以用來獲取異步執(zhí)行的結(jié)果。
這其實(shí)是很有用的一個(gè)特性,因?yàn)槎嗑€程相比單線程更難、更復(fù)雜的一個(gè)重要原因就是因?yàn)槎嗑€程充滿著未知性,某條線程是否執(zhí)行了?某條線程執(zhí)行了多久?某條線程執(zhí)行的時(shí)候我們期望的數(shù)據(jù)是否已經(jīng)賦值完畢?無法得知,我們能 做 的 只 是 等 待 這 條 多 線 程 的 任 務(wù) 執(zhí) 行 完 畢 而 已 。 而Callable+Future/FutureTask 卻可以獲取多線程運(yùn)行的結(jié)果,可以在等待時(shí)間太長沒獲取到需要的數(shù)據(jù)的情況下取消該線程的任務(wù),真的是非常有用。
十二、CyclicBarrier 和 CountDownLatch 的區(qū)別
兩個(gè)看上去有點(diǎn)像的類,都在 java.util.concurrent 下,都可以用來表示代碼運(yùn)行到某個(gè)點(diǎn)上,二者的區(qū)別在于:
CyclicBarrier 的某個(gè)線程運(yùn)行到某個(gè)點(diǎn)上之后,該線程即停止運(yùn)行,直到所有的線程都到達(dá)了這個(gè)點(diǎn),所有線程才重新運(yùn)行;CountDownLatch 則不是,某線程運(yùn)行到某個(gè)點(diǎn)上之后,只是給某個(gè)數(shù)值-1 而已,該線程繼續(xù)運(yùn)行。
CyclicBarrier 只能喚起一個(gè)任務(wù),CountDownLatch 可以喚起多個(gè)任務(wù)。
CyclicBarrier 可重用,CountDownLatch 不可重用,計(jì)數(shù)值為 0 該CountDownLatch 就不可再用了。
十三、volatile 關(guān)鍵字的作用
一個(gè)非常重要的問題,是每個(gè)學(xué)習(xí)、應(yīng)用多線程的 Java 程序員都必須掌握的。volatile 關(guān)鍵字的作用主要有兩個(gè):
1、多線程主要圍繞可見性和原子性兩個(gè)特性而展開,使用 volatile 關(guān)鍵字修飾的變量,保證了其在多線程之間的可見性,即每次讀取到 volatile 變量,一定是最新的數(shù)據(jù)。
2、代碼底層執(zhí)行不像我們看到的高級(jí)語言----Java 程序這么簡單,它的執(zhí)行是 Java 代 碼 --> 字節(jié)碼 --> 根據(jù)字節(jié)碼執(zhí)行對(duì)應(yīng)的 C/C++ 代 碼-->C/C++代碼被編譯成匯編語言-->和硬件電路交互,現(xiàn)實(shí)中,為了獲取更好的性能 JVM 可能會(huì)對(duì)指令進(jìn)行重排序,多線程下可能會(huì)出現(xiàn)一些意想不到的問題。使用 volatile 則會(huì)對(duì)禁止語義重排序,當(dāng)然這也一定程度上降低了代碼執(zhí)行效率。
從實(shí)踐角度而言,volatile 的一個(gè)重要作用就是和 CAS 結(jié)合,保證了原子性。
十四、什么是線程安全
如果你的代碼在多線程下執(zhí)行和在單線程下執(zhí)行永遠(yuǎn)都能獲得一樣的結(jié)果,那么你的代碼就是線程安全的。這個(gè)問題有值得一提的地方,就是線程安全也是有幾個(gè)級(jí)別的:
14.1 不可變
像 String、Integer、Long 這些,都是 final 類型的類,任何一個(gè)線程都改變不了它們的值,要改變除非新創(chuàng)建一個(gè),因此這些不可變對(duì)象不需要任何同步手段就可以直接在多線程環(huán)境下使用。
14.2 絕對(duì)線程安全
不管運(yùn)行時(shí)環(huán)境如何,調(diào)用者都不需要額外的同步措施。要做到這一點(diǎn)通常需要付出許多額外的代價(jià),Java 中標(biāo)注自己是線程安全的類,實(shí)際上絕大多數(shù)都 不 是 線 程 安 全 的 , 不 過 絕 對(duì) 線 程 安 全 的 類 , Java 中 也 有 , 比 方 說CopyOnWriteArrayList、CopyOnWriteArraySet
14.3 相對(duì)線程安全
相對(duì)線程安全也就是我們通常意義上所說的線程安全,像 Vector 這種,add、remove 方法都是原子操作,不會(huì)被打斷,但也僅限于此,如果有個(gè)線程在遍歷某個(gè) Vector、有個(gè)線程同時(shí)在 add 這個(gè) Vector,99%的情況下都會(huì)出現(xiàn)ConcurrentModificationException,也就是 fail-fast 機(jī)制。
14.4 線程非安全
這個(gè)就沒什么好說的了,ArrayList、LinkedList、HashMap 等都是線程非安全的類
十五、Java 中如何獲取到線程 dump 文件
死循環(huán)、死鎖、阻塞、頁面打開慢等問題,打線程 dump 是最好的解決問題的途徑。所謂線程 dump 也就是線程堆棧,獲取到線程堆棧有兩步:
1、獲取到線程的 pid,可以通過使用 jps 命令,在 Linux 環(huán)境下還可以使用ps -ef | grep java
2、打印線程堆棧,可以通過使用 jstack pid 命令,在 Linux 環(huán)境下還可以使用 kill -3 pid
另外提一點(diǎn),Thread 類提供了一個(gè) getStackTrace()方法也可以用于獲取線程堆棧。這是一個(gè)實(shí)例方法,因此此方法是和具體線程實(shí)例綁定的,每次獲取獲取到的是具體某個(gè)線程當(dāng)前運(yùn)行的堆棧。
十六、如何在兩個(gè)線程之間共享數(shù)據(jù)
通 過 在 線 程 之 間 共 享 對(duì) 象 就 可 以 了 , 然 后 通 過 wait/notify/notifyAll 、await/signal/signalAll 進(jìn)行喚起和等待,比方說阻塞隊(duì)列 BlockingQueue就是為線程之間共享數(shù)據(jù)而設(shè)計(jì)的。
十七、sleep 方法和 wait 方法有什么區(qū)別
這個(gè)問題常問,sleep 方法和 wait 方法都可以用來放棄 CPU 一定的時(shí)間,不同點(diǎn)在于如果線程持有某個(gè)對(duì)象的監(jiān)視器,sleep 方法不會(huì)放棄這個(gè)對(duì)象的監(jiān)視器,wait 方法會(huì)放棄這個(gè)對(duì)象的監(jiān)視器。
十八、生產(chǎn)者消費(fèi)者模型的作用是什么
1)通過平衡生產(chǎn)者的生產(chǎn)能力和消費(fèi)者的消費(fèi)能力來提升整個(gè)系統(tǒng)的運(yùn)行效率,這是生產(chǎn)者消費(fèi)者模型最重要的作用。
2)解耦,這是生產(chǎn)者消費(fèi)者模型附帶的作用,解耦意味著生產(chǎn)者和消費(fèi)者之間的聯(lián)系少,聯(lián)系越少越可以獨(dú)自發(fā)展而不需要收到相互的制約。
十九、ThreadLocal 有什么用
簡單說 ThreadLocal 就是一種以空間換時(shí)間的做法,在每個(gè) Thread 里面維護(hù)了一個(gè)以開地址法實(shí)現(xiàn)的 ThreadLocal.ThreadLocalMap,把數(shù)據(jù)進(jìn)行隔離,數(shù)據(jù)不共享,自然就沒有線程安全方面的問題了。
二十、為什么 wait()方法和 notify()/notifyAll()方法要在同步塊中被調(diào)用
這是 JDK 強(qiáng)制的,wait()方法和 notify()/notifyAll()方法在調(diào)用前都必須先獲得對(duì)象的鎖。
二十一、wait()方法和 notify()/notifyAll()方法在放棄對(duì)象監(jiān)視器時(shí)有什么區(qū)別
wait()方法立即釋放對(duì)象監(jiān)視器,notify()/notifyAll()方法則會(huì)等待線程剩余代碼執(zhí)行完畢才會(huì)放棄對(duì)象監(jiān)視器。
二十二、為什么要使用線程池
避免頻繁地創(chuàng)建和銷毀線程,達(dá)到線程對(duì)象的重用。另外,使用線程池還可以根據(jù)項(xiàng)目靈活地控制并發(fā)的數(shù)目。
二十三、怎么檢測(cè)一個(gè)線程是否持有對(duì)象監(jiān)視器
Thread 類提供了一個(gè) holdsLock(Object obj)方法,當(dāng)且僅當(dāng)對(duì)象 obj 的監(jiān)視器被某條線程持有的時(shí)候才會(huì)返回 true,注意這是一個(gè)static 方法,這意味著"某條線程"指的是當(dāng)前線程。
二十四、synchronized 和 ReentrantLock 的區(qū)別
synchronized 是和 if、else、for、while 一樣的關(guān)鍵字,ReentrantLock是類,這是二者的本質(zhì)區(qū)別。既然 ReentrantLock 是類,那么它就提供了比synchronized 更多更靈活的特性,可以被繼承、可以有方法、可以有各種各樣的類變量,ReentrantLock 比 synchronized 的擴(kuò)展性體現(xiàn)在幾點(diǎn)上:
1、ReentrantLock 可以對(duì)獲取鎖的等待時(shí)間進(jìn)行設(shè)置,這樣就避免了死鎖
2、ReentrantLock 可以獲取各種鎖的信息
3、ReentrantLock 可以靈活地實(shí)現(xiàn)多路通知
另外,二者的鎖機(jī)制其實(shí)也是不一樣的。 ReentrantLock 底層調(diào)用的是Unsafe 的 park 方法加鎖,synchronized 操作的應(yīng)該是對(duì)象頭中 mark word。
二十五、ConcurrentHashMap 的并發(fā)度是什么
ConcurrentHashMap 的并發(fā)度就是 segment 的大小,默認(rèn)為 16,這意味著 最 多 同 時(shí) 可 以 有 16 條線程 操 作 ConcurrentHashMap ,這也是ConcurrentHashMap 對(duì) Hashtable 的最大優(yōu)勢(shì)。
二十六、ReadWriteLock 是什么
如果使用 ReentrantLock,可能本身是為了防止線程 A 在寫數(shù)據(jù)、線程 B 在讀數(shù)據(jù)造成的數(shù)據(jù)不一致,但這樣,如果線程 C 在讀數(shù)據(jù)、線程 D也在讀數(shù)據(jù),讀數(shù)據(jù)是不會(huì)改變數(shù)據(jù)的,沒有必要加鎖,但是還是加鎖了,降低了程序的性能。因?yàn)檫@個(gè),才誕生了讀寫鎖 ReadWriteLock。ReadWriteLock 是一個(gè)讀寫鎖接口,ReentrantReadWriteLock 是 ReadWriteLock 接口的一個(gè)具體實(shí)現(xiàn),實(shí)現(xiàn)了讀寫的分離,讀鎖是共享的,寫鎖是獨(dú)占的,讀和讀之間不會(huì)互斥,讀和寫、寫和讀、寫和寫之間才會(huì)互斥,提升了讀寫的性能。
二十七、FutureTask 是什么
FutureTask 表示一個(gè)異步運(yùn)算的任務(wù)。FutureTask里面可以傳入一個(gè) Callable 的具體實(shí)現(xiàn)類,可以對(duì)這個(gè)異步運(yùn)算的任務(wù)的結(jié)果進(jìn)行等待獲取、判斷是否已經(jīng)完成、取消任務(wù)等操作。當(dāng)然,由于 FutureTask也是 Runnable 接口的實(shí)現(xiàn)類,所以 FutureTask 也可以放入線程池中。
二十八、Linux 環(huán)境下如何查找哪個(gè)線程使用 CPU 最長
1、獲取項(xiàng)目的 pid,jps 或者 ps -ef | grep java
2、top -H -p pid,順序不能改變這樣就可以打印出當(dāng)前的項(xiàng)目,每條線程占用 CPU 時(shí)間的百分比。注意這里
打出的是 LWP,也就是操作系統(tǒng)原生線程的線程號(hào)。
使用"top -H -p pid"+"jps pid"可以很容易地找到某條占用 CPU 高的線程的線程堆棧,從而定位占用 CPU 高的原因,一般是因?yàn)椴划?dāng)?shù)拇a操作導(dǎo)致了死循環(huán)。最后提一點(diǎn),"top -H -p pid"打出來的 LWP 是十進(jìn)制的,"jps pid"打出來的本地線程號(hào)是十六進(jìn)制的,轉(zhuǎn)換一下,就能定位到占用 CPU 高的線程的當(dāng)前線程堆棧了。
二十九、什么是多線程的上下文切換
多線程的上下文切換是指 CPU 控制權(quán)由一個(gè)已經(jīng)正在運(yùn)行的線程切換到另外一個(gè)就緒并等待獲取 CPU 執(zhí)行權(quán)的線程的過程。
三十、如果你提交任務(wù)時(shí),線程池隊(duì)列已滿,這時(shí)會(huì)發(fā)生什么
1、如果使用的是無界隊(duì)列 LinkedBlockingQueue,也就是無界隊(duì)列的話,沒關(guān)系,繼續(xù)添加任務(wù)到阻塞隊(duì)列中等待執(zhí)行,因?yàn)?LinkedBlockingQueue 可以近乎認(rèn)為是一個(gè)無窮大的隊(duì)列,可以無限存放任務(wù)
2、如果使用的是有界隊(duì)列比如 ArrayBlockingQueue,任務(wù)首先會(huì)被添加到ArrayBlockingQueue 中 , ArrayBlockingQueue 滿 了 , 會(huì) 根 據(jù)maximumPoolSize 的值增加線程數(shù)量,如果增加了線程數(shù)量還是處理不過來,ArrayBlockingQueue 繼 續(xù) 滿,那么則會(huì)使用拒絕策略RejectedExecutionHandler 處理滿了的任務(wù),默認(rèn)是 AbortPolicy
三十一、Java 中用到的線程調(diào)度算法是什么
搶占式。一個(gè)線程用完 CPU 之后,操作系統(tǒng)會(huì)根據(jù)線程優(yōu)先級(jí)、線程饑餓情況等數(shù)據(jù)算出一個(gè)總的優(yōu)先級(jí)并分配下一個(gè)時(shí)間片給某個(gè)線程執(zhí)行。
三十二、Thread.sleep(0)的作用是什么
由于Java采用搶占式的線程調(diào)度算法,因此可能會(huì)出現(xiàn)某條線程常常獲取到CPU控制權(quán)的情況,為了讓某些優(yōu)先級(jí)比較低的線程也能獲取到CPU控制權(quán),可以使用Thread.sleep(0)手動(dòng)觸發(fā)一次操作系統(tǒng)分配時(shí)間片的操作,這也是平衡CPU控制權(quán)的一種操作。
三十三、什么是自旋
很多 synchronized 里面的代碼只是一些很簡單的代碼,執(zhí)行時(shí)間非常快,此時(shí)等待的線程都加鎖可能是一種不太值得的操作,因?yàn)榫€程阻塞涉及到用戶態(tài)和內(nèi)核態(tài)切換的問題。既然 synchronized 里面的代碼執(zhí)行得非??欤环磷尩却i的線程不要被阻塞,而是在 synchronized 的邊界做忙循環(huán),這就是自旋。如果做了多次忙循環(huán)發(fā)現(xiàn)還沒有獲得鎖,再阻塞,這樣可能是一種更好的策略。
三十四、什么是 Java 內(nèi)存模型
Java 內(nèi)存模型定義了一種多線程訪問 Java 內(nèi)存的規(guī)范。Java 內(nèi)存模型的幾部分內(nèi)容:
1、Java 內(nèi)存模型將內(nèi)存分為了主內(nèi)存和工作內(nèi)存。類的狀態(tài),也就是類之間共享的變量,是存儲(chǔ)在主內(nèi)存中的,每次 Java 線程用到這些主內(nèi)存中的變量的時(shí)候,會(huì)讀一次主內(nèi)存中的變量,并讓這些內(nèi)存在自己的工作內(nèi)存中有一份拷貝,運(yùn)行自己線程代碼的時(shí)候,用到這些變量,操作的都是自己工作內(nèi)存中的那一份。在線程代碼執(zhí)行完畢之后,會(huì)將最新的值更新到主內(nèi)存中去。
2、定義了幾個(gè)原子操作,用于操作主內(nèi)存和工作內(nèi)存中的變量
3、定義了 volatile 變量的使用規(guī)則
4、happens-before,即先行發(fā)生原則,定義了操作 A 必然先行發(fā)生于操作B 的一些規(guī)則,比如在同一個(gè)線程內(nèi)控制流前面的代碼一定先行發(fā)生于控制流后面的代碼、一個(gè)釋放鎖 unlock 的動(dòng)作一定先行發(fā)生于后面對(duì)于同一個(gè)鎖進(jìn)行鎖定 lock 的動(dòng)作等等,只要符合這些規(guī)則,則不需要額外做同步措施,如果某段代碼不符合所有的 happens-before 規(guī)則,則這段代碼一定是線程非安全的。
三十五、什么是 CAS
CAS,全稱為 Compare and Swap,即比較-替換。假設(shè)有三個(gè)操作數(shù):內(nèi)存值 V、舊的預(yù)期值 A、要修改的值 B,當(dāng)且僅當(dāng)預(yù)期值 A 和內(nèi)存值 V 相同時(shí),才會(huì)將內(nèi)存值修改為 B 并返回 true,否則什么都不做并返回 false。當(dāng)然 CAS 一定要 volatile 變量配合,這樣才能保證每次拿到的變量是主內(nèi)存中最新的那個(gè)值,否則舊的預(yù)期值 A 對(duì)某條線程來說,永遠(yuǎn)是一個(gè)不會(huì)變的值 A,只要某次 CAS 操作失敗,永遠(yuǎn)都不可能成功。
三十六、什么是樂觀鎖和悲觀鎖
36.1 樂觀鎖
就像它的名字一樣,對(duì)于并發(fā)間操作產(chǎn)生的線程安全問題持樂觀狀態(tài),樂觀鎖認(rèn)為競爭不總是會(huì)發(fā)生,因此它不需要持有鎖,將比較-替換這兩個(gè)動(dòng)作作為一個(gè)原子操作嘗試去修改內(nèi)存中的變量,如果失敗則表示發(fā)生沖突,那么就應(yīng)該有相應(yīng)的重試邏輯。
36.2 悲觀鎖
還是像它的名字一樣,對(duì)于并發(fā)間操作產(chǎn)生的線程安全問題持悲觀狀態(tài),悲觀鎖認(rèn)為競爭總是會(huì)發(fā)生,因此每次對(duì)某資源進(jìn)行操作時(shí),都會(huì)持有一個(gè)獨(dú)占的鎖,就像 synchronized,不管三七二十一,直接上了鎖就操作資源了。
三十七、什么是 AQS
簡單說一下 AQS,AQS 全稱為 AbstractQueuedSychronizer,翻譯過來應(yīng)該是抽象隊(duì)列同步器。如果說 java.util.concurrent 的基礎(chǔ)是 CAS 的話,那么 AQS 就是整個(gè) Java并發(fā)包的核心了,ReentrantLock、CountDownLatch、Semaphore 等等都用到了它。AQS 實(shí)際上以雙向隊(duì)列的形式連接所有的 Entry,比方說ReentrantLock,所有等待的線程都被放在一個(gè) Entry 中并連成雙向隊(duì)列,前面一個(gè)線程使用 ReentrantLock 好了,則雙向隊(duì)列實(shí)際上的第一個(gè) Entry開始運(yùn)行。AQS 定義了對(duì)雙向隊(duì)列所有的操作,而只開放了 tryLock 和 tryRelease 方法給開發(fā)者使用,開發(fā)者可以根據(jù)自己的實(shí)現(xiàn)重寫 tryLock 和 tryRelease 方法,以實(shí)現(xiàn)自己的并發(fā)功能。
三十八、Hashtable 的 size()方法中明明只有一條語句"return count",為什么還要做同步?
1、同一時(shí)間只能有一條線程執(zhí)行固定類的同步方法,但是對(duì)于類的非同步方法,可以多條線程同時(shí)訪問。所以,這樣就有問題了,可能線程 A 在執(zhí)行Hashtable 的 put 方法添加數(shù)據(jù),線程 B 則可以正常調(diào)用 size()方法讀取Hashtable 中當(dāng)前元素的個(gè)數(shù),那讀取到的值可能不是最新的,可能線程 A添加了完了數(shù)據(jù),但是沒有對(duì) size++,線程 B 就已經(jīng)讀取 size 了,那么對(duì)于線程 B 來說讀取到的 size 一定是不準(zhǔn)確的。而給 size()方法加了同步之后,意味著線程 B 調(diào)用 size()方法只有在線程 A 調(diào)用 put 方法完畢之后才可以調(diào)用,這樣就保證了線程安全性
2、CPU 執(zhí)行代碼,執(zhí)行的不是 Java 代碼,這點(diǎn)很關(guān)鍵。Java代碼最終是被翻譯成機(jī)器碼執(zhí)行的,機(jī)器碼才是真正可以和硬件電路交互的代碼。即使你看到 Java 代碼只有一行,甚至你看到 Java 代碼編譯之后生成的字節(jié)碼也只有一行,也不意味著對(duì)于底層來說這句語句的操作只有一個(gè)。一句"return count"假設(shè)被翻譯成了三句匯編語句執(zhí)行,一句匯編語句和其機(jī)器碼做對(duì)應(yīng),完全可能執(zhí)行完第一句,線程就切換了。
三十九、同步方法和同步塊,哪個(gè)是更好的選擇
同步塊,這意味著同步塊之外的代碼是異步執(zhí)行的,這比同步整個(gè)方法更提升代碼的效率。請(qǐng)知道一條原則:同步的范圍越小越好。
雖說同步的范圍越少越好,但是在 Java 虛擬機(jī)中還是存在著一種叫做鎖粗化的優(yōu)化方法,這種方法就是把同步范圍變大。這是有用的,比方說 StringBuffer,它是一個(gè)線程安全的類,自然最常用的append()方法是一個(gè)同步方法,我們寫代碼的時(shí)候會(huì)反復(fù) append 字符串,這意味著要進(jìn)行反復(fù)的加鎖->解鎖,這對(duì)性能不利,因?yàn)檫@意味著 Java 虛擬機(jī)在這條線程上要反復(fù)地在內(nèi)核態(tài)和用戶態(tài)之間進(jìn)行切換,因此 Java 虛擬機(jī)會(huì)將多次 append 方法調(diào)用的代碼進(jìn)行一個(gè)鎖粗化的操作,將多次的 append的操作擴(kuò)展到 append 方法的頭尾,變成一個(gè)大的同步塊,這樣就減少了加鎖-->解鎖的次數(shù),有效地提升了代碼執(zhí)行的效率。
四十、高并發(fā)、任務(wù)執(zhí)行時(shí)間短的業(yè)務(wù)怎樣使用線程池?并發(fā)不高、任務(wù)執(zhí)行時(shí)間長的業(yè)務(wù)怎樣使用線程池?并發(fā)高、業(yè)務(wù)執(zhí)行時(shí)間長的業(yè)務(wù)怎樣使用線程池?
1、高并發(fā)、任務(wù)執(zhí)行時(shí)間短的業(yè)務(wù),線程池線程數(shù)可以設(shè)置為 CPU 核數(shù)+1,減少線程上下文的切換
2、并發(fā)不高、任務(wù)執(zhí)行時(shí)間長的業(yè)務(wù)要區(qū)分開看:
a)假如是業(yè)務(wù)時(shí)間長集中在 IO 操作上,也就是 IO 密集型的任務(wù),因?yàn)?IO操作并不占用 CPU,所以不要讓所有的 CPU 閑下來,可以加大線程池中的線程數(shù)目,讓 CPU 處理更多的業(yè)務(wù)。
b)假如是業(yè)務(wù)時(shí)間長集中在計(jì)算操作上,也就是計(jì)算密集型任務(wù),這個(gè)就沒辦法了,和(1)一樣吧,線程池中的線程數(shù)設(shè)置得少一些,減少線程上下文的切換
3、并發(fā)高、業(yè)務(wù)執(zhí)行時(shí)間長,解決這種類型任務(wù)的關(guān)鍵不在于線程池而在于整體架構(gòu)的設(shè)計(jì),看看這些業(yè)務(wù)里面某些數(shù)據(jù)是否能做緩存是第一步,增加服務(wù)器是第二步,至于線程池的設(shè)置,設(shè)置參考其他有關(guān)線程池的文章。最后,業(yè)務(wù)執(zhí)行時(shí)間長的問題,也可能需要分析一下,看看能不能使用中間件對(duì)任務(wù)進(jìn)行拆分和解耦。