一、現(xiàn)在有 T1低缩、T2、T3 三個線程曹货,你怎樣保證 T2 在 T1 執(zhí)行完后執(zhí)行咆繁,T3 在 T2 執(zhí)行完后執(zhí)行?
這個線程問題通常會在第一輪或電話面試階段被問到顶籽,目的是檢測你對”join”方法是否熟悉玩般。這個多線程問題比較簡單,可以用 join 方法實現(xiàn)礼饱。
線程按順序執(zhí)行-方式一(這種方式更好坏为,不會阻塞主線程)
/**
* @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的對象鎖才能進(jìn)入镊绪,只有獲得了鎖才能調(diào)用wait放棄對鎖的獨占并等待再次獲取鎖匀伏。
join方法用線程對象調(diào)用,如果在一個線程A中調(diào)用另一個線程B的join方法蝴韭,線程A將會等待線程B執(zhí)行完畢后再執(zhí)行够颠。
join 方法是一個阻塞方法,用來進(jìn)行線程之間的交流榄鉴。線程 A 調(diào)用 線程 B 的 join 方法摧找,則線程 A 將阻塞核行,線程 B 執(zhí)行結(jié)束后 線程 A 開始執(zhí)行。
問:join方法的作用蹬耘?
答:Thread類中的join方法的主要作用就是同步芝雪,它可以使得線程之間的并行執(zhí)行變?yōu)榇袌?zhí)行。當(dāng)我們調(diào)用某個線程的這個方法時综苔,這個方法會掛起調(diào)用線程惩系,直到被調(diào)用線程結(jié)束執(zhí)行,調(diào)用線程才會繼續(xù)執(zhí)行如筛。
問:join方法傳參和不傳參的區(qū)別堡牡?
答:join方法中如果傳入?yún)?shù),則表示這樣的意思:如果A線程中調(diào)用B線程的join(10)杨刨,則表示A線程會等待B線程執(zhí)行10毫秒晤柄,10毫秒過后,A妖胀、B線程并行執(zhí)行芥颈。需要注意的是,jdk規(guī)定赚抡,join(0)的意思不是A線程等待B線程0秒爬坑,而是A線程等待B線程無限時間,直到B線程執(zhí)行完畢涂臣,即join(0)等價于join()盾计。
問:join與start調(diào)用順序問題
答:join方法必須在線程start方法調(diào)用之后調(diào)用才有意義。這個也很容易理解:如果一個線程都沒有start赁遗,那它也就無法同步了署辉。因為執(zhí)行完start方法才會創(chuàng)建線程。
問:join方法實現(xiàn)原理
答:join方法是通過調(diào)用線程的wait方法來達(dá)到同步的目的的岩四。例如A線程中調(diào)用了B線程的join方法哭尝,則相當(dāng)于在A線程中調(diào)用了B線程的wait方法,當(dāng)B線程執(zhí)行完(或者到達(dá)等待時間)炫乓,B線程會自動調(diào)用自身的notifyAll方法喚醒A線程,從而達(dá)到同步的目的献丑。
二末捣、在 Java 中 Lock 接口比 synchronized 塊的優(yōu)勢是什么?你需要實現(xiàn)一個高效的緩存创橄,它允許多個用戶讀箩做,但只允許一個用戶寫,以此來保持它的完整性妥畏,你會怎樣去實現(xiàn)它邦邦?
lock 接口在多線程和并發(fā)編程中最大的優(yōu)勢是它們?yōu)樽x和寫分別提供了鎖安吁,它能滿足你寫像ConcurrentHashMap 這樣的高性能數(shù)據(jù)結(jié)構(gòu)和有條件的阻塞。Java 線程面試的問題越來越會根據(jù)面試者的回答來提問燃辖。我強(qiáng)烈建議在你去參加多線程的面試之前認(rèn)真讀一下 Locks鬼店,因為當(dāng)前其大量用于構(gòu)建電子交易終統(tǒng)的客戶端緩存和交易連接空間。
三黔龟、在 Java 中 wait 和 sleep 方法的不同妇智?
通常會在電話面試中經(jīng)常被問到的 Java 線程面試問題。最大的不同是在等待時 wait 會釋放鎖氏身,而 sleep 一直持有鎖巍棱。Wait 通常被用于線程間交互,sleep 通常被用于暫停執(zhí)行蛋欣。
四航徙、用 Java 實現(xiàn)阻塞隊列。
這是一個相對艱難的多線程面試問題陷虎,它能達(dá)到很多的目的到踏。第一,它可以檢測侯選者是否能實際的用 Java 線程寫程序泻红;第二夭禽,可以檢測侯選者對并發(fā)場景的理解,并且你可以根據(jù)這個問很多問題谊路。如果他用wait()和 notify()方法來實現(xiàn)阻塞隊列讹躯,你可以要求他用最新的Java5 中的并發(fā)類來再寫一次。
五缠劝、用 Java 寫代碼來解決生產(chǎn)者——消費者問題
與上面的問題很類似潮梯,但這個問題更經(jīng)典,有些時候面試都會問下面的問題惨恭。在 Java 中怎么解決生產(chǎn)者——消費者問題秉馏,當(dāng)然有很多解決方法,我已經(jīng)分享了一種用阻塞隊列實現(xiàn)的方法脱羡。有些時候他們甚至?xí)栐趺磳崿F(xiàn)哲學(xué)家進(jìn)餐問題萝究。
六、什么是原子操作锉罐,Java 中的原子操作是什么帆竹?
6.1 原子操作
原子操作是無法被別的線程打斷的操作。要么不執(zhí)行脓规,要么就執(zhí)行成功栽连。
例如:x=3是原子操作。過程就是先把工作內(nèi)存的x賦成3,再把主存的x賦成3秒紧。y=x不是原子操作绢陌,它涉及在工作內(nèi)存先把x值讀出來,再把這個值賦給y熔恢。x++或x=x+1也不是原子操作脐湾,它涉及取值,自加和賦值绩聘。
6.2 Java 中的原子操作
在Java中沥割,我們可以通過同步鎖或者CAS操作來實現(xiàn)原子操作。
CAS是Compare and swap的簡稱凿菩,這個操作是硬件級別的操作机杜,在硬件層面保證了操作的原子性。CAS有3個操作數(shù)衅谷,內(nèi)存值V椒拗,舊的預(yù)期值A(chǔ),要修改的新值B获黔。當(dāng)且僅當(dāng)預(yù)期值A(chǔ)和內(nèi)存值V相同時蚀苛,將內(nèi)存值V修改為B,否則什么都不做玷氏。Java中的sun.misc.Unsafe類提供了compareAndSwapInt和compareAndSwapLong等幾個方法實現(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)勢
隨著工業(yè)的進(jìn)步糟需,現(xiàn)在的筆記本屉佳、臺式機(jī)乃至商用的應(yīng)用服務(wù)器至少也都是雙核的,4 核洲押、8 核甚至 16 核的也都不少見武花,如果是單線程的程序,那么在雙核 CPU 上就浪費了 50%诅诱,在 4 核 CPU 上就浪費了 75%髓堪。單核 CPU 上所謂的"多線程"那是假的多線程送朱,同一時間處理器只會處理一段邏輯瞪慧,只不過線程之間切換得比較快臀晃,看著像多個線程"同時"運行罷了养渴。多核 CPU 上的多線程才是真正的多線程,它能讓你的多段邏輯同時工作争群,多線程,可以真正發(fā)揮出多核 CPU 的優(yōu)勢來大年,達(dá)到充分利用 CPU 的目的换薄。
8.2 防止阻塞
從程序運行效率的角度來看,單核 CPU 不但不會發(fā)揮出多線程的優(yōu)勢翔试,反而會因為在單核 CPU 上運行多線程導(dǎo)致線程上下文的切換轻要,而降低程序整體的效率。但是單核 CPU 我們還是要應(yīng)用多線程垦缅,就是為了防止阻塞冲泥。試想,如果單核 CPU 使用單線程壁涎,那么只要這個線程阻塞了凡恍,比方說遠(yuǎn)程讀取某個數(shù)據(jù)吧,對端遲遲未返回又沒有設(shè)置超時時間怔球,那么你的整個程序在數(shù)據(jù)返回回來之前就停止運行了嚼酝。多線程可以防止這個問題,多條線程同時運行竟坛,哪怕一條線程的代碼執(zhí)行讀取數(shù)據(jù)阻塞闽巩,也不會影響其它任務(wù)的執(zhí)行。
8.3 便于建模
這是另外一個沒有這么明顯的優(yōu)點了流码。假設(shè)有一個大的任務(wù) A又官,單線程編程,那么就要考慮很多漫试,建立整個程序模型比較麻煩六敬。但是如果把這個大的任務(wù) A分解成幾個小任務(wù),任務(wù) B驾荣、任務(wù) C外构、任務(wù) D,分別建立程序模型播掷,并通過多線程分別運行這幾個任務(wù)审编,那就簡單很多了。
九歧匈、創(chuàng)建線程的方式
9.1 繼承Thread類
看jdk源碼可以發(fā)現(xiàn)垒酬,Thread類其實是實現(xiàn)了Runnable接口的一個實例,繼承Thread類后需要重寫run方法并通過start方法啟動線程。繼承Thread類耦合性太強(qiáng)了勘究,因為java只能單繼承矮湘,所以不利于擴(kuò)展。
9.2 實現(xiàn)Runnable接口
通過實現(xiàn)Runnable接口并重寫run方法口糕,并把Runnable實例傳給Thread對象缅阳,Thread的start方法調(diào)用run方法再通過調(diào)用Runnable實例的run方法啟動線程。所以如果一個類繼承了另外一個父類景描,此時要實現(xiàn)多線程就不能通過繼承Thread的類實現(xiàn)十办。
9.3 實現(xiàn)Callable接口
通過實現(xiàn)Callable接口并重寫call方法,并把Callable實例傳給FutureTask對象超棺,再把FutureTask對象傳給Thread對象向族。它與Thread、Runnable最大的不同是Callable能返回一個異步處理的結(jié)果Future對象并能拋出異常棠绘,而其他兩種不能炸枣。
十、start()方法和 run()方法的區(qū)別
只有調(diào)用了 start()方法弄唧,才會表現(xiàn)出多線程的特性适肠,不同線程的 run()方法里面的代碼交替執(zhí)行。如果只是調(diào)用 run()方法候引,那么代碼還是同步執(zhí)行的侯养,必須等待一個線程的 run()方法里面的代碼全部執(zhí)行完畢之后,另外一個線程才可以執(zhí)行其 run()方法里面的代碼
十一澄干、Runnable 接口和 Callable 接口的區(qū)別
有點深的問題了逛揩,也看出一個 Java 程序員學(xué)習(xí)知識的廣度。Runnable 接口中的 run()方法的返回值是 void麸俘,它做的事情只是純粹地去執(zhí)行 run()方法中的代碼而已辩稽;Callable 接口中的 call()方法是有返回值的,是一個泛型从媚,和 Future逞泄、FutureTask 配合可以用來獲取異步執(zhí)行的結(jié)果。
這其實是很有用的一個特性拜效,因為多線程相比單線程更難喷众、更復(fù)雜的一個重要原因就是因為多線程充滿著未知性,某條線程是否執(zhí)行了紧憾?某條線程執(zhí)行了多久到千?某條線程執(zhí)行的時候我們期望的數(shù)據(jù)是否已經(jīng)賦值完畢?無法得知赴穗,我們能 做 的 只 是 等 待 這 條 多 線 程 的 任 務(wù) 執(zhí) 行 完 畢 而 已 憔四。 而Callable+Future/FutureTask 卻可以獲取多線程運行的結(jié)果膀息,可以在等待時間太長沒獲取到需要的數(shù)據(jù)的情況下取消該線程的任務(wù),真的是非常有用了赵。
十二履婉、CyclicBarrier 和 CountDownLatch 的區(qū)別
兩個看上去有點像的類,都在 java.util.concurrent 下斟览,都可以用來表示代碼運行到某個點上,二者的區(qū)別在于:
CyclicBarrier 的某個線程運行到某個點上之后辑奈,該線程即停止運行苛茂,直到所有的線程都到達(dá)了這個點,所有線程才重新運行鸠窗;CountDownLatch 則不是妓羊,某線程運行到某個點上之后,只是給某個數(shù)值-1 而已稍计,該線程繼續(xù)運行躁绸。
CyclicBarrier 只能喚起一個任務(wù),CountDownLatch 可以喚起多個任務(wù)臣嚣。
CyclicBarrier 可重用净刮,CountDownLatch 不可重用,計數(shù)值為 0 該CountDownLatch 就不可再用了硅则。
十三淹父、volatile 關(guān)鍵字的作用
一個非常重要的問題,是每個學(xué)習(xí)怎虫、應(yīng)用多線程的 Java 程序員都必須掌握的暑认。volatile 關(guān)鍵字的作用主要有兩個:
1、多線程主要圍繞可見性和原子性兩個特性而展開大审,使用 volatile 關(guān)鍵字修飾的變量蘸际,保證了其在多線程之間的可見性,即每次讀取到 volatile 變量徒扶,一定是最新的數(shù)據(jù)粮彤。
2、代碼底層執(zhí)行不像我們看到的高級語言----Java 程序這么簡單姜骡,它的執(zhí)行是 Java 代 碼 --> 字節(jié)碼 --> 根據(jù)字節(jié)碼執(zhí)行對應(yīng)的 C/C++ 代 碼-->C/C++代碼被編譯成匯編語言-->和硬件電路交互驾诈,現(xiàn)實中,為了獲取更好的性能 JVM 可能會對指令進(jìn)行重排序溶浴,多線程下可能會出現(xiàn)一些意想不到的問題乍迄。使用 volatile 則會對禁止語義重排序,當(dāng)然這也一定程度上降低了代碼執(zhí)行效率士败。
從實踐角度而言闯两,volatile 的一個重要作用就是和 CAS 結(jié)合褥伴,保證了原子性。
十四漾狼、什么是線程安全
如果你的代碼在多線程下執(zhí)行和在單線程下執(zhí)行永遠(yuǎn)都能獲得一樣的結(jié)果重慢,那么你的代碼就是線程安全的。這個問題有值得一提的地方逊躁,就是線程安全也是有幾個級別的:
14.1 不可變
像 String似踱、Integer、Long 這些稽煤,都是 final 類型的類轧简,任何一個線程都改變不了它們的值察藐,要改變除非新創(chuàng)建一個,因此這些不可變對象不需要任何同步手段就可以直接在多線程環(huán)境下使用惨寿。
14.2 絕對線程安全
不管運行時環(huán)境如何,調(diào)用者都不需要額外的同步措施。要做到這一點通常需要付出許多額外的代價,Java 中標(biāo)注自己是線程安全的類,實際上絕大多數(shù)都 不 是 線 程 安 全 的 蒸其, 不 過 絕 對 線 程 安 全 的 類 义屏, Java 中 也 有 , 比 方 說CopyOnWriteArrayList财喳、CopyOnWriteArraySet
14.3 相對線程安全
相對線程安全也就是我們通常意義上所說的線程安全,像 Vector 這種,add、remove 方法都是原子操作愈捅,不會被打斷青团,但也僅限于此,如果有個線程在遍歷某個 Vector瘪松、有個線程同時在 add 這個 Vector墅诡,99%的情況下都會出現(xiàn)ConcurrentModificationException然磷,也就是 fail-fast 機(jī)制寡润。
14.4 線程非安全
這個就沒什么好說的了变抽,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
另外提一點樱调,Thread 類提供了一個 getStackTrace()方法也可以用于獲取線程堆棧约素。這是一個實例方法,因此此方法是和具體線程實例綁定的笆凌,每次獲取獲取到的是具體某個線程當(dāng)前運行的堆棧圣猎。
十六、如何在兩個線程之間共享數(shù)據(jù)
通 過 在 線 程 之 間 共 享 對 象 就 可 以 了 乞而, 然 后 通 過 wait/notify/notifyAll 送悔、await/signal/signalAll 進(jìn)行喚起和等待,比方說阻塞隊列 BlockingQueue就是為線程之間共享數(shù)據(jù)而設(shè)計的爪模。
十七欠啤、sleep 方法和 wait 方法有什么區(qū)別
這個問題常問,sleep 方法和 wait 方法都可以用來放棄 CPU 一定的時間呻右,不同點在于如果線程持有某個對象的監(jiān)視器跪妥,sleep 方法不會放棄這個對象的監(jiān)視器鞋喇,wait 方法會放棄這個對象的監(jiān)視器声滥。
十八、生產(chǎn)者消費者模型的作用是什么
1)通過平衡生產(chǎn)者的生產(chǎn)能力和消費者的消費能力來提升整個系統(tǒng)的運行效率侦香,這是生產(chǎn)者消費者模型最重要的作用落塑。
2)解耦,這是生產(chǎn)者消費者模型附帶的作用罐韩,解耦意味著生產(chǎn)者和消費者之間的聯(lián)系少,聯(lián)系越少越可以獨自發(fā)展而不需要收到相互的制約。
十九晦款、ThreadLocal 有什么用
簡單說 ThreadLocal 就是一種以空間換時間的做法,在每個 Thread 里面維護(hù)了一個以開地址法實現(xiàn)的 ThreadLocal.ThreadLocalMap缓溅,把數(shù)據(jù)進(jìn)行隔離,數(shù)據(jù)不共享赁温,自然就沒有線程安全方面的問題了袜匿。
二十、為什么 wait()方法和 notify()/notifyAll()方法要在同步塊中被調(diào)用
這是 JDK 強(qiáng)制的穆壕,wait()方法和 notify()/notifyAll()方法在調(diào)用前都必須先獲得對象的鎖缨该。
二十一熄云、wait()方法和 notify()/notifyAll()方法在放棄對象監(jiān)視器時有什么區(qū)別
wait()方法立即釋放對象監(jiān)視器膨更,notify()/notifyAll()方法則會等待線程剩余代碼執(zhí)行完畢才會放棄對象監(jiān)視器。
二十二、為什么要使用線程池
避免頻繁地創(chuàng)建和銷毀線程誊役,達(dá)到線程對象的重用。另外谷市,使用線程池還可以根據(jù)項目靈活地控制并發(fā)的數(shù)目蛔垢。
二十三、怎么檢測一個線程是否持有對象監(jiān)視器
Thread 類提供了一個 holdsLock(Object obj)方法歌懒,當(dāng)且僅當(dāng)對象 obj 的監(jiān)視器被某條線程持有的時候才會返回 true啦桌,注意這是一個static 方法,這意味著"某條線程"指的是當(dāng)前線程。
二十四甫男、synchronized 和 ReentrantLock 的區(qū)別
synchronized 是和 if且改、else、for板驳、while 一樣的關(guān)鍵字又跛,ReentrantLock是類,這是二者的本質(zhì)區(qū)別若治。既然 ReentrantLock 是類慨蓝,那么它就提供了比synchronized 更多更靈活的特性,可以被繼承端幼、可以有方法礼烈、可以有各種各樣的類變量,ReentrantLock 比 synchronized 的擴(kuò)展性體現(xiàn)在幾點上:
1婆跑、ReentrantLock 可以對獲取鎖的等待時間進(jìn)行設(shè)置此熬,這樣就避免了死鎖
2、ReentrantLock 可以獲取各種鎖的信息
3滑进、ReentrantLock 可以靈活地實現(xiàn)多路通知
另外犀忱,二者的鎖機(jī)制其實也是不一樣的。 ReentrantLock 底層調(diào)用的是Unsafe 的 park 方法加鎖扶关,synchronized 操作的應(yīng)該是對象頭中 mark word阴汇。
二十五、ConcurrentHashMap 的并發(fā)度是什么
ConcurrentHashMap 的并發(fā)度就是 segment 的大小节槐,默認(rèn)為 16搀庶,這意味著 最 多 同 時 可 以 有 16 條線程 操 作 ConcurrentHashMap ,這也是ConcurrentHashMap 對 Hashtable 的最大優(yōu)勢疯淫。
二十六地来、ReadWriteLock 是什么
如果使用 ReentrantLock戳玫,可能本身是為了防止線程 A 在寫數(shù)據(jù)熙掺、線程 B 在讀數(shù)據(jù)造成的數(shù)據(jù)不一致,但這樣咕宿,如果線程 C 在讀數(shù)據(jù)币绩、線程 D也在讀數(shù)據(jù),讀數(shù)據(jù)是不會改變數(shù)據(jù)的府阀,沒有必要加鎖缆镣,但是還是加鎖了,降低了程序的性能试浙。因為這個董瞻,才誕生了讀寫鎖 ReadWriteLock。ReadWriteLock 是一個讀寫鎖接口,ReentrantReadWriteLock 是 ReadWriteLock 接口的一個具體實現(xiàn)钠糊,實現(xiàn)了讀寫的分離挟秤,讀鎖是共享的,寫鎖是獨占的抄伍,讀和讀之間不會互斥艘刚,讀和寫、寫和讀截珍、寫和寫之間才會互斥攀甚,提升了讀寫的性能。
二十七岗喉、FutureTask 是什么
FutureTask 表示一個異步運算的任務(wù)秋度。FutureTask里面可以傳入一個 Callable 的具體實現(xiàn)類,可以對這個異步運算的任務(wù)的結(jié)果進(jìn)行等待獲取钱床、判斷是否已經(jīng)完成静陈、取消任務(wù)等操作。當(dāng)然诞丽,由于 FutureTask也是 Runnable 接口的實現(xiàn)類鲸拥,所以 FutureTask 也可以放入線程池中。
二十八僧免、Linux 環(huán)境下如何查找哪個線程使用 CPU 最長
1刑赶、獲取項目的 pid,jps 或者 ps -ef | grep java
2懂衩、top -H -p pid撞叨,順序不能改變這樣就可以打印出當(dāng)前的項目,每條線程占用 CPU 時間的百分比浊洞。注意這里
打出的是 LWP牵敷,也就是操作系統(tǒng)原生線程的線程號。
使用"top -H -p pid"+"jps pid"可以很容易地找到某條占用 CPU 高的線程的線程堆棧法希,從而定位占用 CPU 高的原因枷餐,一般是因為不當(dāng)?shù)拇a操作導(dǎo)致了死循環(huán)。最后提一點苫亦,"top -H -p pid"打出來的 LWP 是十進(jìn)制的毛肋,"jps pid"打出來的本地線程號是十六進(jìn)制的,轉(zhuǎn)換一下屋剑,就能定位到占用 CPU 高的線程的當(dāng)前線程堆棧了润匙。
二十九、什么是多線程的上下文切換
多線程的上下文切換是指 CPU 控制權(quán)由一個已經(jīng)正在運行的線程切換到另外一個就緒并等待獲取 CPU 執(zhí)行權(quán)的線程的過程唉匾。
三十孕讳、如果你提交任務(wù)時,線程池隊列已滿,這時會發(fā)生什么
1厂财、如果使用的是無界隊列 LinkedBlockingQueue油啤,也就是無界隊列的話,沒關(guān)系蟀苛,繼續(xù)添加任務(wù)到阻塞隊列中等待執(zhí)行益咬,因為 LinkedBlockingQueue 可以近乎認(rèn)為是一個無窮大的隊列,可以無限存放任務(wù)
2帜平、如果使用的是有界隊列比如 ArrayBlockingQueue幽告,任務(wù)首先會被添加到ArrayBlockingQueue 中 , ArrayBlockingQueue 滿 了 裆甩, 會 根 據(jù)maximumPoolSize 的值增加線程數(shù)量冗锁,如果增加了線程數(shù)量還是處理不過來,ArrayBlockingQueue 繼 續(xù) 滿嗤栓,那么則會使用拒絕策略RejectedExecutionHandler 處理滿了的任務(wù)冻河,默認(rèn)是 AbortPolicy
三十一、Java 中用到的線程調(diào)度算法是什么
搶占式茉帅。一個線程用完 CPU 之后叨叙,操作系統(tǒng)會根據(jù)線程優(yōu)先級、線程饑餓情況等數(shù)據(jù)算出一個總的優(yōu)先級并分配下一個時間片給某個線程執(zhí)行堪澎。
三十二擂错、Thread.sleep(0)的作用是什么
由于Java采用搶占式的線程調(diào)度算法,因此可能會出現(xiàn)某條線程常常獲取到CPU控制權(quán)的情況樱蛤,為了讓某些優(yōu)先級比較低的線程也能獲取到CPU控制權(quán)钮呀,可以使用Thread.sleep(0)手動觸發(fā)一次操作系統(tǒng)分配時間片的操作,這也是平衡CPU控制權(quán)的一種操作昨凡。
三十三爽醋、什么是自旋
很多 synchronized 里面的代碼只是一些很簡單的代碼,執(zhí)行時間非潮慵梗快蚂四,此時等待的線程都加鎖可能是一種不太值得的操作,因為線程阻塞涉及到用戶態(tài)和內(nèi)核態(tài)切換的問題就轧。既然 synchronized 里面的代碼執(zhí)行得非持ず迹快田度,不妨讓等待鎖的線程不要被阻塞妒御,而是在 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)异希,也就是類之間共享的變量,是存儲在主內(nèi)存中的绒瘦,每次 Java 線程用到這些主內(nèi)存中的變量的時候称簿,會讀一次主內(nèi)存中的變量,并讓這些內(nèi)存在自己的工作內(nèi)存中有一份拷貝惰帽,運行自己線程代碼的時候憨降,用到這些變量,操作的都是自己工作內(nèi)存中的那一份该酗。在線程代碼執(zhí)行完畢之后授药,會將最新的值更新到主內(nèi)存中去。
2呜魄、定義了幾個原子操作悔叽,用于操作主內(nèi)存和工作內(nèi)存中的變量
3、定義了 volatile 變量的使用規(guī)則
4爵嗅、happens-before骄蝇,即先行發(fā)生原則,定義了操作 A 必然先行發(fā)生于操作B 的一些規(guī)則操骡,比如在同一個線程內(nèi)控制流前面的代碼一定先行發(fā)生于控制流后面的代碼九火、一個釋放鎖 unlock 的動作一定先行發(fā)生于后面對于同一個鎖進(jìn)行鎖定 lock 的動作等等,只要符合這些規(guī)則册招,則不需要額外做同步措施岔激,如果某段代碼不符合所有的 happens-before 規(guī)則,則這段代碼一定是線程非安全的是掰。
三十五虑鼎、什么是 CAS
CAS,全稱為 Compare and Swap键痛,即比較-替換炫彩。假設(shè)有三個操作數(shù):內(nèi)存值 V、舊的預(yù)期值 A絮短、要修改的值 B江兢,當(dāng)且僅當(dāng)預(yù)期值 A 和內(nèi)存值 V 相同時,才會將內(nèi)存值修改為 B 并返回 true丁频,否則什么都不做并返回 false杉允。當(dāng)然 CAS 一定要 volatile 變量配合邑贴,這樣才能保證每次拿到的變量是主內(nèi)存中最新的那個值,否則舊的預(yù)期值 A 對某條線程來說叔磷,永遠(yuǎn)是一個不會變的值 A拢驾,只要某次 CAS 操作失敗,永遠(yuǎn)都不可能成功改基。
三十六繁疤、什么是樂觀鎖和悲觀鎖
36.1 樂觀鎖
就像它的名字一樣,對于并發(fā)間操作產(chǎn)生的線程安全問題持樂觀狀態(tài)秕狰,樂觀鎖認(rèn)為競爭不總是會發(fā)生嵌洼,因此它不需要持有鎖,將比較-替換這兩個動作作為一個原子操作嘗試去修改內(nèi)存中的變量封恰,如果失敗則表示發(fā)生沖突麻养,那么就應(yīng)該有相應(yīng)的重試邏輯。
36.2 悲觀鎖
還是像它的名字一樣诺舔,對于并發(fā)間操作產(chǎn)生的線程安全問題持悲觀狀態(tài)鳖昌,悲觀鎖認(rèn)為競爭總是會發(fā)生,因此每次對某資源進(jìn)行操作時低飒,都會持有一個獨占的鎖许昨,就像 synchronized,不管三七二十一褥赊,直接上了鎖就操作資源了糕档。
三十七、什么是 AQS
簡單說一下 AQS拌喉,AQS 全稱為 AbstractQueuedSychronizer速那,翻譯過來應(yīng)該是抽象隊列同步器。如果說 java.util.concurrent 的基礎(chǔ)是 CAS 的話尿背,那么 AQS 就是整個 Java并發(fā)包的核心了端仰,ReentrantLock、CountDownLatch田藐、Semaphore 等等都用到了它荔烧。AQS 實際上以雙向隊列的形式連接所有的 Entry,比方說ReentrantLock汽久,所有等待的線程都被放在一個 Entry 中并連成雙向隊列鹤竭,前面一個線程使用 ReentrantLock 好了,則雙向隊列實際上的第一個 Entry開始運行景醇。AQS 定義了對雙向隊列所有的操作臀稚,而只開放了 tryLock 和 tryRelease 方法給開發(fā)者使用,開發(fā)者可以根據(jù)自己的實現(xiàn)重寫 tryLock 和 tryRelease 方法啡直,以實現(xiàn)自己的并發(fā)功能烁涌。
三十八苍碟、Hashtable 的 size()方法中明明只有一條語句"return count"酒觅,為什么還要做同步撮执?
1、同一時間只能有一條線程執(zhí)行固定類的同步方法舷丹,但是對于類的非同步方法抒钱,可以多條線程同時訪問。所以颜凯,這樣就有問題了谋币,可能線程 A 在執(zhí)行Hashtable 的 put 方法添加數(shù)據(jù),線程 B 則可以正常調(diào)用 size()方法讀取Hashtable 中當(dāng)前元素的個數(shù)症概,那讀取到的值可能不是最新的蕾额,可能線程 A添加了完了數(shù)據(jù),但是沒有對 size++彼城,線程 B 就已經(jīng)讀取 size 了诅蝶,那么對于線程 B 來說讀取到的 size 一定是不準(zhǔn)確的。而給 size()方法加了同步之后募壕,意味著線程 B 調(diào)用 size()方法只有在線程 A 調(diào)用 put 方法完畢之后才可以調(diào)用调炬,這樣就保證了線程安全性
2、CPU 執(zhí)行代碼舱馅,執(zhí)行的不是 Java 代碼缰泡,這點很關(guān)鍵。Java代碼最終是被翻譯成機(jī)器碼執(zhí)行的代嗤,機(jī)器碼才是真正可以和硬件電路交互的代碼棘钞。即使你看到 Java 代碼只有一行,甚至你看到 Java 代碼編譯之后生成的字節(jié)碼也只有一行干毅,也不意味著對于底層來說這句語句的操作只有一個武翎。一句"return count"假設(shè)被翻譯成了三句匯編語句執(zhí)行,一句匯編語句和其機(jī)器碼做對應(yīng)溶锭,完全可能執(zhí)行完第一句宝恶,線程就切換了。
三十九趴捅、同步方法和同步塊垫毙,哪個是更好的選擇
同步塊,這意味著同步塊之外的代碼是異步執(zhí)行的拱绑,這比同步整個方法更提升代碼的效率综芥。請知道一條原則:同步的范圍越小越好。
雖說同步的范圍越少越好猎拨,但是在 Java 虛擬機(jī)中還是存在著一種叫做鎖粗化的優(yōu)化方法膀藐,這種方法就是把同步范圍變大屠阻。這是有用的,比方說 StringBuffer额各,它是一個線程安全的類国觉,自然最常用的append()方法是一個同步方法,我們寫代碼的時候會反復(fù) append 字符串虾啦,這意味著要進(jìn)行反復(fù)的加鎖->解鎖麻诀,這對性能不利,因為這意味著 Java 虛擬機(jī)在這條線程上要反復(fù)地在內(nèi)核態(tài)和用戶態(tài)之間進(jìn)行切換傲醉,因此 Java 虛擬機(jī)會將多次 append 方法調(diào)用的代碼進(jìn)行一個鎖粗化的操作蝇闭,將多次的 append的操作擴(kuò)展到 append 方法的頭尾,變成一個大的同步塊硬毕,這樣就減少了加鎖-->解鎖的次數(shù)呻引,有效地提升了代碼執(zhí)行的效率。
四十吐咳、高并發(fā)逻悠、任務(wù)執(zhí)行時間短的業(yè)務(wù)怎樣使用線程池?并發(fā)不高挪丢、任務(wù)執(zhí)行時間長的業(yè)務(wù)怎樣使用線程池蹂风?并發(fā)高、業(yè)務(wù)執(zhí)行時間長的業(yè)務(wù)怎樣使用線程池乾蓬?
1惠啄、高并發(fā)、任務(wù)執(zhí)行時間短的業(yè)務(wù)任内,線程池線程數(shù)可以設(shè)置為 CPU 核數(shù)+1撵渡,減少線程上下文的切換
2、并發(fā)不高死嗦、任務(wù)執(zhí)行時間長的業(yè)務(wù)要區(qū)分開看:
a)假如是業(yè)務(wù)時間長集中在 IO 操作上趋距,也就是 IO 密集型的任務(wù),因為 IO操作并不占用 CPU越除,所以不要讓所有的 CPU 閑下來节腐,可以加大線程池中的線程數(shù)目,讓 CPU 處理更多的業(yè)務(wù)摘盆。
b)假如是業(yè)務(wù)時間長集中在計算操作上翼雀,也就是計算密集型任務(wù),這個就沒辦法了孩擂,和(1)一樣吧狼渊,線程池中的線程數(shù)設(shè)置得少一些,減少線程上下文的切換
3类垦、并發(fā)高狈邑、業(yè)務(wù)執(zhí)行時間長城须,解決這種類型任務(wù)的關(guān)鍵不在于線程池而在于整體架構(gòu)的設(shè)計,看看這些業(yè)務(wù)里面某些數(shù)據(jù)是否能做緩存是第一步米苹,增加服務(wù)器是第二步糕伐,至于線程池的設(shè)置,設(shè)置參考其他有關(guān)線程池的文章驱入。最后赤炒,業(yè)務(wù)執(zhí)行時間長的問題氯析,也可能需要分析一下亏较,看看能不能使用中間件對任務(wù)進(jìn)行拆分和解耦。