史上最全的Java併發面試題(珍藏版)

多執行緒
java中有幾種方法可以實現一個執行緒?
- 繼承Thread類;
- 實現Runnable介面;
- 實現Callable介面通過FutureTask包裝器來建立Thread執行緒;
- 使用ExecutorService、Callable、Future實現有返回結果的多執行緒(也就是使用了ExecutorService來管理前面的三種方式)。
如何停止一個正在執行的執行緒?
- 使用退出標誌,使執行緒正常退出,也就是當run方法完成後執行緒終止。
- 使用stop方法強行終止,但是不推薦這個方法,因為stop和suspend及resume一樣都是過期作廢的方法。
- 使用interrupt方法中斷執行緒。
notify()和notifyAll()有什麼區別?
如果執行緒呼叫了物件的 wait()方法,那麼執行緒便會處於該物件的等待池中,等待池中的執行緒不會去競爭該物件的鎖。
當有執行緒呼叫了物件的 notifyAll()方法(喚醒所有 wait 執行緒)或 notify()方法(只隨機喚醒一個 wait 執行緒),被喚醒的的執行緒便會進入該物件的鎖池中,鎖池中的執行緒會去競爭該物件鎖。也就是說,呼叫了notify後只要一個執行緒會由等待池進入鎖池,而notifyAll會將該物件等待池內的所有執行緒移動到鎖池中,等待鎖競爭。
優先順序高的執行緒競爭到物件鎖的概率大,假若某執行緒沒有競爭到該物件鎖,它還會留在鎖池中,唯有執行緒再次呼叫 wait()方法,它才會重新回到等待池中。而競爭到物件鎖的執行緒則繼續往下執行,直到執行完了 synchronized 程式碼塊,它會釋放掉該物件鎖,這時鎖池中的執行緒會繼續競爭該物件鎖。
sleep()和 wait()有什麼區別?
對於sleep()方法,我們首先要知道該方法是屬於Thread類中的。而wait()方法,則是屬於Object類中的。
sleep()方法導致了程式暫停執行指定的時間,讓出cpu該其他執行緒,但是他的監控狀態依然保持者,當指定的時間到了又會自動恢復執行狀態。在呼叫sleep()方法的過程中,執行緒不會釋放物件鎖。
當呼叫wait()方法的時候,執行緒會放棄物件鎖,進入等待此物件的等待鎖定池,只有針對此物件呼叫notify()方法後本執行緒才進入物件鎖定池準備,獲取物件鎖進入執行狀態。
什麼是Daemon執行緒?它有什麼意義?
Java語言自己可以建立兩種程序“使用者執行緒”和“守護執行緒”
- 使用者執行緒:就是我們平時建立的普通執行緒.
- 守護執行緒:主要是用來服務使用者執行緒.
Daemon就是守護執行緒,他的意義是:
只要當前JVM例項中尚存在任何一個非守護執行緒沒有結束,守護執行緒就全部工作;只有當最後一個非守護執行緒結束時,守護執行緒隨著JVM一同結束工作。
Daemon的作用是為其他執行緒的執行提供便利服務,守護執行緒最典型的應用就是 GC (垃圾回收器),它就是一個很稱職的守護者。
鎖
什麼是可重入鎖(ReentrantLock)?
執行緒可以進入任何一個它已經擁有的鎖所同步著的程式碼塊。
程式碼設計如下:
public class Lock{ Boolean isLocked = false; ThreadlockedBy = null; int lockedCount = 0; public synchronized void lock() throws InterruptedException{ Thread thread = Thread.currentThread(); while(isLocked && lockedBy != thread){ wait(); } isLocked = true; lockedCount++; lockedBy = thread; } public synchronized void unlock(){ if(Thread.currentThread() == this.lockedBy){ lockedCount--; if(lockedCount == 0){ isLocked = false; notify(); } } } }
當一個執行緒進入某個物件的一個synchronized的例項方法後,其它執行緒是否可進入此物件的其它方法?
如果其他方法前加了synchronized關鍵字,就不能,如果沒加synchronized,則能夠進去。
如果這個方法內部呼叫了wait(),則可以進入其他加synchronized的方法。
如果其他方法加了synchronized關鍵字,並且沒有呼叫wai方法,則不能。
synchronized和java.util.concurrent.locks.Lock的異同?
主要相同點:Lock能完成Synchronized所實現的所有功能。
主要不同點:Lock有比Synchronized更精確的執行緒予以和更好的效能。Synchronized會自動釋放鎖,但是Lock一定要求程式設計師手工釋放,並且必須在finally從句中釋放。
樂觀鎖和悲觀鎖的理解及如何實現,有哪些實現方式?
樂觀鎖是假設每次操作都不會衝突,若是遇到衝突失敗就重試直到成功;悲觀鎖是讓其他執行緒都等待,等鎖釋放完了再競爭鎖。
樂觀鎖實現方式:cas,volatile
悲觀鎖實現方式:synchronized,Lock
併發框架
SynchronizedMap和ConcurrentHashMap有什麼區別?
SynchronizedMap()和Hashtable一樣,實現上在呼叫map所有方法時,都對整個map進行同步。而ConcurrentHashMap的實現卻更加精細,它對map中的所有桶加了鎖。所以,只要有一個執行緒訪問map,其他執行緒就無法進入map,而如果一個執行緒在訪問ConcurrentHashMap某個桶時,其他執行緒,仍然可以對map執行某些操作。
所以,ConcurrentHashMap在效能以及安全性方面,明顯比Collections.synchronizedMap()更加有優勢。同時,同步操作精確控制到桶,這樣,即使在遍歷map時,如果其他執行緒試圖對map進行資料修改,也不會丟擲ConcurrentModificationException。
CopyOnWriteArrayList可以用於什麼應用場景?
CopyOnWriteArrayList的特性是針對讀操作,不做處理,和普通的ArrayList效能一樣。而在寫操作時,會先拷貝一份,實現新舊版本的分離,然後在拷貝的版本上進行修改操作,修改完後,將其更新至就版本中。
那麼他的使用場景就是:一個需要在多執行緒中操作,並且頻繁遍歷。其解決了由於長時間鎖定整個陣列導致的效能問題,解決方案即寫時拷貝。
另外需要注意的是CopyOnWrite容器只能保證資料的最終一致性,不能保證資料的實時一致性。所以如果你希望寫入的的資料,馬上能讀到,請不要使用CopyOnWrite容器。
執行緒安全
什麼叫執行緒安全?servlet是執行緒安全嗎?
執行緒安全就是說多執行緒訪問同一程式碼,不會產生不確定的結果。
在多執行緒環境中,當各執行緒不共享資料的時候,即都是私有(private)成員,那麼一定是執行緒安全的。但這種情況並不多見,在多數情況下需要共享資料,這時就需要進行適當的同步控制了。
執行緒安全一般都涉及到synchronized, 就是一段程式碼同時只能有一個執行緒來操作 不然中間過程可能會產生不可預製的結果。
如果你的程式碼所在的程序中有多個執行緒在同時執行,而這些執行緒可能會同時執行這段程式碼。如果每次執行結果和單執行緒執行的結果是一樣的,而且其他的變數的值也和預期的是一樣的,就是執行緒安全的。
同步有幾種實現方法?
1.同步方法
即有synchronized關鍵字修飾的方法。
由於java的每個物件都有一個內建鎖,當用此關鍵字修飾方法時,內建鎖會保護整個方法。在呼叫該方法前,需要獲得內建鎖,否則就處於阻塞狀態。
2.同步程式碼塊
即有synchronized關鍵字修飾的語句塊。
被該關鍵字修飾的語句塊會自動被加上內建鎖,從而實現同步。
3.使用特殊域變數(volatile)實現執行緒同步
a.volatile關鍵字為域變數的訪問提供了一種免鎖機制,
b.使用volatile修飾域相當於告訴虛擬機器該域可能會被其他執行緒更新,
c.因此每次使用該域就要重新計算,而不是使用暫存器中的值
d.volatile不會提供任何原子操作,它也不能用來修飾final型別的變數
4.使用重入鎖實現執行緒同步
在JavaSE5.0中新增了一個java.util.concurrent包來支援同步。ReentrantLock類是可重入、互斥、實現了Lock介面的鎖,它與使用synchronized方法和快具有相同的基本行為和語義,並且擴充套件了其能力。
5.使用區域性變數實現執行緒同步。
volatile有什麼用?能否用一句話說明下volatile的應用場景?
作用是:作為指令關鍵字,確保本條指令不會因編譯器的優化而省略,且要求每次直接讀值,即不是從暫存器裡取備份值,而是去該地址記憶體儲存的值。
一句話說明volatile的應用場景:
對變數的寫操作不依賴於當前值且該變數沒有包含在具有其他變數的不變式中。
請說明下java的記憶體模型。
Java記憶體模型的邏輯檢視

為了保證併發程式設計中可以滿足原子性、可見性及有序性。有一個重要的概念,那就是記憶體模型。
為了保證共享記憶體的正確性(可見性、有序性、原子性),記憶體模型定義了共享記憶體系統中多執行緒程式讀寫操作行為的規範。
通過這些規則來規範對記憶體的讀寫操作,從而保證指令執行的正確性。它與處理器有關、與快取有關、與併發有關、與編譯器也有關。
它解決了 CPU 多級快取、處理器優化、指令重排等導致的記憶體訪問問題,保證了併發場景下的一致性、原子性和有序性。
記憶體模型解決併發問題主要採用兩種方式:
- 限制處理器優化
- 使用記憶體屏障
關於主記憶體與工作記憶體之間的具體互動協議,即一個變數如何從主記憶體拷貝到工作記憶體、如何從工作記憶體同步到主記憶體之間的實現細節,Java記憶體模型定義了以下八種操作來完成:
- lock(鎖定):作用於主記憶體的變數,把一個變數標識為一條執行緒獨佔狀態。
- unlock(解鎖):作用於主記憶體變數,把一個處於鎖定狀態的變數釋放出來,釋放後的變數才可以被其他執行緒鎖定。
- read(讀取):作用於主記憶體變數,把一個變數值從主記憶體傳輸到執行緒的工作記憶體中,以便隨後的load動作使用
- load(載入):作用於工作記憶體的變數,它把read操作從主記憶體中得到的變數值放入工作記憶體的變數副本中。
- use(使用):作用於工作記憶體的變數,把工作記憶體中的一個變數值傳遞給執行引擎,每當虛擬機器遇到一個需要使用變數的值的位元組碼指令時將會執行這個操作。
- assign(賦值):作用於工作記憶體的變數,它把一個從執行引擎接收到的值賦值給工作記憶體的變數,每當虛擬機器遇到一個給變數賦值的位元組碼指令時執行這個操作。
- store(儲存):作用於工作記憶體的變數,把工作記憶體中的一個變數的值傳送到主記憶體中,以便隨後的write的操作。
- write(寫入):作用於主記憶體的變數,它把store操作從工作記憶體中一個變數的值傳送到主記憶體的變數中。
如果要把一個變數從主記憶體中複製到工作記憶體,就需要按順尋地執行read和load操作,如果把變數從工作記憶體中同步回主記憶體中,就要按順序地執行store和write操作。Java記憶體模型只要求上述操作必須按順序執行,而沒有保證必須是連續執行。也就是read和load之間,store和write之間是可以插入其他指令的,如對主記憶體中的變數a、b進行訪問時,可能的順序是read a,read b,load b, load a。Java記憶體模型還規定了在執行上述八種基本操作時,必須滿足如下規則:
- 不允許read和load、store和write操作之一單獨出現
- 不允許一個執行緒丟棄它的最近assign的操作,即變數在工作記憶體中改變了之後必須同步到主記憶體中。
- 不允許一個執行緒無原因地(沒有發生過任何assign操作)把資料從工作記憶體同步回主記憶體中。
- 一個新的變數只能在主記憶體中誕生,不允許在工作記憶體中直接使用一個未被初始化(load或assign)的變數。即就是對一個變數實施use和store操作之前,必須先執行過了assign和load操作。
- 一個變數在同一時刻只允許一條執行緒對其進行lock操作,lock和unlock必須成對出現
- 如果對一個變數執行lock操作,將會清空工作記憶體中此變數的值,在執行引擎使用這個變數前需要重新執行load或assign操作初始化變數的值
- 如果一個變數事先沒有被lock操作鎖定,則不允許對它執行unlock操作;也不允許去unlock一個被其他執行緒鎖定的變數。
- 對一個變數執行unlock操作之前,必須先把此變數同步到主記憶體中(執行store和write操作)。
併發容器和框架
如何讓一段程式併發的執行,並最終彙總結果?
使用CyclicBarrier 在多個關口處將多個執行緒執行結果彙總, CountDownLatch 在各執行緒執行完畢後向匯流排程彙報結果。
CountDownLatch : 一個執行緒(或者多個), 等待另外N個執行緒完成某個事情之後才能執行。
CyclicBarrier : N個執行緒相互等待,任何一個執行緒完成之前,所有的執行緒都必須等待。
這樣應該就清楚一點了,對於CountDownLatch來說,重點是那個“一個執行緒”, 是它在等待,而另外那N的執行緒在把“某個事情”做完之後可以繼續等待,可以終止。而對於CyclicBarrier來說,重點是那N個執行緒,他們之間任何一個沒有完成,所有的執行緒都必須等待。
從api上理解就是CountdownLatch有主要配合使用兩個方法countDown()和await(),countDown()是做事的執行緒用的方法,await()是等待事情完成的執行緒用個方法,這兩種執行緒是可以分開的(下面例子:CountdownLatchTest2),當然也可以是同一組執行緒;CyclicBarrier只有一個方法await(),指的是做事執行緒必須大家同時等待,必須是同一組執行緒的工作。
import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * 各個執行緒執行完成後,主執行緒做總結性工作的例子 * @author xuexiaolei * @version 2019年4月16日 */ public class CountdownLatchTest2 { private final static int THREAD_NUM = 10; public static void main(String[] args) { CountDownLatch lock = new CountDownLatch(THREAD_NUM); ExecutorService exec = Executors.newCachedThreadPool(); for (int i = 0; i < THREAD_NUM; i++) { exec.submit(new CountdownLatchTask(lock, "Thread-"+i)); } try { lock.await(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("大家都執行完成了,做總結性工作"); exec.shutdown(); } static class CountdownLatchTask implements Runnable{ private final CountDownLatch lock; private final String threadName; CountdownLatchTask(CountDownLatch lock, String threadName) { this.lock = lock; this.threadName = threadName; } @Override public void run() { System.out.println(threadName + " 執行完成"); lock.countDown(); } } }
CyclicBarrier例子:
import java.util.concurrent.*; /** * * @author xuexiaolei * @version 2019年4月16日 */ public class CyclicBarrierTest { private final static int THREAD_NUM = 10; public static void main(String[] args) { CyclicBarrier lock = new CyclicBarrier(THREAD_NUM, new Runnable() { @Override public void run() { System.out.println("這階段大家都執行完成了,我總結一下,然後開始下一階段"); } } ); ExecutorService exec = Executors.newCachedThreadPool(); for (int i = 0; i < THREAD_NUM; i++) { exec.submit(new CountdownLatchTask(lock, "Task-"+i)); } exec.shutdown(); } static class CountdownLatchTask implements Runnable{ private final CyclicBarrier lock; private final String threadName; CountdownLatchTask(CyclicBarrier lock, String threadName) { this.lock = lock; this.threadName = threadName; } @Override public void run() { for (int i = 0; i < 3; i++) { System.out.println(threadName + " 執行完成"); try { lock.await(); } catch (BrokenBarrierException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } } } } }
如何合理的配置java執行緒池?如CPU密集型的任務,基本執行緒池應該配置多大?IO密集型的任務,基本執行緒池應該配置多大?用有界佇列好還是無界佇列好?任務非常多的時候,使用什麼阻塞佇列能獲取最好的吞吐量?
雖然Exectors可以生成一些很常用的執行緒池,但畢竟在什麼情況下使用還是開發者最清楚的。在某些自己很清楚的使用場景下,java執行緒池還是推薦自己配置的。下面是java執行緒池的配置類的引數,我們逐一分析一下:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler
- corePoolSize - 池中所儲存的執行緒數,包括空閒執行緒。
- maximumPoolSize - 池中允許的最大執行緒數。
- keepAliveTime - 當執行緒數大於核心時,此為終止前多餘的空閒執行緒等待新任務的最長時間。
- unit - keepAliveTime 引數的時間單位。
- workQueue - 執行前用於保持任務的佇列。此佇列僅保持由 execute 方法提交的 Runnable 任務。用BlocingQueue的實現類都可以。
- threadFactory - 執行程式建立新執行緒時使用的工廠。自定義執行緒工廠可以做一些額外的操作,比如統計生產的執行緒數等。
- handler - 飽和策略,即超出執行緒範圍和佇列容量而使執行被阻塞時所使用的處理程式。策略有:Abort終止並丟擲異常,Discard悄悄拋棄任務,Discard-Oldest拋棄最老的任務策略,Caller-Runs將任務退回給呼叫者策略。
至於執行緒池應當配置多大的問題,一般有如下的經驗設定:
-
如果是CPU密集型應用,則執行緒池大小設定為N+1。
-
如果是IO密集型應用,則執行緒池大小設定為2N+1。
用有界佇列好還是無界佇列好?這種問題的答案肯定是視情況而定:
-
有界佇列有助於避免資源耗盡的情況發生。但他帶來了新的問題:當佇列填滿後,新的任務怎麼辦?所以有界佇列適用於執行比較耗資源的任務,同時要設計好相應的飽和策略。
-
無界佇列和有界佇列剛好相反,在資源無限的情況下可以一直接收新任務。適用於小任務,請求和處理速度相對持平的狀況。
-
其實還有一種同步移交的佇列 SynchronousQueue ,這種佇列不儲存任務資訊,直接將任務提交給執行緒池。可以理解為容量只有1的有界佇列,在特殊場景下有特殊作用,同樣得設計好相應的飽和策略。
如何使用阻塞佇列實現一個生產者和消費者模型?請寫程式碼。
下面這是一個完整的生產者消費者程式碼例子,對比傳統的wait、nofity程式碼,它更易於理解。
ProducerConsumerPattern.java如下:
import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.logging.Level; import java.util.logging.Logger; public class ProducerConsumerPattern { public static void main(String args[]){ //Creating shared object BlockingQueue sharedQueue = new LinkedBlockingQueue(); //Creating Producer and Consumer Thread Thread prodThread = new Thread(new Producer(sharedQueue)); Thread consThread = new Thread(new Consumer(sharedQueue)); //Starting producer and Consumer thread prodThread.start(); consThread.start(); } }
生產者,Producer.java如下:
class Producer implements Runnable { private final BlockingQueue sharedQueue; public Producer(BlockingQueue sharedQueue) { this.sharedQueue = sharedQueue; } @Override public void run() { for (int i=0; i<10; i++){ try { System.out.println("Produced: " + i); sharedQueue.put(i); } catch (InterruptedException ex) { Logger.getLogger(Producer.class.getName()).log(Level.SEVERE, null, ex); } } } }
消費者,Consumer.java如下所示:
class Consumer implements Runnable{ private final BlockingQueue sharedQueue; public Consumer (BlockingQueue sharedQueue) { this.sharedQueue = sharedQueue; } @Override public void run() { while(true){ try { System.out.println("Consumed: "+ sharedQueue.take()); } catch (InterruptedException ex) { Logger.getLogger(Consumer.class.getName()).log(Level.SEVERE, null, ex); } } } }
Java中的鎖
如何實現樂觀鎖(CAS)?如何避免ABA問題?
CAS是項樂觀鎖技術,當多個執行緒嘗試使用CAS同時更新同一個變數時,只有其中一個執行緒能更新變數的值,而其它執行緒都失敗,失敗的執行緒並不會被掛起,而是被告知這次競爭中失敗,並可以再次嘗試。
CAS 操作包含三個運算元 —— 記憶體位置(V)、預期原值(A)和新值(B)。如果記憶體位置的值與預期原值相匹配,那麼處理器會自動將該位置值更新為新值。否則,處理器不做任何操作。無論哪種情況,它都會在 CAS 指令之前返回該位置的值。(在 CAS 的一些特殊情況下將僅返回 CAS 是否成功,而不提取當前值。)CAS 有效地說明了“我認為位置 V 應該包含值 A;如果包含該值,則將 B 放到這個位置;否則,不要更改該位置,只告訴我這個位置現在的值即可。”這其實和樂觀鎖的衝突檢查+資料更新的原理是一樣的。
這裡再強調一下,樂觀鎖是一種思想。CAS是這種思想的一種實現方式。
ABA問題:
比如說一個執行緒one從記憶體位置V中取出A,這時候另一個執行緒two也從記憶體中取出A,並且two進行了一些操作變成了B,然後two又將V位置的資料變成A,這時候執行緒one進行CAS操作發現記憶體中仍然是A,然後one操作成功。儘管執行緒one的CAS操作成功,但是不代表這個過程就是沒有問題的。
解決方法:通過版本號(version)的方式來解決,每次比較要比較資料的值和版本號兩項內容即可。
讀寫鎖可以用於什麼應用場景?
在多執行緒的環境下,對同一份資料進行讀寫,會涉及到執行緒安全的問題。比如在一個執行緒讀取資料的時候,另外一個執行緒在寫資料,而導致前後資料的不一致性;一個執行緒在寫資料的時候,另一個執行緒也在寫,同樣也會導致執行緒前後看到的資料的不一致性。
這時候可以在讀寫方法中加入互斥鎖,任何時候只能允許一個執行緒的一個讀或寫操作,而不允許其他執行緒的讀或寫操作,這樣是可以解決這樣以上的問題,但是效率卻大打折扣了。因為在真實的業務場景中,一份資料,讀取資料的操作次數通常高於寫入資料的操作,而執行緒與執行緒間的讀讀操作是不涉及到執行緒安全的問題,沒有必要加入互斥鎖,只要在讀-寫,寫-寫期間上鎖就行了。
對於以上這種情況,讀寫鎖是最好的解決方案!其中它的實現類:ReentrantReadWriteLock--顧名思義是可重入的讀寫鎖,允許多個讀執行緒獲得ReadLock,但只允許一個寫執行緒獲得WriteLock
讀寫鎖的機制:
- "讀-讀" 不互斥
- "讀-寫" 互斥
- "寫-寫" 互斥
什麼時候應該使用可重入鎖?
可重入鎖,也叫做遞迴鎖,指的是同一執行緒 外層函式獲得鎖之後 ,內層遞迴函式仍然有獲取該鎖的程式碼,但不受影響。
在JAVA環境下 ReentrantLock 和synchronized 都是 可重入鎖。
什麼場景下可以使用volatile替換synchronized?
狀態標誌:把簡單地volatile變數作為狀態標誌,來達成執行緒之間通訊的目的,省去了用synchronized還要wait,notify或者interrupt的編碼麻煩。
替換重量級鎖:如果某個變數僅是單次讀或者單次寫操作,沒有複合操作(i++,先檢查後判斷之類的)就可以用volatile替換synchronized。
併發工具
如何實現一個流控程式,用於控制請求的呼叫次數?
import java.util.Random; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; /** * 阻塞訪問的執行緒,直到獲取了訪問令牌 * @author xuexiaolei * @version 2017年11月15日 */ public class FlowControl2 { private final static int MAX_COUNT = 10; private final Semaphore semaphore = new Semaphore(MAX_COUNT); private final ExecutorService exec = Executors.newCachedThreadPool(); public void access(int i){ exec.submit(new Runnable() { @Override public void run() { semaphore.acquireUninterruptibly(); doSomething(i); semaphore.release(); } } ); } public void doSomething(int i){ try { Thread.sleep(new Random().nextint(100)); System.out.println(String.format("%s 通過執行緒:%s 訪問成功",i,Thread.currentThread().getName())); } catch (InterruptedException e) { } } public static void main(String[] args) { FlowControl2 web = new FlowControl2(); for (int i = 0; i < 2000; i++) { web.access(i); } } }