2020年Java多執行緒與並發系列22道高頻面試題(附思維導圖和答案解析)

前言

現在不管是大公司還是小公司,去面試都會問到多執行緒與並發編程的知識,大家面試的時候這方面的知識一定要提前做好儲備。

關於多執行緒與並發的知識總結了一個思維導圖,分享給大家

1、Java中實現多執行緒有幾種方法

(1)繼承Thread類;
(2)實現Runnable介面;
(3)實現Callable介面通過FutureTask包裝器來創建Thread執行緒;
(4)使用ExecutorService、Callable、Future實現有返回結果的多執行緒(也就是使用了ExecutorService來管理前面的三種方式)。

2、如何停止一個正在運行的執行緒

(1)使用退出標誌,使執行緒正常退出,也就是當run方法完成後執行緒終止。
(2)使用stop方法強行終止,但是不推薦這個方法,因為stop和suspend及resume一樣都是過期作廢的方法。
(3)使用interrupt方法中斷執行緒。
class MyThread extends Thread {  	volatile Boolean stop = false;  	public void run() {  		while (!stop) {  			System.out.println(getName() + " is running");  			try {  				sleep(1000);  			}  			catch (InterruptedException e) {  				System.out.println("week up from blcok...");  				stop = true;  				// 在異常處理程式碼中修改共享變數的狀態  			}  		}  		System.out.println(getName() + " is exiting...");  	}  }  class InterruptThreadDemo3 {  	public static void main(String[] args) throws InterruptedException {  		MyThread m1 = new MyThread();  		System.out.println("Starting thread...");  		m1.start();  		Thread.sleep(3000);  		m1.interrupt();  		// 阻塞時退出阻塞狀態  		Thread.sleep(3000);  		// 主執行緒休眠3秒以便觀察執行緒m1的中斷情況  		System.out.println("Stopping application...");  	}  }複製程式碼

3、notify()和notifyAll()有什麼區別?

notify可能會導致死鎖,而notifyAll則不會
任何時候只有一個執行緒可以獲得鎖,也就是說只有一個執行緒可以運行synchronized 中的程式碼使用notifyall,可以喚醒所有處於wait狀態的執行緒,使其重新進入鎖的爭奪隊列中,而notify只能喚醒一個。
wait() 應配合while循環使用,不應使用if,務必在wait()調用前後都檢查條件,如果不滿足,必須調用notify()喚醒另外的執行緒來處理,自己繼續wait()直至條件滿足再往下執行。
notify() 是對notifyAll()的一個優化,但它有很精確的應用場景,並且要求正確使用。不然可能導致死鎖。正確的場景應該是 WaitSet中等待的是相同的條件,喚醒任一個都能正確處理接下來的事項,如果喚醒的執行緒無法正確處理,務必確保繼續notify()下一個執行緒,並且自身需要重新回到WaitSet中。

4、sleep()和wait() 有什麼區別?

對於sleep()方法,我們首先要知道該方法是屬於Thread類中的。而wait()方法,則是屬於Object類中
的。
sleep()方法導致了程式暫停執行指定的時間,讓出cpu該其他執行緒,但是他的監控狀態依然保持者,當指定的時間到了又會自動恢復運行狀態。在調用sleep()方法的過程中,執行緒不會釋放對象鎖。
當調用wait()方法的時候,執行緒會放棄對象鎖,進入等待此對象的等待鎖定池,只有針對此對象調用notify()方法後本執行緒才進入對象鎖定池準備,獲取對象鎖進入運行狀態。

5、volatile 是什麼?可以保證有序性嗎?

一旦一個共享變數(類的成員變數、類的靜態成員變數)被volatile修飾之後,那麼就具備了兩層語義:
(1)保證了不同執行緒對這個變數進行操作時的可見性,即一個執行緒修改了某個變數的值,這新值對其他執行緒來說是立即可見的,volatile關鍵字會強制將修改的值立即寫入主存。
(2)禁止進行指令重排序。
volatile 不是原子性操作
什麼叫保證部分有序性?
當程式執行到volatile變數的讀操作或者寫操作時,在其前面的操作的更改肯定全部已經進行,且結果已經對後面的操作可見;在其後面的操作肯定還沒有進行;
x = 2;//語句1  y = 0;//語句2  flag = true;//語句3  x = 4;//語句4  y = -1;//語句5複製程式碼
由於flag變數為volatile變數,那麼在進行指令重排序的過程的時候,不會將語句3放到語句1、語句2前面,也不會講語句3放到語句4、語句5後面。但是要注意語句1和語句2的順序、語句4和語句5的順序是不作任何保證的。
使用 Volatile 一般用於 狀態標記量 和 單例模式的雙檢鎖

6、Thread 類中的start() 和 run() 方法有什麼區別?

start()方法被用來啟動新創建的執行緒,而且start()內部調用了run()方法,這和直接調用run()方法的效果不一樣。當你調用run()方法的時候,只會是在原來的執行緒中調用,沒有新的執行緒啟動,start()方法才會啟動新執行緒。

7、為什麼wait, notify 和 notifyAll這些方法不在thread類裡面?

明顯的原因是JAVA提供的鎖是對象級的而不是執行緒級的,每個對象都有鎖,通過執行緒獲得。如果執行緒需要等待某些鎖那麼調用對象中的wait()方法就有意義了。如果wait()方法定義在Thread類中,執行緒正在等待的是哪個鎖就不明顯了。簡單的說,由於wait,notify和notifyAll都是鎖級別的操作,所以把他們定義在Object類中因為鎖屬於對象。

8、為什麼wait和notify方法要在同步塊中調用?

(1)只有在調用執行緒擁有某個對象的獨佔鎖時,才能夠調用該對象的wait(),notify()和notifyAll()方法。
(2)如果你不這麼做,你的程式碼會拋出IllegalMonitorStateException異常。
(3)還有一個原因是為了避免wait和notify之間產生競態條件。
wait()方法強制當前執行緒釋放對象鎖。這意味著在調用某對象的wait()方法之前,當前執行緒必須已經獲得該對象的鎖。因此,執行緒必須在某個對象的同步方法或同步程式碼塊中才能調用該對象的wait()方法。
在調用對象的notify()和notifyAll()方法之前,調用執行緒必須已經得到該對象的鎖。因此,必須在某個對象的同步方法或同步程式碼塊中才能調用該對象的notify()或notifyAll()方法。
調用wait()方法的原因通常是,調用執行緒希望某個特殊的狀態(或變數)被設置之後再繼續執行。調用notify()或notifyAll()方法的原因通常是,調用執行緒希望告訴其他等待中的執行緒:”特殊狀態已經被設置”。這個狀態作為執行緒間通訊的通道,它必須是一個可變的共享狀態(或變數)。

9、Java中interrupted 和 isInterruptedd方法的區別?

interrupted() 和 isInterrupted()的主要區別是前者會將中斷狀態清除而後者不會。Java多執行緒的中斷機制是用內部標識來實現的,調用Thread.interrupt()來中斷一個執行緒就會設置中斷標識為true。當中斷執行緒調用靜態方法Thread.interrupted()來檢查中斷狀態時,中斷狀態會被清零。而非靜態方法isInterrupted()用來查詢其它執行緒的中斷狀態且不會改變中斷狀態標識。簡單的說就是任何拋出InterruptedException異常的方法都會將中斷狀態清零。無論如何,一個執行緒的中斷狀態有有可能被其它執行緒調用中斷來改變。

10、Java中synchronized 和 ReentrantLock 有什麼不同?

相似點:
這兩種同步方式有很多相似之處,它們都是加鎖方式同步,而且都是阻塞式的同步,也就是說當如果一個執行緒獲得了對象鎖,進入了同步塊,其他訪問該同步塊的執行緒都必須阻塞在同步塊外面等待,而進行執行緒阻塞和喚醒的代價是比較高的。
區別:
這兩種方式最大區別就是對於Synchronized來說,它是java語言的關鍵字,是原生語法層面的互斥,需要jvm實現。而ReentrantLock它是JDK 1.5之後提供的API層面的互斥鎖,需要lock()和unlock()方法配合try/finally語句塊來完成。
Synchronized進過編譯,會在同步塊的前後分別形成monitorenter和monitorexit這個兩個位元組碼指令。在執行monitorenter指令時,首先要嘗試獲取對象鎖。如果這個對象沒被鎖定,或者當前執行緒已經擁有了那個對象鎖,把鎖的計算器加1,相應的,在執行monitorexit指令時會將鎖計算器就減1,當計算器為0時,鎖就被釋放了。如果獲取對象鎖失敗,那當前執行緒就要阻塞,直到對象鎖被另一個執行緒釋放為止。
由於ReentrantLock是java.util.concurrent包下提供的一套互斥鎖,相比Synchronized,ReentrantLock類提供了一些高級功能,主要有以下3項:
(1)等待可中斷,持有鎖的執行緒長期不釋放的時候,正在等待的執行緒可以選擇放棄等待,這相當於Synchronized來說可以避免出現死鎖的情況。
(2)公平鎖,多個執行緒等待同一個鎖時,必須按照申請鎖的時間順序獲得鎖,Synchronized鎖非公平鎖,ReentrantLock默認的構造函數是創建的非公平鎖,可以通過參數true設為公平鎖,但公平鎖表現的性能不是很好。
(3)鎖綁定多個條件,一個ReentrantLock對象可以同時綁定對個對象。

11、有三個執行緒T1,T2,T3,如何保證順序執行?

在多執行緒中有多種方法讓執行緒按特定順序執行,你可以用執行緒類的join()方法在一個執行緒中啟動另一個執行緒,另外一個執行緒完成該執行緒繼續執行。為了確保三個執行緒的順序你應該先啟動最後一個(T3調用T2,T2調用T1),這樣T1就會先完成而T3最後完成。
實際上先啟動三個執行緒中哪一個都行,因為在每個執行緒的run方法中用join方法限定了三個執行緒的執行順序。
public class JoinTest2 {  	// 1.現在有T1、T2、T3三個執行緒,你怎樣保證T2在T1執行完後執行,T3在T2執行完後執行  	public static void main(String[] args) {  		final Thread t1 = new Thread(new Runnable() {  			@Override  			public void run() {  				System.out.println("t1");  			}  		}  		);  		@Override  		public void run() {  			try {  				// 引用t1執行緒,等待t1執行緒執行完  				t1.join();  			}  			catch (InterruptedException e) {  				e.printStackTrace();  			}  			System.out.println("t2");  		}  	}  	);  	Thread t3 = new Thread(new Runnable() {  		@Override  		public void run() {  			try {  				// 引用t2執行緒,等待t2執行緒執行完  				t2.join();  			}  			catch (InterruptedException e) {  				e.printStackTrace();  			}  			System.out.println("t3");  		}  	}  	);  	t3.start();  	//這裡三個執行緒的啟動順序可以任意,大家可以試下!  	t2.start();  	t1.start();  }  }複製程式碼

12、SynchronizedMap和ConcurrentHashMap有什麼區別?

SynchronizedMap()和Hashtable一樣,實現上在調用map所有方法時,都對整個map進行同步。而ConcurrentHashMap的實現卻更加精細,它對map中的所有桶加了鎖。所以,只要有一個執行緒訪問map,其他執行緒就無法進入map,而如果一個執行緒在訪問ConcurrentHashMap某個桶時,其他執行緒,仍然可以對map執行某些操作。
所以,ConcurrentHashMap在性能以及安全性方面,明顯比Collections.synchronizedMap()更加有優勢。同時,同步操作精確控制到桶,這樣,即使在遍歷map時,如果其他執行緒試圖對map進行數據修改,也不會拋出ConcurrentModificationException。

13、什麼是執行緒安全

執行緒安全就是說多執行緒訪問同一程式碼,不會產生不確定的結果。
在多執行緒環境中,當各執行緒不共享數據的時候,即都是私有(private)成員,那麼一定是執行緒安全的。但這種情況並不多見,在多數情況下需要共享數據,這時就需要進行適當的同步控制了。
執行緒安全一般都涉及到synchronized, 就是一段程式碼同時只能有一個執行緒來操作 不然中間過程可能會產生不可預製的結果。
如果你的程式碼所在的進程中有多個執行緒在同時運行,而這些執行緒可能會同時運行這段程式碼。如果每次運行的ArrayList不是執行緒安全的。

14、Thread類中的yield方法有什麼作用?

Yield方法可以暫停當前正在執行的執行緒對象,讓其它有相同優先順序的執行緒執行。它是一個靜態方法而且只保證當前執行緒放棄CPU佔用而不能保證使其它執行緒一定能佔用CPU,執行yield()的執行緒有可能在進入到暫停狀態後馬上又被執行。

15、Java執行緒池中submit() 和 execute()方法有什麼區別?

兩個方法都可以向執行緒池提交任務,execute()方法的返回類型是void,它定義在Executor介面中, 而submit()方法可以返回持有計算結果的Future對象,它定義在ExecutorService介面中,它擴展了Executor介面,其它執行緒池類像ThreadPoolExecutor和ScheduledThreadPoolExecutor都有這些方法。

16、說一說自己對於 synchronized 關鍵字的了解

synchronized關鍵字解決的是多個執行緒之間訪問資源的同步性,synchronized關鍵字可以保證被它修飾的方法或者程式碼塊在任意時刻只能有一個執行緒執行。
另外,在 Java 早期版本中,synchronized屬於重量級鎖,效率低下,因為監視器鎖(monitor)是依賴於底層的作業系統的 Mutex Lock 來實現的,Java 的執行緒是映射到作業系統的原生執行緒之上的。如果要掛起或者喚醒一個執行緒,都需要作業系統幫忙完成,而作業系統實現執行緒之間的切換時需要從用戶態轉換到內核態,這個狀態之間的轉換需要相對比較長的時間,時間成本相對較高,這也是為什麼早期的synchronized 效率低的原因。慶幸的是在 Java 6 之後 Java 官方對從 JVM 層面對synchronized 較大優化,所以現在的 synchronized 鎖效率也優化得很不錯了。JDK1.6對鎖的實現引入了大量的優化,如自旋鎖、適應性自旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級鎖等技術來減少鎖操作的開銷。

17、說說自己是怎麼使用 synchronized 關鍵字,在項目中用到了嗎synchronized關鍵字最主要的三種使用方式:

(1)修飾實例方法: 作用於當前對象實例加鎖,進入同步程式碼前要獲得當前對象實例的鎖
(2)修飾靜態方法: 也就是給當前類加鎖,會作用於類的所有對象實例,因為靜態成員不屬於任何一個實例對象,是類成員( static 表明這是該類的一個靜態資源,不管new了多少個對象,只有一份)。所以如果一個執行緒A調用一個實例對象的非靜態 synchronized 方法,而執行緒B需要調用這個實例對象所屬類的靜態 synchronized 方法,是允許的,不會發生互斥現象,因為訪問靜態 synchronized 方法佔用的鎖是當前類的鎖,而訪問非靜態 synchronized 方法佔用的鎖是當前實例對象鎖。
(3)修飾程式碼塊: 指定加鎖對象,對給定對象加鎖,進入同步程式碼庫前要獲得給定對象的鎖。
總結: synchronized 關鍵字加到 static 靜態方法和 synchronized(class)程式碼塊上都是是給 Class 類上鎖。synchronized 關鍵字加到實例方法上是給對象實例上鎖。盡量不要使用 synchronized(String a) 因為JVM中,字元串常量池具有快取功能!

18、什麼是執行緒安全?Vector是一個執行緒安全類嗎?

如果你的程式碼所在的進程中有多個執行緒在同時運行,而這些執行緒可能會同時運行這段程式碼。如果每次運
行結果和單執行緒運行的結果是一樣的,而且其他的變數 的值也和預期的是一樣的,就是執行緒安全的。

19、 volatile關鍵字的作用?

一旦一個共享變數(類的成員變數、類的靜態成員變數)被volatile修飾之後,那麼就具備了兩層語
義:
(1)保證了不同執行緒對這個變數進行操作時的可見性,即一個執行緒修改了某個變數的值,這新值對其他執行緒來說是立即可見的。
(2)禁止進行指令重排序。
(3)volatile本質是在告訴jvm當前變數在暫存器(工作記憶體)中的值是不確定的,需要從主存中讀取;synchronized則是鎖定當前變數,只有當前執行緒可以訪問該變數,其他執行緒被阻塞住。
(4)volatile僅能使用在變數級別;synchronized則可以使用在變數、方法、和類級別的。
(5)volatile僅能實現變數的修改可見性,並不能保證原子性;synchronized則可以保證變數的修改可見性和原子性。
(6)volatile不會造成執行緒的阻塞;synchronized可能會造成執行緒的阻塞。
(7)volatile標記的變數不會被編譯器優化;synchronized標記的變數可以被編譯器優化。

20、常用的執行緒池有哪些?

(1)newSingleThreadExecutor:創建一個單執行緒的執行緒池,此執行緒池保證所有任務的執行順序按照任務的提交順序執行。
(2)newFixedThreadPool:創建固定大小的執行緒池,每次提交一個任務就創建一個執行緒,直到執行緒達到執行緒池的最大大小。
(3)newCachedThreadPool:創建一個可快取的執行緒池,此執行緒池不會對執行緒池大小做限制,執行緒池大小完全依賴於作業系統(或者說JVM)能夠創建的最大執行緒大小。
(4)newScheduledThreadPool:創建一個大小無限的執行緒池,此執行緒池支援定時以及周期性執行任務的需求。
(5)newSingleThreadExecutor:創建一個單執行緒的執行緒池。此執行緒池支援定時以及周期性執行任務的需求。

21、簡述一下你對執行緒池的理解

(如果問到了這樣的問題,可以展開的說一下執行緒池如何用、執行緒池的好處、執行緒池的啟動策略)合理利用執行緒池能夠帶來三個好處。
(1)降低資源消耗。通過重複利用已創建的執行緒降低執行緒創建和銷毀造成的消耗。
(2)提高響應速度。當任務到達時,任務可以不需要等到執行緒創建就能立即執行。
(3)提高執行緒的可管理性。執行緒是稀缺資源,如果無限制的創建,不僅會消耗系統資源,還會降低系統的穩定性,使用執行緒池可以進行統一的分配,調優和監控。

22、Java程式是如何執行的

我們日常的工作中都使用開發工具(IntelliJ IDEA 或 Eclipse 等)可以很方便的調試程式,或者是通過打包工具把項目打包成 jar 包或者 war 包,放入 Tomcat 等 Web 容器中就可以正常運行了
(1)先把 Java 程式碼編譯成位元組碼,也就是把 .java 類型的文件編譯成 .class 類型的文件。這個過程的大致執行流程:Java 源程式碼 -> 詞法分析器 -> 語法分析器 -> 語義分析器 -> 字元碼生成器 -> 最終生成位元組碼,其中任何一個節點執行失敗就會造成編譯失敗;
(2)把 class 文件放置到 Java 虛擬機,這個虛擬機通常指的是 Oracle 官方自帶的 Hotspot JVM;
(3)Java 虛擬機使用類載入器(Class Loader)裝載 class 文件;
(4)類載入完成之後,會進行位元組碼效驗,位元組碼效驗通過之後 JVM 解釋器會把位元組碼翻譯成機器碼交由作業系統執行。但不是所有程式碼都是解釋執行的,JVM 對此做了優化,比如,以 Hotspot 虛擬機來說,它本身提供了 JIT(Just In Time)也就是我們通常所說的動態編譯器,它能夠在運行時將熱點程式碼編譯為機器碼,這個時候位元組碼就變成了編譯執行。Java 程式執行流程圖如下:
 

最後

歡迎大家一起交流,喜歡文章記得關注我點個贊喲,感謝支援!