多个线程访问统一对象的不同方法_不会多线程还想进BAT?精选19道多线程面试题,有答案边看边学...
一. Java程序如何停止一個(gè)線程?
建議使用”異常法”來(lái)終止線程的繼續(xù)運(yùn)行。在想要被中斷執(zhí)行的線程中, 調(diào)用 interrupted()方法,該方法用來(lái)檢驗(yàn)當(dāng)前線程是否已經(jīng)被中斷,即該線程 是否被打上了中斷的標(biāo)記,并不會(huì)使得線程立即停止運(yùn)行,如果返回 true,則 拋出異常,停止線程的運(yùn)行。在線程外,調(diào)用 interrupt()方法,使得該線程打 上中斷的標(biāo)記。
二. 說(shuō)一下 java 中的多線程。
1. Java 中實(shí)現(xiàn)多線程的四種方式(創(chuàng)建多線程的四種方式)?
①. 繼承 Thread 類(lèi)創(chuàng)建線程類(lèi)
- 定義 Thread 類(lèi)的子類(lèi),并重寫(xiě)該類(lèi)的 run 方法,該 run 方法的方 法體就代表了線程要完成的任務(wù)。因此把 run()方法稱為執(zhí)行體。
- 創(chuàng)建 Thread 子類(lèi)的實(shí)例,即創(chuàng)建了線程對(duì)象。
- 調(diào)用線程對(duì)象的 start()方法來(lái)啟動(dòng)該線程。
②. 通過(guò) Runnable 接口創(chuàng)建線程類(lèi)
- 定義 Runnable 接口的實(shí)現(xiàn)類(lèi),并重寫(xiě)該接口的 run()方法,該 run() 方法的方法體同樣是該線程的線程執(zhí)行體。
- 創(chuàng)建 Runnable 實(shí)現(xiàn)類(lèi)的實(shí)例,并依此實(shí)例作為 Thread 的 target 來(lái)創(chuàng)建 Thread 對(duì)象,該 Thread 對(duì)象才是真正的線程對(duì)象。
- 調(diào)用線程對(duì)象的 start()方法來(lái)啟動(dòng)該線程。
③. 通過(guò) Callable 和 Future 創(chuàng)建線程
- 創(chuàng)建 Callable 接口的實(shí)現(xiàn)類(lèi),并實(shí)現(xiàn) call()方法,該 call()方法將作 為線程執(zhí)行體,并且有返回值。
- 創(chuàng)建 Callable 實(shí)現(xiàn)類(lèi)的實(shí)例,使用 FutureTask 類(lèi)來(lái)包裝 Callable 對(duì)象,該 FutureTask 對(duì)象封裝了該 Callable 對(duì)象的 call()方法的返回值。
- 使用 FutureTask 對(duì)象作為 Thread 對(duì)象的 target 創(chuàng)建并啟動(dòng)新線 程。
- 調(diào)用 FutureTask 對(duì)象的 get()方法來(lái)獲得子線程執(zhí)行結(jié)束后的返回值。
④. 通過(guò)線程池創(chuàng)建線程
利用線程池不用 new 就可以創(chuàng)建線程,線程可復(fù)用,利用 Executors 創(chuàng) 建線程池。
擴(kuò)展1:Java 中 Runnable 和 Callable 有什么不同?
- Callable 定義的方法是 call(),而 Runnable 定義的方法是 run()。
- Callable 的 call 方法可以有返回值,而 Runnable 的 run 方法不能有 返回值。
- Callable 的 call 方法可拋出異常,而 Runnable 的 run 方法不能拋出 異常。
擴(kuò)展2:一個(gè)類(lèi)是否可以同時(shí)繼承 Thread 和實(shí)現(xiàn) Runnable接口?<
可以。比如下面的程序可以通過(guò)編譯。因?yàn)?Test 類(lèi)從 Thread 類(lèi)中繼承了 run()方法,這個(gè) run()方法可以被當(dāng)作對(duì) Runnable 接口的實(shí)現(xiàn)。
public class Test extends Thread implements Runnable { public static void main(String[] args) { Thread t = new Thread(new Test()); t.start(); }}2. 實(shí)現(xiàn)多線程的同步。
在多線程的環(huán)境中,經(jīng)常會(huì)遇到數(shù)據(jù)的共享問(wèn)題,即當(dāng)多個(gè)線程需要訪問(wèn)同 一資源時(shí),他們需要以某種順序來(lái)確保該資源在某一時(shí)刻只能被一個(gè)線程使用, 否則,程序的運(yùn)行結(jié)果將會(huì)是不可預(yù)料的,在這種情況下,就必須對(duì)數(shù)據(jù)進(jìn)行 同步。
在 Java 中,提供了四種方式來(lái)實(shí)現(xiàn)同步互斥訪問(wèn): synchronized 和 Lock 和 wait()/notify()/notifyAll()方法和 CAS。
①. synchronized 的用法。
A . 同步代碼塊
synchronized 塊寫(xiě)法: synchronized(object) {}表示線程在執(zhí)行的時(shí)候會(huì)將 object 對(duì)象上鎖。(注意這個(gè)對(duì)象可以是任意 類(lèi)的對(duì)象,也可以使用 this 關(guān)鍵字或者是 class 對(duì)象)。
可能一個(gè)方法中只有幾行代碼會(huì)涉及到線程同步問(wèn)題,所以 synchronized 塊 比 synchronized 方法更加細(xì)粒度地控制了多個(gè)線程的訪問(wèn), 只有 synchronized 塊中的內(nèi)容不能同時(shí)被多個(gè)線程所訪問(wèn),方法中的其他語(yǔ)句仍然 可以同時(shí)被多個(gè)線程所訪問(wèn)(包括 synchronized 塊之前的和之后的)。
B . 修飾非靜態(tài)的方法
當(dāng) synchronized 關(guān)鍵字修飾一個(gè)方法的時(shí)候,該方法叫做同步方法。
Java 中的每個(gè)對(duì)象都有一個(gè)鎖(lock),或者叫做監(jiān)視器(monitor), 當(dāng)一個(gè)線程訪問(wèn)某個(gè)對(duì)象的 synchronized 方法時(shí),將該對(duì)象上鎖,其他任何 線程都無(wú)法再去訪問(wèn)該對(duì)象的 synchronized 方法了(這里是指所有的同步方 法,而不僅僅是同一個(gè)方法),直到之前的那個(gè)線程執(zhí)行方法完畢后(或者是 拋出了異常),才將該對(duì)象的鎖釋放掉,其他線程才有可能再去訪問(wèn)該對(duì)象的 synchronized 方法。
注意這時(shí)候是給對(duì)象上鎖,如果是不同的對(duì)象,則各個(gè)對(duì)象之間沒(méi)有限制 關(guān)系。
注意,如果一個(gè)對(duì)象有多個(gè) synchronized 方法,某一時(shí)刻某個(gè)線程已經(jīng)進(jìn)入 到了某個(gè) synchronized 方法,那么在該方法沒(méi)有執(zhí)行完畢前,其他線程是無(wú)法訪 問(wèn)該對(duì)象的任何 synchronized 方法的。
C . 修飾靜態(tài)的方法
當(dāng)一個(gè) synchronized 關(guān)鍵字修飾的方法同時(shí)又被 static 修飾,之前說(shuō)過(guò), 非靜態(tài)的同步方法會(huì)將對(duì)象上鎖,但是靜態(tài)方法不屬于對(duì)象,而是屬于類(lèi),它 會(huì)將這個(gè)方法所在的類(lèi)的 Class 對(duì)象上鎖。一個(gè)類(lèi)不管生成多少個(gè)對(duì)象,它們 所對(duì)應(yīng)的是同一個(gè) Class 對(duì)象。
因此,當(dāng)線程分別訪問(wèn)同一個(gè)類(lèi)的兩個(gè)對(duì)象的兩個(gè) static,synchronized 方法時(shí),它們的執(zhí)行順序也是順序的,也就是說(shuō)一個(gè)線程先去執(zhí)行方法,執(zhí)行 完畢后另一個(gè)線程才開(kāi)始。
結(jié)論:
- synchronized 方法是一種粗粒度的并發(fā)控制,某一時(shí)刻,只能有一個(gè)線 程執(zhí)行該 synchronized 方法。
- synchronized 塊則是一種細(xì)粒度的并發(fā)控制,只會(huì)將塊中的代碼同步, 位于方法內(nèi),synchronized 塊之外的其他代碼是可以被多個(gè)線程同時(shí)訪問(wèn)到 的。
②.Lock 的用法。
使用 Lock 必須在 try-catch-finally 塊中進(jìn)行,并且將釋放鎖的操作放在 finally 塊中進(jìn)行,以保證鎖一定被釋放,防止死鎖的發(fā)生。通常使用 Lock 來(lái) 進(jìn)行同步的話,是以下面這種形式去使用的:
Lock lock = ...;lock.lock();try{ //處理任務(wù)}catch(Exception ex){}finally{ lock.unlock(); //釋放鎖}Lock 和 synchronized 的區(qū)別和 Lock 的優(yōu)勢(shì)。你需要實(shí)現(xiàn) 一個(gè)高效的緩存,它允許多個(gè)用戶讀,但只允許一個(gè)用戶寫(xiě),以此 來(lái)保持它的完整性,你會(huì)怎樣去實(shí)現(xiàn)它?
- Lock 是一個(gè)接口,而 synchronized 是 Java 中的關(guān)鍵字, synchronized 是內(nèi)置的語(yǔ)言實(shí)現(xiàn);
- synchronized 在發(fā)生異常時(shí),會(huì)自動(dòng)釋放線程占有的鎖,因此不會(huì)導(dǎo) 致死鎖現(xiàn)象發(fā)生;而 Lock 在發(fā)生異常時(shí),如果沒(méi)有主動(dòng)通過(guò) unLock()去釋放 鎖,則很可能造成死鎖現(xiàn)象,因此使用 Lock 時(shí)需要在 finally 塊中釋放鎖;
- Lock 可以讓等待鎖的線程響應(yīng)中斷(可中斷鎖),而 synchronized 卻不行,使用 synchronized 時(shí),等待的線程會(huì)一直等待下去,不能夠響應(yīng)中 斷(不可中斷鎖);
- 通過(guò) Lock 可以知道有沒(méi)有成功獲取鎖(tryLock()方法:如果獲取 了鎖,則返回 true;否則返回 false,也就說(shuō)這個(gè)方法無(wú)論如何都會(huì)立即返回。 在拿不到鎖時(shí)不會(huì)一直在那等待。),而 synchronized 卻無(wú)法辦到。
- Lock 可以提高多個(gè)線程進(jìn)行讀操作的效率(讀寫(xiě)鎖)。
- Lock 可以實(shí)現(xiàn)公平鎖,synchronized 不保證公平性。 在性能上來(lái)說(shuō),如果線程競(jìng)爭(zhēng)資源不激烈時(shí),兩者的性能是差不多的,而 當(dāng)競(jìng)爭(zhēng)資源非常激烈時(shí)(即有大量線程同時(shí)競(jìng)爭(zhēng)),此時(shí) Lock 的性能要遠(yuǎn)遠(yuǎn)優(yōu) 于 synchronized。所以說(shuō),在具體使用時(shí)要根據(jù)適當(dāng)情況選擇。
擴(kuò)展1: volatile 和 synchronized 區(qū)別。
- volatile 是變量修飾符,而 synchronized 則作用于代碼塊或方法。
- volatile 不會(huì)對(duì)變量加鎖,不會(huì)造成線程的阻塞;synchronized 會(huì) 對(duì)變量加鎖,可能會(huì)造成線程的阻塞。
- volatile 僅能實(shí)現(xiàn)變量的修改可見(jiàn)性,并不能保證原子性;而 synchronized 則 可 以 保 證 變 量 的 修 改 可 見(jiàn) 性 和 原 子 性 。 (synchronized 有兩個(gè)重要含義:它確保了一次只有一個(gè)線程可以執(zhí) 行代碼的受保護(hù)部分(互斥),而且它確保了一個(gè)線程更改的數(shù)據(jù)對(duì)于 其它線程是可見(jiàn)的(更改的可見(jiàn)性),在釋放鎖之前會(huì)將對(duì)變量的修改 刷新到主存中)。
- volatile 標(biāo)記的變量不會(huì)被編譯器優(yōu)化,禁止指令重排序; synchronized 標(biāo)記的變量可以被編譯器優(yōu)化。
擴(kuò)展 2:什么場(chǎng)景下可以使用 volatile 替換 synchronized?
只需要保證共享資源的可見(jiàn)性的時(shí)候可以使用 volatile 替代, synchronized 保證可操作的原子性,一致性和可見(jiàn)性。
③.wait()otify()otifyAll()的用法(Java 中怎樣喚醒一個(gè)阻塞的線程?)。
在 Java 發(fā)展史上曾經(jīng)使用 suspend()、resume()方法對(duì)于線程進(jìn)行阻塞喚醒,但隨之出 現(xiàn)很多問(wèn)題,比較典型的還是死鎖問(wèn)題。
解決方案可以使用以對(duì)象為目標(biāo)的阻塞,即利用 Object 類(lèi)的 wait()和 notify()方法實(shí)現(xiàn) 線程阻塞。
首先,wait、notify 方法是針對(duì)對(duì)象的,調(diào)用任意對(duì)象的 wait()方法都將導(dǎo)致線程阻塞, 阻塞的同時(shí)也將釋放該對(duì)象的鎖,相應(yīng)地,調(diào)用任意對(duì)象的 notify()方法則將隨機(jī)解除該對(duì) 象阻塞的線程,但它需要重新獲取改對(duì)象的鎖,直到獲取成功才能往下執(zhí)行;其次,wait、 notify 方法必須在 synchronized 塊或方法中被調(diào)用,并且要保證同步塊或方法的鎖對(duì)象與調(diào) 用 wait、notify 方法的對(duì)象是同一個(gè),如此一來(lái)在調(diào)用 wait 之前當(dāng)前線程就已經(jīng)成功獲取 某對(duì)象的鎖,執(zhí)行 wait 阻塞后當(dāng)前線程就將之前獲取的對(duì)象鎖釋放。
擴(kuò)展 1: 為什么 wait(),notify(),notifyAll()等方法都定義在 Object 類(lèi)中?
因?yàn)檫@三個(gè)方法都需要定義在同步代碼塊或同步方法中,這些方法的調(diào)用是依賴鎖對(duì) 象的,而同步代碼塊或同步方法中的鎖對(duì)象可以是任意對(duì)象,那么能被任意對(duì)象調(diào)用的方 法一定定義在 Object 類(lèi)中。
擴(kuò)展 2: notify()和 notifyAll()有什么區(qū)別?
notify()和 notifyAll()都是 Object 對(duì)象用于通知處在等待該對(duì)象的線程的方法。
void notify(): 喚醒一個(gè)正在等待該對(duì)象的線程,進(jìn)入就緒隊(duì)列等待 CPU 的調(diào)度。
void notifyAll(): 喚醒所有正在等待該對(duì)象的線程,進(jìn)入就緒隊(duì)列等待 CPU 的調(diào)度。
兩者的最大區(qū)別在于:
notifyAll 使所有原來(lái)在該對(duì)象上等待被 notify 的線程統(tǒng)統(tǒng)退出 wait 的狀態(tài),變成等待該對(duì) 象上的鎖,一旦該對(duì)象被解鎖,他們就會(huì)去競(jìng)爭(zhēng)。 notify 他只是選擇一個(gè) wait 狀態(tài)線程進(jìn)行通知,并使它獲得該對(duì)象上的鎖,但不驚動(dòng)其他 同樣在等待被該對(duì)象 notify 的線程們,當(dāng)?shù)谝粋€(gè)線程運(yùn)行完畢以后釋放對(duì)象上的鎖,此時(shí) 如果該對(duì)象沒(méi)有再次使用 notify 語(yǔ)句,即便該對(duì)象已經(jīng)空閑,其他 wait 狀態(tài)等待的線程由 于沒(méi)有得到該對(duì)象的通知,繼續(xù)處在 wait 狀態(tài),直到這個(gè)對(duì)象發(fā)出一個(gè) notify 或 notifyAll, 它們等待的是被 notify 或 notifyAll,而不是鎖。
④.CAS
它是一種非阻塞的同步方式。具體參見(jiàn)上面的部分。
擴(kuò)展一:同步鎖的分類(lèi)?
- Synchronized 和 Lock 都是悲觀鎖。
- 樂(lè)觀鎖,CAS 同步原語(yǔ),如原子類(lèi),非阻塞同步方式。
擴(kuò)展二:鎖的分類(lèi)?
- 一種是代碼層次上的,如 java 中的同步鎖,可重入鎖,公平鎖,讀寫(xiě)鎖。另外一種是數(shù)據(jù)庫(kù)層次上的,比較典型的就是悲觀鎖和樂(lè)觀鎖,表鎖,行鎖,頁(yè)鎖。
擴(kuò)展三:java 中的悲觀鎖和樂(lè)觀鎖?
- 悲觀鎖:悲觀鎖是認(rèn)為肯定有其他線程來(lái)爭(zhēng)奪資源,因此不管到底會(huì)不會(huì)發(fā)生爭(zhēng)奪, 悲觀鎖總是會(huì)先去鎖住資源,會(huì)導(dǎo)致其它所有需要鎖的線程掛起,等待持有鎖的線程釋放 鎖。Synchronized 和 Lock 都是悲觀鎖。
- 樂(lè)觀鎖:每次不加鎖,假設(shè)沒(méi)有沖突去完成某項(xiàng)操作,如果因?yàn)闆_突失敗就重試,直 到成功為止。就是當(dāng)去做某個(gè)修改或其他操作的時(shí)候它認(rèn)為不會(huì)有其他線程來(lái)做同樣的操 作(競(jìng)爭(zhēng)),這是一種樂(lè)觀的態(tài)度,通常是基于 CAS 原子指令來(lái)實(shí)現(xiàn)的。CAS 通常不會(huì)將 線程掛起,因此有時(shí)性能會(huì)好一些。樂(lè)觀鎖的一種實(shí)現(xiàn)方式——CAS。
三. 實(shí)現(xiàn)線程之間的通信?
當(dāng)線程間是可以共享資源時(shí),線程間通信是協(xié)調(diào)它們的重要的手段。
1. Object 類(lèi)中 wait()otify()otifyAll()方法。
2. 用 Condition 接口。
- Condition 是被綁定到 Lock 上的,要?jiǎng)?chuàng)建一個(gè) Lock 的 Condition 對(duì) 象必須用 newCondition()方法。在一個(gè) Lock 對(duì)象里面可以創(chuàng)建多個(gè) Condition 對(duì)象,線程可以注冊(cè)在指定的 Condition 對(duì)象中,從而可以有 選擇性地進(jìn)行線程通知,在線程調(diào)度上更加靈活。
- 在 Condition 中,用 await()替換 wait(),用 signal()替換 notify(), 用 signalAll()替換 notifyAll(),傳統(tǒng)線程的通信方式, Condition 都可以實(shí)現(xiàn)。 調(diào)用 Condition 對(duì)象中的方法時(shí),需要被包含在 lock()和 unlock()之間。
3. 管道實(shí)現(xiàn)線程間的通信。
- 實(shí)現(xiàn)方式:一個(gè)線程發(fā)送數(shù)據(jù)到輸出管道流,另一個(gè)線程從輸入管道流中 讀取數(shù)據(jù)。
- 基本流程:
- 1)創(chuàng)建管道輸出流 PipedOutputStream pos 和管道輸入流 PipedInputStream pis。
- 2)將 pos 和 pis 匹配,pos.connect(pis)。
- 3)將 pos 賦給信息輸入信息的線程,pis 賦給獲取信息的線程,就可以實(shí) 現(xiàn)線程間的通訊了。
- 缺點(diǎn):
- 1)管道流只能在兩個(gè)線程之間傳遞數(shù)據(jù)。
- 線程 consumer1 和 consumer2 同時(shí)從 pis 中 read 數(shù)據(jù),當(dāng)線程 producer 往管道流中寫(xiě)入一段數(shù)據(jù)(1,2,3,4,5,6)后,每一個(gè)時(shí)刻只有一個(gè) 線程能獲取到數(shù)據(jù),并不是兩個(gè)線程都能獲取到 producer 發(fā)送來(lái)的數(shù)據(jù),因 此一個(gè)管道流只能用于兩個(gè)線程間的通訊。
- 2)管道流只能實(shí)現(xiàn)單向發(fā)送,如果要兩個(gè)線程之間互通訊,則需要兩個(gè)管道流。
- 線程 producer 通過(guò)管道流向線程 consumer 發(fā)送數(shù)據(jù),如果線程 consumer 想給線程 producer 發(fā)送數(shù)據(jù),則需要新建另一個(gè)管道流 pos1 和 pis1,將 pos1 賦給 consumer1,將 pis1 賦給 producer1。
4. 使用 volatile 關(guān)鍵字
見(jiàn)上面部分。
四. 如何確保線程安全?
如果多個(gè)線程同時(shí)運(yùn)行某段代碼,如果每次運(yùn)行結(jié)果和單線程運(yùn)行的結(jié)果 是一樣的,而且其他變量的值也和預(yù)期的是一樣的,就是線程安全的。
Synchronized,Lock,原子類(lèi)(如 atomicinteger 等),同步容器、并 發(fā)容器、 阻塞隊(duì)列 、 同步輔助類(lèi)(比 如 CountDownLatch, Semaphore, CyclicBarrier)。
五. 多線程的優(yōu)點(diǎn)和缺點(diǎn)?
1. 優(yōu)點(diǎn):
- 充分利用 cpu,避免 cpu 空轉(zhuǎn)。
- 程序響應(yīng)更快。
2. 缺點(diǎn):
- 上下文切換的開(kāi)銷(xiāo)
- 當(dāng) CPU 從執(zhí)行一個(gè)線程切換到執(zhí)行另外一個(gè)線程的時(shí)候,它需要先存儲(chǔ)當(dāng) 前線程的本地的數(shù)據(jù),程序指針等,然后載入另一個(gè)線程的本地?cái)?shù)據(jù),程序指 針等,最后才開(kāi)始執(zhí)行。這種切換稱為“上下文切換”。CPU 會(huì)在一個(gè)上下文 中執(zhí)行一個(gè)線程,然后切換到另外一個(gè)上下文中執(zhí)行另外一個(gè)線程。上下文切 換并不廉價(jià)。如果沒(méi)有必要,應(yīng)該減少上下文切換的發(fā)生。
- 增加資源消耗
- 線程在運(yùn)行的時(shí)候需要從計(jì)算機(jī)里面得到一些資源。 除了 CPU,線程還需 要一些內(nèi)存來(lái)維持它本地的堆棧。它也需要占用操作系統(tǒng)中一些資源來(lái)管理線 程。
- 編程更復(fù)雜
- 在多線程訪問(wèn)共享數(shù)據(jù)的時(shí)候,要考慮線程安全問(wèn)題。
六. 寫(xiě)出 3 條你遵循的多線程最佳實(shí)踐。
七. 多線程的性能一定就優(yōu)于單線程嗎?
不一定,要看具體的任務(wù)以及計(jì)算機(jī)的配置。比如說(shuō):
- 對(duì)于單核 CPU,如果是 CPU 密集型任務(wù),如解壓文件,多線程的性能反 而不如單線程性能,因?yàn)榻鈮何募枰恢闭加?CPU 資源,如果采用多線程, 線程切換導(dǎo)致的開(kāi)銷(xiāo)反而會(huì)讓性能下降。如果是交互類(lèi)型的任務(wù),肯定是需要 使用多線程的。
- 對(duì)于多核 CPU,對(duì)于解壓文件來(lái)說(shuō),多線程肯定優(yōu)于單線程,因?yàn)槎鄠€(gè)線 程能夠更加充分利用每個(gè)核的資源。
八. 多線程中鎖的種類(lèi)。
1. 可重入鎖
ReentrantLock 和 synchronized 都是可重入鎖。
如果當(dāng)前線程已經(jīng)獲得了某個(gè)監(jiān)視器對(duì)象所持有的鎖,那么該線程在該方法 中調(diào)用另外一個(gè)同步方法也同樣持有該鎖。
比如:
public sychrnozied void test() { xxxxxx; test2();}public sychronized void test2() { yyyyy;}在上面代碼段中,執(zhí)行 test 方法需要獲得當(dāng)前對(duì)象作為監(jiān)視器的對(duì)象鎖, 但方法中又調(diào)用了 test2 的同步方法。
如果鎖是具有可重入性的話,那么該線程在調(diào)用 test2 時(shí)并不需要再次獲 得當(dāng)前對(duì)象的鎖,可以直接進(jìn)入 test2 方法進(jìn)行操作。
可重入鎖最大的作用是避免死鎖。如果鎖是不具有可重入性的話,那么該 線程在調(diào)用 test2 前會(huì)等待當(dāng)前對(duì)象鎖的釋放,實(shí)際上該對(duì)象鎖已被當(dāng)前線程 所持有,不可能再次獲得,那么線程在調(diào)用同步方法、含有鎖的方法時(shí)就會(huì)產(chǎn) 生死鎖。
2. 可中斷鎖
顧名思義,就是可以響應(yīng)中斷的鎖。
在 Java 中,synchronized 不是可中斷鎖,而 Lock 是可中斷鎖。 lockInterruptibly()的用法已經(jīng)體現(xiàn)了 Lock 的可中斷性。如果某一線程 A 正 在執(zhí)行鎖中的代碼,另一線程 B 正在等待獲取該鎖,可能由于等待時(shí)間過(guò)長(zhǎng), 線程 B 不想等待了,想先處理其他事情,我們可以讓它中斷自己或者在別的線 程中斷它,這種就是可中斷鎖。
3. 公平鎖
在 Java 中,synchronized 就是非公平鎖,它無(wú)法保證等待的線程獲取鎖 的順序。而對(duì)于 ReentrantLock 和 ReentrantReadWriteLock,它默認(rèn)情況 下是非公平鎖,但是可以設(shè)置為公平鎖。
公平鎖即盡量以請(qǐng)求鎖的順序來(lái)獲取鎖。比如同是有多個(gè)線程在等待一個(gè) 鎖,當(dāng)這個(gè)鎖被釋放時(shí),等待時(shí)間最久的線程(最先請(qǐng)求的線程)會(huì)獲得該鎖, 這種就是公平鎖。
4. 讀寫(xiě)鎖
正因?yàn)橛辛俗x寫(xiě)鎖,才使得多個(gè)線程之間的讀操作不會(huì)發(fā)生沖突。 ReadWriteLock 就是讀寫(xiě)鎖,它是一個(gè)接口,ReentrantReadWriteLock 實(shí) 現(xiàn)了這個(gè)接口??梢酝ㄟ^(guò) readLock()獲取讀鎖,通過(guò) writeLock()獲取寫(xiě)鎖。
九. 鎖優(yōu)化
1. 自旋鎖
- 為了讓線程等待,讓線程執(zhí)行一個(gè)忙循環(huán)(自旋)。需要物理機(jī)器有一個(gè)以 上的處理器。自旋等待雖然避免了線程切換的開(kāi)銷(xiāo),但它是要占用處理器時(shí)間 的,所以如果鎖被占用的時(shí)間很短,自旋等待的效果就會(huì)非常好,反之自旋的 線程只會(huì)白白消耗處理器資源。自旋次數(shù)的默認(rèn)值是 10 次,可以使用參數(shù) -XX:PreBlockSpin 來(lái)更改。
- 自適應(yīng)自旋鎖:自旋的時(shí)間不再固定,而是由前一次在同一個(gè)鎖上的自旋 時(shí)間及鎖的擁有者的狀態(tài)來(lái)決定。
2. 鎖清除
- 指虛擬機(jī)即時(shí)編譯器在運(yùn)行時(shí),對(duì)一些代碼上要求同步,但是被檢測(cè)到不 可能存在共享數(shù)據(jù)競(jìng)爭(zhēng)的鎖進(jìn)行清除(逃逸分析技術(shù):在堆上的所有數(shù)據(jù)都不會(huì) 逃逸出去被其它線程訪問(wèn)到,可以把它們當(dāng)成棧上數(shù)據(jù)對(duì)待)。
3. 鎖粗化
如果虛擬機(jī)探測(cè)到有一串零碎的操作都對(duì)同一個(gè)對(duì)象加鎖,將會(huì)把加鎖同 步的范圍擴(kuò)展到整個(gè)操作序列的外部。
4. 輕量級(jí)鎖
- 在代碼進(jìn)入同步塊時(shí),如果此同步對(duì)象沒(méi)有被鎖定,虛擬機(jī)首先將在當(dāng)前 線程的棧幀中建立一個(gè)名為鎖記錄(Lock Record)的空間,用于存儲(chǔ)所對(duì)象目 前的 Mark Word 的拷貝。然后虛擬機(jī)將使用 CAS 操作嘗試將對(duì)象的 Mark Word 更新為執(zhí)行 Lock Record 的指針。如果成功,那么這個(gè)線程就擁有了該 對(duì)象的鎖。如果更新操作失敗,虛擬機(jī)首先會(huì)檢查對(duì)象的 Mark Word 是否指 向當(dāng)前線程的棧幀,如果是就說(shuō)明當(dāng)前線程已經(jīng)擁有了這個(gè)對(duì)象的鎖,否則說(shuō) 明這個(gè)對(duì)象已經(jīng)被其它線程搶占。如果有兩條以上的線程爭(zhēng)用同一個(gè)鎖,那輕 量級(jí)鎖就不再有效,要膨脹為重量級(jí)鎖。
- 解鎖過(guò)程:如果對(duì)象的 Mark Word 仍然指向著線程的鎖記錄,那就用 CAS 操作把對(duì)象當(dāng)前的 Mark Word 和和線程中復(fù)制的 Displaced Mark Word 替 換回來(lái),如果替換成功,整個(gè)過(guò)程就完成。如果失敗,說(shuō)明有其他線程嘗試過(guò) 獲取該鎖,那就要在釋放鎖的同時(shí),喚醒被掛起的線程。
- 輕量級(jí)鎖的依據(jù):對(duì)于絕大部分的鎖,在整個(gè)同步周期內(nèi)都是不存在競(jìng)爭(zhēng) 的。
- 傳統(tǒng)鎖(重量級(jí)鎖)使用操作系統(tǒng)互斥量來(lái)實(shí)現(xiàn)的。
HotSpot 虛擬機(jī)的對(duì)象的內(nèi)存布局:對(duì)象頭(Object Header)分為兩部分信息嗎,第 一部分(Mark Word)用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù),另一個(gè)部分用于存儲(chǔ)指向方法區(qū) 對(duì)象數(shù)據(jù)類(lèi)型的指針,如果是數(shù)組的話,還會(huì)由一個(gè)額外的部分用于存儲(chǔ)數(shù)組的長(zhǎng)度。
32 位 HotSpot 虛擬機(jī)中對(duì)象未被鎖定的狀態(tài)下, Mark Word 的 32 個(gè) Bits 空間中 25 位 用于存儲(chǔ)對(duì)象哈希碼,4 位存儲(chǔ)對(duì)象分代年齡,2 位存儲(chǔ)鎖標(biāo)志位,1 位固定為 0。
HotSpot 虛擬機(jī)對(duì)象頭 Mark Word
存儲(chǔ)內(nèi)容標(biāo)志位狀態(tài)對(duì)象哈希碼、對(duì)象分代年齡01未鎖定指向鎖記錄的指針00輕量級(jí)鎖定指向重量級(jí)鎖的指針10膨脹(重量級(jí)鎖)空,不記錄信息11GC 標(biāo)記偏向線程 ID,偏向時(shí)間戳、對(duì)象分代年齡01可偏向
5. 偏向鎖
- 目的是消除在無(wú)競(jìng)爭(zhēng)情況下的同步原語(yǔ),進(jìn)一步提高程序的運(yùn)行性能。鎖 會(huì)偏向第一個(gè)獲得它的線程,如果在接下來(lái)的執(zhí)行過(guò)程中,該鎖沒(méi)有被其它線 程獲取,則持有鎖的線程將永遠(yuǎn)不需要再進(jìn)行同步。
- 當(dāng)鎖第一次被線程獲取的時(shí)候,虛擬機(jī)將會(huì)把對(duì)象頭中的標(biāo)志位設(shè)為 01, 同時(shí)使用 CAS 操作把獲取到這個(gè)鎖的線程的 ID 記錄在對(duì)象的 Mark Word 之 中,如果成功,持有偏向鎖的線程以后每次進(jìn)入這個(gè)鎖相關(guān)的同步塊時(shí),都可 以不進(jìn)行任何同步操作。
- 當(dāng)有另一個(gè)線程去嘗試獲取這個(gè)鎖時(shí),偏向模式就宣告結(jié)束。根據(jù)鎖對(duì)象 目前是否處于被鎖定的狀態(tài),撤銷(xiāo)偏向后恢復(fù)到未鎖定或輕量級(jí)鎖定狀態(tài)。
十. wait()和 sleep()的區(qū)別。
特別注意: sleep 和 wait 必須捕獲異常(Thread.sleep()和 Object.wait() 都會(huì)拋出 InterruptedException), notify 和 notifyAll 不需要捕獲異常。
十一. Java 中 interrupted() 和 isInterrupted()方法的區(qū)別?
二個(gè)方法都是判斷線程是否停止的方法。
十二. Java 創(chuàng)建線程之后,直接調(diào)用 start()方法和 run()的區(qū)別 ?
十三. 什么是線程的上下文切換?
對(duì)于單核 CPU,CPU 在一個(gè)時(shí)刻只能運(yùn)行一個(gè)線程,當(dāng)在運(yùn)行一個(gè)線程的 過(guò)程中轉(zhuǎn)去運(yùn)行另外一個(gè)線程,這個(gè)叫做線程上下文切換(對(duì)于進(jìn)程也是類(lèi)似)。
線程上下文切換過(guò)程中會(huì)記錄程序計(jì)數(shù)器、CPU 寄存器的狀態(tài)等數(shù)據(jù)。
雖然多線程可以使得任務(wù)執(zhí)行的效率得到提升,但是由于在線程切換時(shí)同 樣會(huì)帶來(lái)一定的開(kāi)銷(xiāo)代價(jià),并且多個(gè)線程會(huì)導(dǎo)致系統(tǒng)資源占用的增加,所以在 進(jìn)行多線程編程時(shí)要注意這些因素。
十四. 怎么檢測(cè)一個(gè)線程是否擁有鎖?
在 java.lang.Thread 中有一個(gè)方法叫 holdsLock(Object obj),它返回 true,如果當(dāng)且僅當(dāng)當(dāng)前線程擁有某個(gè)具體對(duì)象的鎖。
十五. 用戶線程和守護(hù)線程有什么區(qū)別?
當(dāng)我們?cè)?Java 程序中創(chuàng)建一個(gè)線程,它就被稱為用戶線程。將一個(gè)用戶線 程設(shè)置為守護(hù)線程的方法就是在調(diào)用start()方法之前, 調(diào)用對(duì)象的 setDamon(true)方法。一個(gè)守護(hù)線程是在后臺(tái)執(zhí)行并且不會(huì)阻止 JVM 終止的 線程,守護(hù)線程的作用是為其他線程的運(yùn)行提供便利服務(wù)。當(dāng)沒(méi)有用戶線程在 運(yùn)行的時(shí)候, JVM 關(guān)閉程序并且退出。一個(gè)守護(hù)線程創(chuàng)建的子線程依然是守護(hù) 線程。
守護(hù)線程的一個(gè)典型例子就是垃圾回收器。
十六. 什么是線程調(diào)度器?
線程調(diào)度器是一個(gè)操作系統(tǒng)服務(wù),它負(fù)責(zé)為 Runnable 狀態(tài)的線程分配 CPU 時(shí)間。一旦我們創(chuàng)建一個(gè)線程并啟動(dòng)它,它的執(zhí)行便依賴于線程調(diào)度器的 實(shí)現(xiàn)。
十七. 線程的狀態(tài)。
版本 1.
在 Java 當(dāng)中,線程通常都有五種狀態(tài),創(chuàng)建、就緒、運(yùn)行、阻塞和死亡。
- 第一是創(chuàng)建狀態(tài)。在生成線程對(duì)象,并沒(méi)有調(diào)用該對(duì)象的 start 方法,這是 線程處于創(chuàng)建狀態(tài)。
- 第二是就緒狀態(tài)。當(dāng)調(diào)用了線程對(duì)象的 start 方法之后,該線程就進(jìn)入了就 緒狀態(tài),但是此時(shí)線程調(diào)度程序還沒(méi)有把該線程設(shè)置為當(dāng)前線程,此時(shí)處于就 緒狀態(tài)。在線程運(yùn)行之后,從等待或者睡眠中回來(lái)之后,也會(huì)處于就緒狀態(tài)。
- 第三是運(yùn)行狀態(tài)。線程調(diào)度程序?qū)⑻幱诰途w狀態(tài)的線程設(shè)置為當(dāng)前線程, 此時(shí)線程就進(jìn)入了運(yùn)行狀態(tài),開(kāi)始運(yùn)行 run 函數(shù)當(dāng)中的代碼。
- 第四是阻塞狀態(tài)。線程正在運(yùn)行的時(shí)候,被暫停,通常是為了等待某個(gè)事 件的發(fā)生(比如說(shuō)某項(xiàng)資源就緒)之后再繼續(xù)運(yùn)行。sleep,wait 等方法都可以導(dǎo) 致線程阻塞。
- 第五是死亡狀態(tài)。如果一個(gè)線程的 run 方法執(zhí)行結(jié)束或者異常中斷后,該 線程就會(huì)死亡。對(duì)于已經(jīng)死亡的線程,無(wú)法再使用 start 方法令其進(jìn)入就緒。
版本 2.
一般來(lái)說(shuō),線程包括以下這幾個(gè)狀態(tài):創(chuàng)建(new)、就緒(runnable)、運(yùn) 行(running)、阻塞(blocked)、timed_waiting、waiting、消亡(dead)。
十八. 有三個(gè)線程 T1,T2,T3,怎么確保它們按順序執(zhí)行?
join()方法。
十九. 在一個(gè)主線程中,要求有大量子線程執(zhí)行完之后,主線程才執(zhí)行完成。多種方式,考慮效率。
1. 在主函數(shù)中使用 join()方法。
t1.start();t2.start();t3.start();t1.join(); //不會(huì)導(dǎo)致 t1 和 t2 和 t3 的順序執(zhí)行 t2.join();t3.join();System.out.println("Main finished");2. CountDownLatch,一個(gè)同步輔助類(lèi),在完成一組正在其他線程中執(zhí)行 的操作之前,它允許一個(gè)或多個(gè)線程一直等待。
public class WithLatch { public static void main(String[] args) { CountDownLatch latch = new CountDownLatch(3); for (int i = 0; i < 3; i++) { new ChildThead(i, latch).start(); } try { latch.await(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Main finished"); } static class ChildThead extends Thread { private int id = -1; private CountDownLatch latch = null; public ChildThead(int id, CountDownLatch latch) { this.id = id; this.latch = latch; } public void run() { try { Thread.sleep(Math.abs(new Random().nextint(5000))); System.out.println(String.format("Child Thread %dfinished總結(jié)
以上是生活随笔為你收集整理的多个线程访问统一对象的不同方法_不会多线程还想进BAT?精选19道多线程面试题,有答案边看边学...的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 855和845差距
- 下一篇: 基于控制台应用程序的生命游戏java_J