併發程式設計44題(含答案)
1、併發程式設計三要素?
1)原子性
原子性指的是一個或者多個操作,要麼全部執行並且在執行的過程中不被其他操作打斷,要麼就全部都不執行。
2)可見性
可見性指多個執行緒操作一個共享變數時,其中一個執行緒對變數進行修改後,其他執行緒可以立即看到修改的結果。
實現可見性的方法:
synchronized或者Lock:保證同一個時刻只有一個執行緒獲取鎖執行程式碼,鎖釋放之前把最新的值重新整理到主記憶體,實現可見性。
3)有序性
有序性,即程式的執行順序按照程式碼的先後順序來執行。
2、多執行緒的價值?
1)發揮多核CPU的優勢
多執行緒,可以真正發揮出多核CPU的優勢來,達到充分利用CPU的目的,採用多執行緒的方式去同時完成幾件事情而不互相干擾。
2)防止阻塞
從程式執行效率的角度來看,單核CPU不但不會發揮出多執行緒的優勢,反而會因為在單核CPU上執行多執行緒導致執行緒上下文的切換,而降低程式整體的效率。但是單核CPU我們還是要應用多執行緒,就是為了防止阻塞。試想,如果單核CPU使用單執行緒,那麼只要這個執行緒阻塞了,比方說遠端讀取某個資料吧,對端遲遲未返回又沒有設定超時時間,那麼你的整個程式在資料返回回來之前就停止運行了。多執行緒可以防止這個問題,多條執行緒同時執行,哪怕一條執行緒的程式碼執行讀取資料阻塞,也不會影響其它任務的執行。
3)便於建模
這是另外一個沒有這麼明顯的優點了。假設有一個大的任務A,單執行緒程式設計,那麼就要考慮很多,建立整個程式模型比較麻煩。但是如果把這個大的任務A分解成幾個小任務,任務B、任務C、任務D,分別建立程式模型,並通過多執行緒分別執行這幾個任務,那就簡單很多了。
3、建立執行緒的有哪些方式?
1)繼承Thread類建立執行緒類
2)通過Runnable介面建立執行緒類
3)通過Callable和Future建立執行緒
4.建立執行緒的三種方式的對比?
1)採用實現Runnable、Callable介面的方式建立多執行緒。
優勢是:
執行緒類只是實現了Runnable介面或Callable介面,還可以繼承其他類。
在這種方式下,多個執行緒可以共享同一個target物件,所以非常適合多個相同執行緒來處理同一份資源的情況,從而可以將CPU、程式碼和資料分開,形成清晰的模型,較好地體現了面向物件的思想。
劣勢是:
程式設計稍微複雜,如果要訪問當前執行緒,則必須使用Thread.currentThread()方法。
2)使用繼承Thread類的方式建立多執行緒
優勢是:
編寫簡單,如果需要訪問當前執行緒,則無需使用Thread.currentThread()方法,直接使用this即可獲得當前執行緒。
劣勢是:
執行緒類已經繼承了Thread類,所以不能再繼承其他父類。
3)Runnable和Callable的區別
- Callable規定(重寫)的方法是call(),Runnable規定(重寫)的方法是run()。
- Callable的任務執行後可返回值,而Runnable的任務是不能返回值的。
- Call方法可以丟擲異常,run方法不可以。
- 執行Callable任務可以拿到一個Future物件,表示非同步計算的結果。它提供了檢查計算是否完成的方法,以等待計算的完成,並檢索計算的結果。通過Future物件可以瞭解任務執行情況,可取消任務的執行,還可獲取執行結果。
5、執行緒的狀態流轉圖
執行緒的生命週期及五種基本狀態:
Java執行緒具有五中基本狀態
1)新建狀態(New):當執行緒物件對建立後,即進入了新建狀態,如:Thread t = new MyThread();
2)就緒狀態(Runnable):當呼叫執行緒物件的start()方法(t.start();),執行緒即進入就緒狀態。處於就緒狀態的執行緒,只是說明此執行緒已經做好了準備,隨時等待CPU排程執行,並不是說執行了t.start()此執行緒立即就會執行;
3)執行狀態(Running):當CPU開始排程處於就緒狀態的執行緒時,此時執行緒才得以真正執行,即進入到執行狀態。注:就 緒狀態是進入到執行狀態的唯一入口,也就是說,執行緒要想進入執行狀態執行,首先必須處於就緒狀態中;
4)阻塞狀態(Blocked):處於執行狀態中的執行緒由於某種原因,暫時放棄對CPU的使用權,停止執行,此時進入阻塞狀態,直到其進入到就緒狀態,才 有機會再次被CPU呼叫以進入到執行狀態。根據阻塞產生的原因不同,阻塞狀態又可以分為三種:
1.等待阻塞:執行狀態中的執行緒執行wait()方法,使本執行緒進入到等待阻塞狀態;
2.同步阻塞 -- 執行緒在獲取synchronized同步鎖失敗(因為鎖被其它執行緒所佔用),它會進入同步阻塞狀態;
3.其他阻塞 -- 通過呼叫執行緒的sleep()或join()或發出了I/O請求時,執行緒會進入到阻塞狀態。當sleep()狀態超時、join()等待執行緒終止或者超時、或者I/O處理完畢時,執行緒重新轉入就緒狀態。
5)死亡狀態(Dead):執行緒執行完了或者因異常退出了run()方法,該執行緒結束生命週期。
6.什麼是執行緒池? 有哪幾種建立方式?
執行緒池就是提前建立若干個執行緒,如果有任務需要處理,執行緒池裡的執行緒就會處理任務,處理完之後執行緒並不會被銷燬,而是等待下一個任務。由於建立和銷燬執行緒都是消耗系統資源的,所以當你想要頻繁的建立和銷燬執行緒的時候就可以考慮使用執行緒池來提升系統的效能。
java 提供了一個 java.util.concurrent.Executor介面的實現用於建立執行緒池。
四種執行緒池的建立:
(1)newCachedThreadPool建立一個可快取執行緒池
(2)newFixedThreadPool 建立一個定長執行緒池,可控制執行緒最大併發數。
(3)newScheduledThreadPool 建立一個定長執行緒池,支援定時及週期性任務執行。
(4)newSingleThreadExecutor 建立一個單執行緒化的執行緒池,它只會用唯一的工作執行緒來執行任務。
7.執行緒池的優點?
1)重用存在的執行緒,減少物件建立銷燬的開銷。
2)可有效的控制最大併發執行緒數,提高系統資源的使用率,同時避免過多資源競爭,避免堵塞。
3)提供定時執行、定期執行、單執行緒、併發數控制等功能。
8.Java中的同步集合與併發集合有什麼區別?
同步集合類:
- Vector
- Stack
- HashTable
- Collections.synchronized方法生成
併發集合類:
- ConcurrentHashMap
- CopyOnWriteArrayList
- CopyOnWriteArraySet等
9.同步集合與併發集合的區別
同步集合與併發集合都為多執行緒和併發提供了合適的執行緒安全的集合,不過併發集合的可擴充套件性更高。同步集合比並發集合會慢得多,主要原因是鎖,同步集合會對整個May或List加鎖,而併發集合例如ConcurrentHashMap, 把整個Map 劃分成幾個片段,只對相關的幾個片段上鎖,同時允許多執行緒訪問其他未上鎖的片段(JDK1.8版本底層加入了紅黑樹)。
10.Java常用的併發工具類有哪些?
- CountDownLatch
- CyclicBarrier
- Semaphore
- Exchanger
11.CyclicBarrier和CountDownLatch的應用場景?
CountDownLatch : 一個執行緒(或者多個), 等待另外N個執行緒完成某個事情之後才能執行。CyclicBarrier : N個執行緒相互等待,任何一個執行緒完成之前,所有的執行緒都必須等待。
CountDownLatch的使用場景:
在一些應用場合中,需要等待某個條件達到要求後才能做後面的事情;同時當執行緒都完成後也會觸發事件,以便進行後面的操作, 這個時候就可以使用CountDownLatch。
CyclicBarrier 使用場景
CyclicBarrier可以用於多執行緒計算資料,最後合併計算結果的應用場景。
12.CyclicBarrier和CountDownLatch的區別
1)CountDownLatch簡單的說就是一個執行緒等待,直到他所等待的其他執行緒都執行完成並且呼叫countDown()方法發出通知後,當前執行緒才可以繼續執行。
2)cyclicBarrier是所有執行緒都進行等待,直到所有執行緒都準備好進入await()方法之後,所有執行緒同時開始執行!
3)CountDownLatch的計數器只能使用一次。而CyclicBarrier的計數器可以使用reset() 方法重置。所以CyclicBarrier能處理更為複雜的業務場景,比如如果計算髮生錯誤,可以重置計數器,並讓執行緒們重新執行一次。
4)CyclicBarrier還提供其他有用的方法,比如getNumberWaiting方法可以獲得CyclicBarrier阻塞的執行緒數量。isBroken方法用來知道阻塞的執行緒是否被中斷。如果被中斷返回true,否則返回false。
13.synchronized的作用?
在Java中,synchronized關鍵字是用來控制執行緒同步的,就是在多執行緒的環境下,控制synchronized程式碼段不被多個執行緒同時執行。
synchronized既可以加在一段程式碼上,也可以加在方法上。
14.volatile關鍵字的作用
對於可見性,Java提供了volatile關鍵字來保證可見性。
當一個共享變數被volatile修飾時,它會保證修改的值會立即被更新到主存,當有其他執行緒需要讀取時,它會去記憶體中讀取新值。
從實踐角度而言,volatile的一個重要作用就是和CAS結合,保證了原子性,詳細的可以參見java.util.concurrent.atomic包下的類,比如AtomicInteger。
15.什麼是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)。
16. CAS的問題
1)CAS容易造成ABA問題。一個執行緒a將數值改成了b,接著又改成了a,此時CAS認為是沒有變化,其實是已經變化過了,而這個問題的解決方案可以使用版本號標識,每操作一次version加1。在java5中,已經提供了AtomicStampedReference來解決問題。
2) 不能保證程式碼塊的原子性
CAS機制所保證的知識一個變數的原子性操作,而不能保證整個程式碼塊的原子性。比如需要保證3個變數共同進行原子性的更新,就不得不使用synchronized了。
3)CAS造成CPU利用率增加。之前說過了CAS裡面是一個迴圈判斷的過程,如果執行緒一直沒有獲取到狀態,cpu資源會一直被佔用。
17.什麼是Future?
在併發程式設計中,我們經常用到非阻塞的模型,在之前的多執行緒的三種實現中,不管是繼承thread類還是實現runnable介面,都無法保證獲取到之前的執行結果。通過實現Callback介面,並用Future可以來接收多執行緒的執行結果。
Future表示一個可能還沒有完成的非同步任務的結果,針對這個結果可以新增Callback以便在任務執行成功或失敗後作出相應的操作。
18.什麼是AQS
AQS是AbustactQueuedSynchronizer的簡稱,它是一個Java提高的底層同步工具類,用一個int型別的變量表示同步狀態,並提供了一系列的CAS操作來管理這個同步狀態。
AQS是一個用來構建鎖和同步器的框架,使用AQS能簡單且高效地構造出應用廣泛的大量的同步器,比如我們提到的ReentrantLock,Semaphore,其他的諸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基於AQS的。
AQS支援兩種同步方式:
1.獨佔式
2.共享式
這樣方便使用者實現不同型別的同步元件,獨佔式如ReentrantLock,共享式如Semaphore,CountDownLatch,組合式的如ReentrantReadWriteLock。總之,AQS為使用提供了底層支撐,如何組裝實現,使用者可以自由發揮。
19.ReadWriteLock是什麼
首先明確一下,不是說ReentrantLock不好,只是ReentrantLock某些時候有侷限。如果使用ReentrantLock,可能本身是為了防止執行緒A在寫資料、執行緒B在讀資料造成的資料不一致,但這樣,如果執行緒C在讀資料、執行緒D也在讀資料,讀資料是不會改變資料的,沒有必要加鎖,但是還是加鎖了,降低了程式的效能。
因為這個,才誕生了讀寫鎖ReadWriteLock。ReadWriteLock是一個讀寫鎖介面,ReentrantReadWriteLock是ReadWriteLock介面的一個具體實現,實現了讀寫的分離,讀鎖是共享的,寫鎖是獨佔的,讀和讀之間不會互斥,讀和寫、寫和讀、寫和寫之間才會互斥,提升了讀寫的效能。
20.FutureTask是什麼
這個其實前面有提到過,FutureTask表示一個非同步運算的任務。FutureTask裡面可以傳入一個Callable的具體實現類,可以對這個非同步運算的任務的結果進行等待獲取、判斷是否已經完成、取消任務等操作。當然,由於FutureTask也是Runnable介面的實現類,所以FutureTask也可以放入執行緒池中。
21.synchronized和ReentrantLock的區別
synchronized是和if、else、for、while一樣的關鍵字,ReentrantLock是類,這是二者的本質區別。既然ReentrantLock是類,那麼它就提供了比synchronized更多更靈活的特性,可以被繼承、可以有方法、可以有各種各樣的類變數,ReentrantLock比synchronized的擴充套件性體現在幾點上:
(1)ReentrantLock可以對獲取鎖的等待時間進行設定,這樣就避免了死鎖
(2)ReentrantLock可以獲取各種鎖的資訊
(3)ReentrantLock可以靈活地實現多路通知
另外,二者的鎖機制其實也是不一樣的。ReentrantLock底層呼叫的是Unsafe的park方法加鎖,synchronized操作的應該是物件頭中mark word,這點我不能確定。
22.什麼是樂觀鎖和悲觀鎖
(1)樂觀鎖:就像它的名字一樣,對於併發間操作產生的執行緒安全問題持樂觀狀態,樂觀鎖認為競爭不總是會發生,因此它不需要持有鎖,將比較-替換這兩個動作作為一個原子操作嘗試去修改記憶體中的變數,如果失敗則表示發生衝突,那麼就應該有相應的重試邏輯。
(2)悲觀鎖:還是像它的名字一樣,對於併發間操作產生的執行緒安全問題持悲觀狀態,悲觀鎖認為競爭總是會發生,因此每次對某資源進行操作時,都會持有一個獨佔的鎖,就像synchronized,不管三七二十一,直接上了鎖就操作資源了。
23.執行緒B怎麼知道執行緒A修改了變數
- volatile修飾變數
- synchronized修飾修改變數的方法
- wait/notify
- while輪詢
24.synchronized、volatile、CAS比較
- synchronized是悲觀鎖,屬於搶佔式,會引起其他執行緒阻塞。
- volatile提供多執行緒共享變數可見性和禁止指令重排序優化。
- CAS是基於衝突檢測的樂觀鎖(非阻塞)
25.sleep方法和wait方法有什麼區別?
這個問題常問,sleep方法和wait方法都可以用來放棄CPU一定的時間,不同點在於如果執行緒持有某個物件的監視器,sleep方法不會放棄這個物件的監視器,wait方法會放棄這個物件的監視器
26.ThreadLocal是什麼?有什麼用?
ThreadLocal是一個本地執行緒副本變數工具類。主要用於將私有執行緒和該執行緒存放的副本物件做一個對映,各個執行緒之間的變數互不干擾,在高併發場景下,可以實現無狀態的呼叫,特別適用於各個執行緒依賴不通的變數值完成操作的場景。
簡單說ThreadLocal就是一種以空間換時間的做法,在每個Thread裡面維護了一個以開地址法實現的ThreadLocal.ThreadLocalMap,把資料進行隔離,資料不共享,自然就沒有執行緒安全方面的問題了。
27.為什麼wait()方法和notify()/notifyAll()方法要在同步塊中被呼叫
這是JDK強制的,wait()方法和notify()/notifyAll()方法在呼叫前都必須先獲得物件的鎖
28.多執行緒同步有哪幾種方法?
Synchronized關鍵字,Lock鎖實現,分散式鎖等。
29.執行緒的排程策略
執行緒排程器選擇優先順序最高的執行緒執行,但是,如果發生以下情況,就會終止執行緒的執行:
(1)執行緒體中呼叫了yield方法讓出了對cpu的佔用權利
(2)執行緒體中呼叫了sleep方法使執行緒進入睡眠狀態
(3)執行緒由於IO操作受到阻塞
(4)另外一個更高優先順序執行緒出現
(5)在支援時間片的系統中,該執行緒的時間片用完
30.ConcurrentHashMap的併發度是什麼
ConcurrentHashMap的併發度就是segment的大小,預設為16,這意味著最多同時可以有16條執行緒操作ConcurrentHashMap,這也是ConcurrentHashMap對Hashtable的最大優勢,任何情況下,Hashtable能同時有兩條執行緒獲取Hashtable中的資料嗎?
31.Java死鎖以及如何避免?
Java中的死鎖是一種程式設計情況,其中兩個或多個執行緒被永久阻塞,Java死鎖情況出現至少兩個執行緒和兩個或更多資源。
Java發生死鎖的根本原因是:在申請鎖時發生了交叉閉環申請。
死鎖的原因
1)是多個執行緒涉及到多個鎖,這些鎖存在著交叉,所以可能會導致了一個鎖依賴的閉環。
例如:執行緒在獲得了鎖A並且沒有釋放的情況下去申請鎖B,這時,另一個執行緒已經獲得了鎖B,在釋放鎖B之前又要先獲得鎖A,因此閉環發生,陷入死鎖迴圈。
2)預設的鎖申請操作是阻塞的。
所以要避免死鎖,就要在一遇到多個物件鎖交叉的情況,就要仔細審查這幾個物件的類中的所有方法,是否存在著導致鎖依賴的環路的可能性。 總之是儘量避免在一個同步方法中呼叫其它物件的延時方法和同步方法。
32.怎麼喚醒一個阻塞的執行緒
如果執行緒是因為呼叫了wait()、sleep()或者join()方法而導致的阻塞,可以中斷執行緒,並且通過丟擲InterruptedException來喚醒它;如果執行緒遇到了IO阻塞,無能為力,因為IO是作業系統實現的,Java程式碼並沒有辦法直接接觸到作業系統。
33.不可變物件對多執行緒有什麼幫助
前面有提到過的一個問題,不可變物件保證了物件的記憶體可見性,對不可變物件的讀取不需要進行額外的同步手段,提升了程式碼執行效率。
34.什麼是多執行緒的上下文切換
多執行緒的上下文切換是指CPU控制權由一個已經正在執行的執行緒切換到另外一個就緒並等待獲取CPU執行權的執行緒的過程。
35.如果你提交任務時,執行緒池佇列已滿,這時會發生什麼
這裡區分一下:
- 如果使用的是無界佇列LinkedBlockingQueue,也就是無界佇列的話,沒關係,繼續新增任務到阻塞佇列中等待執行,因為LinkedBlockingQueue可以近乎認為是一個無窮大的佇列,可以無限存放任務
- 如果使用的是有界佇列比如ArrayBlockingQueue,任務首先會被新增到ArrayBlockingQueue中,ArrayBlockingQueue滿了,會根據maximumPoolSize的值增加執行緒數量,如果增加了執行緒數量還是處理不過來,ArrayBlockingQueue繼續滿,那麼則會使用拒絕策略RejectedExecutionHandler處理滿了的任務,預設是AbortPolicy
36.Java中用到的執行緒排程演算法是什麼
搶佔式。一個執行緒用完CPU之後,作業系統會根據執行緒優先順序、執行緒飢餓情況等資料算出一個總的優先順序並分配下一個時間片給某個執行緒執行。
37.什麼是執行緒排程器(Thread Scheduler)和時間分片(Time Slicing)?
執行緒排程器是一個作業系統服務,它負責為Runnable狀態的執行緒分配CPU時間。一旦我們建立一個執行緒並啟動它,它的執行便依賴於執行緒排程器的實現。時間分片是指將可用的CPU時間分配給可用的Runnable執行緒的過程。分配CPU時間可以基於執行緒優先順序或者執行緒等待的時間。執行緒排程並不受到Java虛擬機器控制,所以由應用程式來控制它是更好的選擇(也就是說不要讓你的程式依賴於執行緒的優先順序)。
38.Java Concurrency API中的Lock介面(Lock interface)是什麼?對比同步它有什麼優勢?
Lock介面比同步方法和同步塊提供了更具擴充套件性的鎖操作。他們允許更靈活的結構,可以具有完全不同的性質,並且可以支援多個相關類的條件物件。
它的優勢有:
- 可以使鎖更公平
- 可以使執行緒在等待鎖的時候響應中斷
- 可以讓執行緒嘗試獲取鎖,並在無法獲取鎖的時候立即返回或者等待一段時間
- 可以在不同的範圍,以不同的順序獲取和釋放鎖
39.單例模式的執行緒安全性
老生常談的問題了,首先要說的是單例模式的執行緒安全意味著:某個類的例項在多執行緒環境下只會被建立一次出來。單例模式有很多種的寫法,我總結一下:
(1)餓漢式單例模式的寫法:執行緒安全
(2)懶漢式單例模式的寫法:非執行緒安全
(3)雙檢鎖單例模式的寫法:執行緒安全
40.Semaphore有什麼作用
Semaphore就是一個訊號量,它的作用是限制某段程式碼塊的併發數。Semaphore有一個建構函式,可以傳入一個int型整數n,表示某段程式碼最多隻有n個執行緒可以訪問,如果超出了n,那麼請等待,等到某個執行緒執行完畢這段程式碼塊,下一個執行緒再進入。由此可以看出如果Semaphore建構函式中傳入的int型整數n=1,相當於變成了一個synchronized了。
41.Executors類是什麼?
Executors為Executor,ExecutorService,ScheduledExecutorService,ThreadFactory和Callable類提供了一些工具方法。
Executors可以用於方便的建立執行緒池
42.執行緒類的構造方法、靜態塊是被哪個執行緒呼叫的
這是一個非常刁鑽和狡猾的問題。請記住:執行緒類的構造方法、靜態塊是被new這個執行緒類所在的執行緒所呼叫的,而run方法裡面的程式碼才是被執行緒自身所呼叫的。
如果說上面的說法讓你感到困惑,那麼我舉個例子,假設Thread2中new了Thread1,main函式中new了Thread2,那麼:
(1)Thread2的構造方法、靜態塊是main執行緒呼叫的,Thread2的run()方法是Thread2自己呼叫的
(2)Thread1的構造方法、靜態塊是Thread2呼叫的,Thread1的run()方法是Thread1自己呼叫的
43.同步方法和同步塊,哪個是更好的選擇
同步塊,這意味著同步塊之外的程式碼是非同步執行的,這比同步整個方法更提升程式碼的效率。請知道一條原則:同步的範圍越小越好。
44.Java執行緒數過多會造成什麼異常?
1)執行緒的生命週期開銷非常高
2)消耗過多的CPU資源
如果可執行的執行緒數量多於可用處理器的數量,那麼有執行緒將會被閒置。大量空閒的執行緒會佔用許多記憶體,給垃圾回收器帶來壓力,而且大量的執行緒在競爭CPU資源時還將產生其他效能的開銷。
3)降低穩定性
JVM在可建立執行緒的數量上存在一個限制,這個限制值將隨著平臺的不同而不同,並且承受著多個因素制約,包括JVM的啟動引數、Thread建構函式中請求棧的大小,以及底層作業系統對執行緒的限制等。如果破壞了這些限制,那麼可能丟擲OutOfMemoryError異常。