【備戰春招系列】最全併發編程面試真題解析,教你如何吊打面試官

一、併發編程三要素?

一、原子性

原子性指的是一個或者多個操做,要麼所有執行而且在執行的過程當中不被其餘操
做打斷,要麼就所有都不執行。

二、可見性

可見性指多個線程操做一個共享變量時,其中一個線程對變量進行修改後,其餘
線程能夠當即看到修改的結果。

三、有序性

有序性,即程序的執行順序按照代碼的前後順序來執行。 

二、實現可見性的方法有哪些?

synchronized 或者 Lock:保證同一個時刻只有一個線程獲取鎖執行代碼,鎖釋放以前把最新的值刷新到主內存,實現可見性。 

三、多線程的價值?

一、發揮多核 CPU 的優點 

多線程,能夠真正發揮出多核 CPU 的優點來,達到充分利用 CPU 的目的,採用多線程的方式去同時完成幾件事情而不互相干擾。 

二、防止阻塞

從程序運行效率的角度來看,單核 CPU 不但不會發揮出多線程的優點,反而會由於在單核 CPU 上運行多線程致使線程上下文的切換,而下降程序總體的效率。可是單核 CPU 咱們仍是要應用多線程,就是爲了防止阻塞。試想,若是單核 CPU 使用單線程,那麼只要這個線程阻塞了,比方說遠程讀取某個數據吧,對端遲遲未返回又沒有設置超時時間,那麼你的整個程序在數據返回回來以前就中止運行了。多線程能夠防止這個問題,多條線程同時運行,哪怕一條線程的代碼執行讀取數據阻塞,也不會影響其它任務的執行。 

三、便於建模 

這是另一個沒有這麼明顯的優勢了。假設有一個大的任務 A,單線程編程,那麼就要考慮不少,創建整個程序模型比較麻煩。可是若是把這個大的任務 A 分解成幾個小任務,任務 B、任務 C、任務 D,分別創建程序模型,並經過多線程分別運行這幾個任務,那就簡單不少了。

四、建立線程的有哪些方式? 

一、繼承 Thread 類建立線程類
二、經過 Runnable 接口建立線程類
三、經過 Callable 和 Future 建立線程
四、經過線程池建立

五、建立線程的三種方式的對比? 

一、採用實現 Runnable、Callable 接口的方式建立多線程。

優點是: java

線程類只是實現了 Runnable 接口或 Callable 接口,還能夠繼承其餘類。在這種方式下,多個線程能夠共享同一個 target 對象,因此很是適合多個相同線程來處理同一份資源的狀況,從而能夠將 CPU、代碼和數據分開,造成清晰的模型,較好地體現了面向對象的思想。 
劣勢是:

編程稍微複雜,若是要訪問當前線程,則必須使用 Thread.currentThread()方法。 

二、使用繼承 Thread 類的方式建立多線程 

優點是: 
編寫簡單,若是須要訪問當前線程,則無需使用 Thread.currentThread()方法,直接使用 this 便可得到當前線程。 
劣勢是:

線程類已經繼承了 Thread 類,因此不能再繼承其餘父類。 

三、Runnable 和 Callable 的區別

一、Callable 規定(重寫)的方法是 call(),Runnable 規定(重寫)的方法是 run()。
二、Callable 的任務執行後可返回值,而 Runnable 的任務是不能返回值的。
三、Call 方法能夠拋出異常,run 方法不能夠。
四、運行 Callable 任務能夠拿到一個 Future 對象,表示異步計算的結果。它提供了檢查計算是否完成的方法,以等待計算的完成,並檢索計算的結果。經過 Future對象能夠了解任務執行狀況,可取消任務的執行,還可獲取執行結果。 

六、線程的狀態流轉圖

線程的生命週期及五種基本狀態:

![img_2.png][img_2.png]複製代碼

七、Java 線程具備五中基本狀態

一、新建狀態(New):當線程對象對建立後,即進入了新建狀態,如:Thread t
= new MyThread();
二、就緒狀態(Runnable):當調用線程對象的 start()方法(t.start();),線程
即進入就緒狀態。處於就緒狀態的線程,只是說明此線程已經作好了準備,隨時
等待 CPU 調度執行,並非說執行了 t.start()此線程當即就會執行;
三、運行狀態(Running):當 CPU 開始調度處於就緒狀態的線程時,此時線程
才得以真正執行,即進入到運行狀態。注:就 緒狀態是進入到運行狀態的惟一入
口,也就是說,線程要想進入運行狀態執行,首先必須處於就緒狀態中;
四、阻塞狀態(Blocked):處於運行狀態中的線程因爲某種緣由,暫時放棄對 CPU
的使用權,中止執行,此時進入阻塞狀態,直到其進入到就緒狀態,才 有機會再
次被 CPU 調用以進入到運行狀態。
根據阻塞產生的緣由不一樣,阻塞狀態又能夠分爲三種:
一、等待阻塞:運行狀態中的線程執行 wait()方法,使本線程進入到等待阻塞狀態;
二、同步阻塞:線程在獲取 synchronized 同步鎖失敗(由於鎖被其它線程所佔用),
它會進入同步阻塞狀態;
三、其餘阻塞:經過調用線程的 sleep()或 join()或發出了 I/O 請求時,線程會進入
到阻塞狀態。當 sleep()狀態超時、join()等待線程終止或者超時、或者 I/O 處理
完畢時,線程從新轉入就緒狀態。
五、死亡狀態(Dead):線程執行完了或者因異常退出了 run()方法,該線程結束
生命週期

八、什麼是線程池?有哪幾種建立方式? 

線程池就是提早建立若干個線程,若是有任務須要處理,線程池裏的線程就會處理任務,處理完以後線程並不會被銷燬,而是等待下一個任務。因爲建立和銷燬線程都是消耗系統資源的,因此當你想要頻繁的建立和銷燬線程的時候就能夠考慮使用線程池來提高系統的性能。java 提供了一個 java.util.concurrent.Executor 接口的實現用於建立線程池。

九、四種線程池的建立:

一、newCachedThreadPool 建立一個可緩存線程池
二、newFixedThreadPool 建立一個定長線程池,可控制線程最大併發數。
三、newScheduledThreadPool 建立一個定長線程池,支持定時及週期性任務執行。
四、newSingleThreadExecutor 建立一個單線程化的線程池,它只會用惟一的工做線程來執行任務。

十、線程池的優勢?

一、重用存在的線程,減小對象建立銷燬的開銷。
二、可有效的控制最大併發線程數,提升系統資源的使用率,同時避免過多資源競爭,避免堵塞。
三、提供定時執行、按期執行、單線程、併發數控制等功能。 

十一、經常使用的併發工具類有哪些?

一、CountDownLatch
二、CyclicBarrier
三、Semaphore
四、Exchanger 

十二、CyclicBarrier 和 CountDownLatch 的區別 

一、CountDownLatch 簡單的說就是一個線程等待,直到他所等待的其餘線程都執行完成而且調用 countDown()方法發出通知後,當前線程才能夠繼續執行。
二、cyclicBarrier 是全部線程都進行等待,直到全部線程都準備好進入 await()方法以後,全部線程同時開始執行!
三、CountDownLatch 的計數器只能使用一次。而 CyclicBarrier 的計數器可使用 reset() 方法重置。因此 CyclicBarrier 能處理更爲複雜的業務場景,好比若是計算髮生錯誤,能夠重置計數器,並讓線程們從新執行一次。
四、CyclicBarrier 還提供其餘有用的方法,好比 getNumberWaiting 方法能夠得到 CyclicBarrier 阻塞的線程數量。isBroken 方法用來知道阻塞的線程是否被中斷。若是被中斷返回 true,不然返回 false。 

1三、synchronized 的做用?

在 Java 中,synchronized 關鍵字是用來控制線程同步的,就是在多線程的環境下,控制 synchronized 代碼段不被多個線程同時執行。
synchronized 既能夠加在一段代碼上,也能夠加在方法上。

1四、volatile 關鍵字的做用

對於可見性,Java 提供了 volatile 關鍵字來保證可見性。
當一個共享變量被 volatile 修飾時,它會保證修改的值會當即被更新到主存,當有其餘線程須要讀取時,它會去內存中讀取新值。
從實踐角度而言,volatile 的一個重要做用就是和 CAS 結合,保證了原子性,詳細的能夠參見 java.util.concurrent.atomic 包下的類,好比 AtomicInteger。 

1五、什麼是 CAS

CAS 是 compare and swap 的縮寫,即咱們所說的比較交換。
cas 是一種基於鎖的操做,並且是樂觀鎖。在 java 中鎖分爲樂觀鎖和悲觀鎖。悲觀鎖是將資源鎖住,等一個以前得到鎖的線程釋放鎖以後,下一個線程才能夠訪問。而樂觀鎖採起了一種寬泛的態度,經過某種方式不加鎖來處理資源,好比經過給記錄加 version 來獲取數據,性能較悲觀鎖有很大的提升。
CAS 操做包含三個操做數 —— 內存位置(V)、預期原值(A)和新值(B)。若是內存地址裏面的值和 A 的值是同樣的,那麼就將內存裏面的值更新成 B。CAS是經過無限循環來獲取數據的,若果在第一輪循環中,a 線程獲取地址裏面的值被b 線程修改了,那麼 a 線程須要自旋,到下次循環纔有可能機會執行。java.util.concurrent.atomic 包下的類大可能是使用 CAS 操做來實現的( AtomicInteger,AtomicBoolean,AtomicLong)。 

1六、CAS 的問題

一、CAS 容易形成 ABA 問題

一個線程 a 將數值改爲了 b,接着又改爲了 a,此時 CAS 認爲是沒有變化,實際上是已經變化過了,而這個問題的解決方案可使用版本號標識,每操做一次version 加 1。在 java5 中,已經提供了 AtomicStampedReference 來解決問題。

二、不能保證代碼塊的原子性

CAS 機制所保證的知識一個變量的原子性操做,而不能保證整個代碼塊的原子性。
好比須要保證 3 個變量共同進行原子性的更新,就不得不使用 synchronized 了。

三、CAS 形成 CPU 利用率增長

以前說過了 CAS 裏面是一個循環判斷的過程,若是線程一直沒有獲取到狀態,cpu資源會一直被佔用。

1七、什麼是 Future? 

在併發編程中,咱們常常用到非阻塞的模型,在以前的多線程的三種實現中,無論是繼承 thread 類仍是實現 runnable 接口,都沒法保證獲取到以前的執行結果。經過實現 Callback 接口,並用 Future 能夠來接收多線程的執行結果。

Future 表示一個可能尚未完成的異步任務的結果,針對這個結果能夠添加Callback 以便在任務執行成功或失敗後做出相應的操做。 程序員

1八、什麼是 AQS

AQS 是 AbustactQueuedSynchronizer 的簡稱,它是一個 Java 提升的底層同步工具類,用一個 int 類型的變量表示同步狀態,並提供了一系列的 CAS 操做來管理這個同步狀態。 
AQS 是一個用來構建鎖和同步器的框架,使用 AQS 能簡單且高效地構造出應用普遍的大量的同步器,好比咱們提到的 ReentrantLock,Semaphore,其餘的諸如ReentrantReadWriteLock,SynchronousQueue,FutureTask 等等皆是基於AQS 的。 

1九、AQS 支持兩種同步方式:

一、獨佔式
二、共享式
這樣方便使用者實現不一樣類型的同步組件,獨佔式如 ReentrantLock,共享式如Semaphore,CountDownLatch,組合式的如 ReentrantReadWriteLock。總之,AQS 爲使用提供了底層支撐,如何組裝實現,使用者能夠自由發揮。

20、ReadWriteLock 是什麼

首先明確一下,不是說 ReentrantLock 很差,只是 ReentrantLock 某些時候有侷限。若是使用 ReentrantLock,可能自己是爲了防止線程 A 在寫數據、線程 B 在讀數據形成的數據不一致,但這樣,若是線程 C 在讀數據、線程 D 也在讀數據,讀數據是不會改變數據的,沒有必要加鎖,可是仍是加鎖了,下降了程序的性能。由於這個,才誕生了讀寫鎖 ReadWriteLock。ReadWriteLock 是一個讀寫鎖接口,ReentrantReadWriteLock 是 ReadWriteLock 接口的一個具體實現,實現了讀寫的分離,讀鎖是共享的,寫鎖是獨佔的,讀和讀之間不會互斥,讀和寫、寫和讀、寫和寫之間纔會互斥,提高了讀寫的性能。

2一、FutureTask 是什麼

這個其實前面有提到過,FutureTask 表示一個異步運算的任務。FutureTask 裏面能夠傳入一個 Callable 的具體實現類,能夠對這個異步運算的任務的結果進行等待獲取、判斷是否已經完成、取消任務等操做。固然,因爲 FutureTask 也是Runnable 接口的實現類,因此 FutureTask 也能夠放入線程池中。

2二、synchronized 和 ReentrantLock 的區別

synchronized 是和 if、else、for、while 同樣的關鍵字,ReentrantLock 是類,這是兩者的本質區別。既然 ReentrantLock 是類,那麼它就提供了比synchronized 更多更靈活的特性,能夠被繼承、能夠有方法、能夠有各類各樣的類變量,ReentrantLock 比 synchronized 的擴展性體如今幾點上:
  1. ReentrantLock 能夠對獲取鎖的等待時間進行設置,這樣就避免了死鎖
  2. ReentrantLock 能夠獲取各類鎖的信息
  3. ReentrantLock 能夠靈活地實現多路通知
另外,兩者的鎖機制其實也是不同的。ReentrantLock 底層調用的是 Unsafe 的park 方法加鎖,synchronized 操做的應該是對象頭中 mark word,這點我不能肯定。 

2三、什麼是樂觀鎖和悲觀鎖

一、樂觀鎖:就像它的名字同樣,對於併發間操做產生的線程安全問題持樂觀狀態,樂觀鎖認爲競爭不老是會發生,所以它不須要持有鎖,將比較-替換這兩個動做做爲一個原子操做嘗試去修改內存中的變量,若是失敗則表示發生衝突,那麼就應該有相應的重試邏輯。
二、悲觀鎖:仍是像它的名字同樣,對於併發間操做產生的線程安全問題持悲觀狀態,悲觀鎖認爲競爭老是會發生,所以每次對某資源進行操做時,都會持有一個獨佔的鎖,就像synchronized,無論三七二十一,直接上了鎖就操做資源了。 

2四、線程 B 怎麼知道線程 A 修改了變量

  1. volatile 修飾變量
  2. synchronized 修飾修改變量的方法
  3. wait/notify
  4. while 輪詢 

2五、synchronized、volatile、CAS 比較

  1. synchronized 是悲觀鎖,屬於搶佔式,會引發其餘線程阻塞。
  2. volatile 提供多線程共享變量可見性和禁止指令重排序優化。
  3. CAS 是基於衝突檢測的樂觀鎖(非阻塞) 

2六、sleep 方法和 wait 方法有什麼區別? 

這個問題常問,sleep 方法和 wait 方法均可以用來放棄 CPU 必定的時間,不一樣點在於若是線程持有某個對象的監視器,sleep 方法不會放棄這個對象的監視器,wait 方法會放棄這個對象的監視器

2七、ThreadLocal 是什麼?有什麼用?

ThreadLocal 是一個本地線程副本變量工具類。主要用於將私有線程和該線程存放的副本對象作一個映射,各個線程之間的變量互不干擾,在高併發場景下,能夠實現無狀態的調用,特別適用於各個線程依賴不通的變量值完成操做的場景。簡單說 ThreadLocal 就是一種以空間換時間的作法,在每一個 Thread 裏面維護了一個以開地址法實現ThreadLocal.ThreadLocalMap,把數據進行隔離,數據不共享,天然就沒有線程安全方面的問題了。

2八、爲何 wait()方法和 notify()/notifyAll()方法要在同步塊中被調用

這是 JDK 強制的,wait()方法和 notify()/notifyAll()方法在調用前都必須先得到對象的鎖 

2九、多線程同步有哪幾種方法? 

Synchronized 關鍵字,Lock 鎖實現,分佈式鎖等。 

30、線程的調度策略

線程調度器選擇優先級最高的線程運行,可是,若是發生如下狀況,就會終止線程的運行:
  1. 線程體中調用了 yield 方法讓出了對 cpu 的佔用權利
  2. 線程體中調用了 sleep 方法使線程進入睡眠狀態
  3. 線程因爲 IO 操做受到阻塞
  4. 另一個更高優先級線程出現
  5. 在支持時間片的系統中,該線程的時間片用完 

3一、ConcurrentHashMap 的併發度是什麼

ConcurrentHashMap 的併發度就是 segment 的大小,默認爲 16,這意味着最多同時能夠有 16 條線程操做 ConcurrentHashMap,這也是ConcurrentHashMap 對 Hashtable 的最大優點,任何狀況下,Hashtable 能同時有兩條線程獲取 Hashtable 中的數據嗎?

3二、Linux 環境下如何查找哪一個線程使用 CPU 最長

  1. 獲取項目的 pid,jps 或者 ps -ef | grep java,這個前面有講過
  2. top -H -p pid,順序不能改變

3三、Java 死鎖以及如何避免?

Java 中的死鎖是一種編程狀況,其中兩個或多個線程被永久阻塞,Java 死鎖狀況出現至少兩個線程和兩個或更多資源。
Java 發生死鎖的根本緣由是:在申請鎖時發生了交叉閉環申請。 

3四、死鎖的緣由

一、是多個線程涉及到多個鎖,這些鎖存在着交叉,因此可能會致使了一個鎖依賴的閉環。 

例如:線程在得到了鎖 A 而且沒有釋放的狀況下去申請鎖 B,這時,另外一個線程已經得到了鎖 B,在釋放鎖 B 以前又要先得到鎖 A,所以閉環發生,陷入死鎖循環。

二、默認的鎖申請操做是阻塞的。

因此要避免死鎖,就要在一遇到多個對象鎖交叉的狀況,就要仔細審查這幾個對象的類中的全部方法,是否存在着致使鎖依賴的環路的可能性。總之是儘可能避免在一個同步方法中調用其它對象的延時方法和同步方法。 

3五、怎麼喚醒一個阻塞的線程

若是線程是由於調用了 wait()、sleep()或者 join()方法而致使的阻塞,能夠中斷線程,而且經過拋出 InterruptedException 來喚醒它;若是線程遇到了 IO 阻塞,無能爲力,由於 IO 是操做系統實現的,Java 代碼並無辦法直接接觸到操做系統。 

3六、不可變對象對多線程有什麼幫助

前面有提到過的一個問題,不可變對象保證了對象的內存可見性,對不可變對象的讀取不須要進行額外的同步手段,提高了代碼執行效率。

3七、什麼是多線程的上下文切換

多線程的上下文切換是指 CPU 控制權由一個已經正在運行的線程切換到另一個就緒並等待獲取 CPU 執行權的線程的過程。 

3八、若是你提交任務時,線程池隊列已滿,這時會發生什麼

這裏區分一下: 
一、若是使用的是無界隊列 LinkedBlockingQueue,也就是無界隊列的話,不要緊,繼續添加任務到阻塞隊列中等待執行,由於 LinkedBlockingQueue 能夠近乎認爲是一個無窮大的隊列,能夠無限存聽任務
二、若是使用的是有界隊列好比 ArrayBlockingQueue,任務首先會被添加到ArrayBlockingQueue 中,ArrayBlockingQueue 滿了,會根據maximumPoolSize 的值增長線程數量,若是增長了線程數量仍是處理不過來,ArrayBlockingQueue 繼續滿,那麼則會使用拒絕策略RejectedExecutionHandler 處理滿了的任務,默認是 AbortPolicy

3九、Java 中用到的線程調度算法是什麼

搶佔式。一個線程用完 CPU 以後,操做系統會根據線程優先級、線程飢餓狀況等數據算出一個總的優先級並分配下一個時間片給某個線程執行。 

40、什麼是線程調度器(Thread Scheduler)和時間分片(TimeSlicing)?

線程調度器是一個操做系統服務,它負責爲 Runnable 狀態的線程分配 CPU 時間。一旦咱們建立一個線程並啓動它,它的執行便依賴於線程調度器的實現。時間分片是指將可用的 CPU 時間分配給可用的 Runnable 線程的過程。分配 CPU 時間能夠基於線程優先級或者線程等待的時間。線程調度並不受到 Java 虛擬機控制,因此由應用程序來控制它是更好的選擇(也就是說不要讓你的程序依賴於線程的優先級)。 

4一、什麼是自旋 

不少 synchronized 裏面的代碼只是一些很簡單的代碼,執行時間很是快,此時等待的線程都加鎖多是一種不太值得的操做,由於線程阻塞涉及到用戶態和內核態切換的問題。既然 synchronized 裏面的代碼執行得很是快,不妨讓等待鎖的線程不要被阻塞,而是在synchronized 的邊界作忙循環,這就是自旋。若是作了屢次忙循環發現尚未得到鎖,再阻塞,這樣多是一種更好的策略。

4二、Java Concurrency API 中的 Lock 接口(Lock interface)是什麼?對比同步它有什麼優點? 

Lock 接口比同步方法和同步塊提供了更具擴展性的鎖操做。他們容許更靈活的結構,能夠具備徹底不一樣的性質,而且能夠支持多個相關類的條件對象。它的優點有:
  1. 可使鎖更公平
  2. 可使線程在等待鎖的時候響應中斷
  3. 可讓線程嘗試獲取鎖,並在沒法獲取鎖的時候當即返回或者等待一段時間
  4. 能夠在不一樣的範圍,以不一樣的順序獲取和釋放鎖

4三、單例模式的線程安全性

老生常談的問題了,首先要說的是單例模式的線程安全意味着:某個類的實例在多線程環境下只會被建立一次出來。單例模式有不少種的寫法,我總結一下:
  1. 餓漢式單例模式的寫法:線程安全
  2. 懶漢式單例模式的寫法:非線程安全
  3. 雙檢鎖單例模式的寫法:線程安全

4四、Semaphore 有什麼做用

Semaphore 就是一個信號量,它的做用是限制某段代碼塊的併發數。Semaphore有一個構造函數,能夠傳入一個 int 型整數 n,表示某段代碼最多隻有 n 個線程能夠訪問,若是超出了 n,那麼請等待,等到某個線程執行完畢這段代碼塊,下一個線程再進入。由此能夠看出若是 Semaphore 構造函數中傳入的 int 型整數 n=1,至關於變成了一個 synchronized 了。

4五、Executors 類是什麼?

Executors 爲 Executor,ExecutorService,ScheduledExecutorService,ThreadFactory 和 Callable 類提供了一些工具方法。Executors 能夠用於方便的建立線程池

4六、線程類的構造方法、靜態塊是被哪一個線程調用的

這是一個很是刁鑽和狡猾的問題。請記住:線程類的構造方法、靜態塊是被 new這個線程類所在的線程所調用的,而 run 方法裏面的代碼纔是被線程自身所調用的。 
若是說上面的說法讓你感到困惑,那麼我舉個例子,假設 Thread2 中 new 了Thread1,main 函數中 new 了 Thread2,那麼:
一、Thread2 的構造方法、靜態塊是 main 線程調用的,Thread2 的 run()方法是
Thread2 本身調用的
二、Thread1 的構造方法、靜態塊是 Thread2 調用的,Thread1 的 run()方法是
Thread1 本身調用的

4七、同步方法和同步塊,哪一個是更好的選擇? 

同步塊,這意味着同步塊以外的代碼是異步執行的,這比同步整個方法更提高代碼的效率。請知道一條原則:同步的範圍越小越好。 

4八、Java 線程數過多會形成什麼異常?

一、線程的生命週期開銷很是高

二、消耗過多的 CPU 資源

若是可運行的線程數量多於可用處理器的數量,那麼有線程將會被閒置。大量空
閒的線程會佔用許多內存,給垃圾回收器帶來壓力,並且大量的線程在競爭 CPU
資源時還將產生其餘性能的開銷。

三、下降穩定性

JVM 在可建立線程的數量上存在一個限制,這個限制值將隨着平臺的不一樣而不一樣,
而且承受着多個因素制約,包括 JVM 的啓動參數、Thread 構造函數中請求棧的
大小,以及底層操做系統對線程的限制等。若是破壞了這些限制,那麼可能拋出
OutOfMemoryError 異常。
相關文章
相關標籤/搜索