Java多线程面试题与答案
生活随笔
收集整理的這篇文章主要介紹了
Java多线程面试题与答案
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
線程
線程與進程的區別是什么?
- 進程指的是應用程序在操作系統中執行的副本(系統分配資源的最小單位),線程是程序執行的最小單位;
- 進程使用獨立的數據空間,而線程共享進程的數據空間。
線程狀態圖
多線程會帶來哪些性能問題?
- 調度開銷,一般線程數往往大于CPU核心數,這樣操作系統再執行線程時就會出現上下文切換,從而產生一定性能開銷;
- 協作開銷,為了保證線程之間共享變量的線程安全,有可能會禁用編譯器和CPU的重排序等優化,還可能會頻繁的將工作內存刷新到主內存,主內存再同步給工作內存,這些開銷都是單線程下不存在的。
JMM內存模型
什么是JMM內存模型?
- JMM 是和多線程相關的一組規范,需要各個 JVM 的實現來遵守 JMM 規范;
- JMM 與處理器、緩存、并發、編譯器有關。它解決了 CPU 多級緩存、處理器優化、指令重排等導致的結果不可預期的問題。
什么是指令重排序?有什么好處?
- 重排序是指編譯器、JVM 或者 CPU 為了提高執行效率,對于實際指令執行的順序進行調整;
- 重排序通過減少執行指令,從而提高整體的運行速度。
什么是內存可見性問題?
- 共享變量的值已經被第 1 個線程修改了,但是其他線程卻看不到。
主內存和工作內存的關系是什么?
- 所有的變量都存儲在主內存中,同時每個線程擁有自己獨立的工作內存,而工作內存中的變量的內容是主內存中該變量的拷貝;
- 線程不能直接讀 / 寫主內存中的變量,但可以操作自己工作內存中的變量,然后再同步到主內存中,這樣,其他線程就可以看到本次修改;
- 主內存是由多個線程所共享的,但線程間不共享各自的工作內存,如果線程間需要通信,則必須借助主內存中轉來完成。
什么是 happens-before 關系?
- 如果第一個操作和第二個操作之間滿足 happens-before 關系,那么我們就說第一個操作對于第二個操作一定是可見的;
volatile
volatile的作用是什么?
- 保證內存可見性以及多線程之間操作的有序性
volatile如何保證可見性?
- volatile變量修飾的共享變量,在進行寫操作的時候會多出一個lock前綴的匯編指令,當對其進行寫操作時,JVM就會向處理器發送一條Lock前綴的指令,把這個變量所在的緩存行的數據寫回到系統內存。然后處理器會根據MESI緩存一致性協議來保證多CPU下的各個高速緩存中的數據的一致性。
volatile是否可以保證原子性?
- volatile是一種輕量級的同步機制,它主要有兩個特性:
- 保證共享變量對所有線程的可見性;
- 禁止指令重排序優化;
- 同時需要注意的是,在多線程場景下,如果僅僅是賦值操作,volatile可以保證原子性,但是像num++這種復合操作(取值、計算、賦值),volatile無法保證其原子性。
synchronized
synchronized有幾種使用方式?
- 類、方法、代碼塊
synchronized的底層實現原理是什么?
- 每個Object對象中都內置了一個Monitor監視器,通過指令Monitor.enter和Monitor.exit進行加鎖和釋放鎖,加鎖失敗的線程會被加入到一個同步隊列中,當鎖被釋放時再重新競爭鎖。
JVM對synchronized做了哪些優化?
- 無鎖 -> 偏向鎖 -> 輕量級鎖 -> 重量級鎖
描述鎖升級的過程
- 偏向鎖升級輕量級鎖:當一個對象持有偏向鎖,一旦第二個線程訪問這個對象,如果產生競爭,偏向鎖升級為輕量級鎖。
- 輕量級鎖升級重量級鎖:一般兩個線程對于同一個鎖的操作都會錯開,或者說稍微等待一下(自旋),另一個線程就會釋放鎖。但是當自旋超過一定的次數,或者一個線程在持有鎖,一個在自旋,又有第三個來訪時,輕量級鎖膨脹為重量級鎖,重量級鎖使除了擁有鎖的線程以外的線程都阻塞,防止CPU空轉。
wait和notify為什么需要在synchronized里面?
- wait方法的語義有兩個,一個是釋放當前的對象鎖、另一個是使得當前線程進入阻塞隊列,而這些操作都和監視器是相關的,所以wait必須要獲得一個監視器鎖。
- 而對于notify來說也是一樣,它是喚醒一個線程,既然要去喚醒,首先得知道它在哪里,所以就必須要找到這個對象獲取到這個對象的鎖,然后到這個對象的等待隊列中去喚醒一個線程。
wait/notify 和 sleep 方法的區別是什么?
- wait 方法必須在 synchronized 保護的代碼中使用,而 sleep 方法并沒有這個要求;
- 在同步代碼中執行 sleep 方法時,并不會釋放 monitor 鎖,但執行 wait 方法時會主動釋放 monitor 鎖;
- sleep 方法中會要求必須定義一個時間,時間到期后會主動恢復,而對于沒有參數的 wait 方法而言,意味著永久等待,直到被中斷或被喚醒才能恢復,它并不會主動恢復;
- wait/notify 是 Object 類的方法,而 sleep 是 Thread 類的方法。
為什么 wait/notify/notifyAll 被定義在 Object 類中,而 sleep 定義在 Thread 類中?
- 因為 Java 中每個對象都有一把稱之為 monitor 監視器的鎖,鎖信息保存在對象頭中,wait/notify/notifyAll 都是鎖級別的操作,所以把它們定義在 Object 類中是最合適;
- 如果把 wait/notify/notifyAll 方法定義在 Thread 類中,會帶來很大的局限性,比如一個線程可能持有多把鎖,以便實現相互配合的復雜邏輯,假設此時 wait 方法定義在 Thread 類中,如何實現讓一個線程持有多把鎖呢?又如何明確線程等待的是哪把鎖呢?既然我們是讓當前線程去等待某個對象的鎖,自然應該通過操作對象來實現,而不是操作線程。
synchronize與volatile的區別是什么?
- volatile 可以看作是一個輕量版的 synchronized,比如一個共享變量如果自始至終只被各個線程賦值和讀取,而沒有其他操作的話,那么就可以用 volatile 來代替 synchronized 或者代替原子變量,足以保證線程安全;
- volatile無法保證對“i++”一類復合操作(包括取值、計算、賦值)的原子性和互斥性,它保證了變量間的可見性,并禁用了指令重排序;
- synchronize沒有禁用指令重排序,這也是單例double check模式,對象必須用volatile修飾的原因。
AQS
什么是AQS,內部組成有哪些?
- AQS提供了一個FIFO雙向隊列,可以看做是一個用來實現鎖以及其他需要同步功能的框架。
- AQS主要由三部分組成:
- 第一個是 state,它是一個數值,在不同的類中表示不同的含義,往往代表一種狀態;
- 第二個是一個FIFO的隊列,該隊列用來存放阻塞狀態的線程;
- 第三個是“獲取/釋放”的相關方法,需要利用 AQS 的工具類根據自己的邏輯去實現。
AQS的底層結構具體是怎樣的?
- 底層是由head節點、tail節點、雙向鏈表組成的雙向隊列;
- head與tail節點主要負責節點的出隊與入隊,時間復雜度O(1);
- 之所以使用雙向鏈表而不是單向鏈表,是因為AQS考慮到高并發的場景下,節點的狀態時刻都有可能發生變化,當前節點的一些動作需要依賴前序節點的狀態,例如:
- 只有當前節點的prev節點為head時,才有資格參與鎖競爭;
- 當前節點進入阻塞之前需要判斷該節點的prev節點的狀態是否為SIGNAL(節點的線程釋放或被取消會通知后繼節點)。
AQS解決了哪些問題?
- 狀態的原子性管理;
- 線程的阻塞與解除阻塞;
- 隊列的管理。
AQS中state的應用有哪些?
- 對于ReentrantLock,持有鎖的線程每次lock重入,state+1,每次unlock,state -1,只有state = 0才表示徹底釋放鎖,其它線程才可以獲取;
- 對于Semaphore,acquire 方法代表獲取許可,此時能不能獲取許可取決于state的值是否足夠,如果足夠state值會減掉對應的許可數量,如果不夠則會進入阻塞,release方法代表釋放許可,state值會增加直到定義的上限值;
- 對于CountDownLatch,await方法會判斷state值是否為0,不為0則進入阻塞等待,直到其它線程通過countDown方法將state減為0才會執行;
- 對于CyclicBarrier,線程調用await方法state會+1,如果state值小于初始設置的閾值,線程阻塞等待,直到state累加等于該閾值,所有等待的線程會一起釋放,同時state會清0。
Lock
Lock和synchronized的區別?
| 存在層次 | Java的關鍵字,在jvm層面上 | 是一個接口 |
| 鎖的釋放 | 1、以獲取鎖的線程執行完同步代碼,釋放鎖;2、線程執行發生異常,jvm會讓線程釋放鎖。 | 必須在finally中釋放鎖,不然容易造成線程死鎖 |
| 鎖的獲取 | 假設A線程獲得鎖,B線程等待。如果A線程阻塞,B線程會一直等待。 | Lock有多種獲取鎖的方式,如lock、tryLock |
| 鎖狀態 | 無法判斷,只能阻塞 | 可以判斷;tryLock();tryLock(long time, TimeUnit unit);可避免死鎖。 |
| 鎖類型 | 可重入,非公平,不可中斷 | 可重入,可公平(兩者皆可)可中斷:lockInterruptibly(); |
| 功能 | 功能單一 | API豐富;tryLock();tryLock(long time, TimeUnit unit);可避免死鎖。 |
描述Lock的加鎖的全流程
- 當一個線程成功地獲取了同步狀態(或者鎖),其他線程將無法獲取,轉而被構造成為尾節點并加入AQS同步隊列,這個過程通過CAS來保證的線程安全。
- 同步隊列遵循FIFO,首節點是獲取同步狀態成功的節點,首節點的線程在釋放同步狀態時,將會喚醒后繼節點,而后繼節點將會在獲取同步狀態成功時將自己設置為首節點。
- 設置首節點是通過獲取同步狀態成功的線程來完成的,由于只有一個線程能夠成功獲取到同步狀態,因此設置頭節點的方法并不需要使用CAS來保證,它只需要將首節點設置成為原首節點的后繼節點并斷開原首節點的next引用即可。
公平鎖與非公平鎖的區別,如何實現的?
- 非公平鎖在獲取鎖的時候,會先通過CAS進行搶占,而公平鎖則不會;
- 公平鎖會優先從同步隊列中去喚醒,這樣就保證了先到先得的順序;
- 非公平鎖的效率更高,因為喚醒線程的過程是比較耗時的,非公平鎖會利用這部分時間完成其它任務,但有可能造成鎖饑餓。
對比悲觀鎖,樂觀鎖的優點和缺點都有哪些?
- 樂觀鎖優點:
- 悲觀鎖需要遵循下面三種模式:一鎖、二讀、三更新,即使在沒有沖突的情況下,執行也會非常慢;
- 樂觀鎖本質上不是鎖,它只是一個判斷邏輯,資源沖突少的情況下,它不會產生任何開銷;
- 樂觀鎖缺點:
- 在并發量比較高的情況下,有些線程可能會一直嘗試修改某個資源,但由于沖突比較嚴重,一直更新不成功,這時候,就會給 CPU 帶來很大的壓力(并發量大可以考慮通過分段鎖的方式優化,例如LongAdder,或者直接使用悲觀鎖);
- 無法解決ABA問題,意思是指在 CAS 操作時,有其他的線程現將變量的值由 A 變成了 B,然后又改成了 A,當前線程在操作時,發現值仍然是 A,于是進行了交換操作。大部分場景下ABA問題不會給業務帶來影響,可以通過引入版本號的方式解決。
線程池
線程池的核心參數有哪些?
public ThreadPoolExecutor( int corePoolSize, // 核心線程數 int maximumPoolSize, // 最大線程數 long keepAliveTime, // 臨時線程等待時間 TimeUnit unit, // 時間單位 BlockingQueue<Runnable> workQueue, // 阻塞隊列 RejectedExecutionHandler handler) // 拒絕策略線程池執行任務的流程是什么?
拒絕策略有哪些?
- AbortPolicy(默認):丟棄任務并拋出RejectedExecutionException異常;
- DiscardPolicy:丟棄任務,但是不拋出異常;
- DiscardOldestPolicy:拋棄隊列中等待最久的任務,然后把當前任務加入到隊列中;
- CallerRunsPolicy:提交任務的主線程調用任務的run()方法,繞過線程池直接執行(這種方法不會發生數據丟失,并且可以延緩任務提交的速度,緩解線程池壓力)。
ForkJoinPool有什么特點?
- 它每個線程都有一個自己的雙端隊列來存儲分裂出來的子任務,避免了公共隊列的阻塞;
- 采用工作竊取模式,空閑線程 t1 可以幫助繁忙線程 t0 完成工作,這也是隊列設計為雙端隊列的目的,t0是按LIFO的順序處理任務,而t1 在steal t0任務時會按照FIFO的順序;
- ForkJoinPool 非常適合用于遞歸的場景,例如樹的遍歷、最優路徑搜索等場景。
ForkJoinPool與ThreadPoolExecutor區別是什么?
- ForkJoinPool中的每個線程都會有一個隊列,而ThreadPoolExecutor只有一個隊列,并根據queue類型不同,細分出各種線程池;
- ForkJoinPool能夠使用數量有限的線程來完成非常多的具有父子關系的任務,ThreadPoolExecutor中根本沒有什么父子關系任務;
- ForkJoinPool在多任務,且任務分配不均是有優勢,但是在單線程或者任務分配均勻的情況下,效率沒有ThreadPoolExecutor高。
JDK提供的線程池用到了哪些阻塞隊列?
- LinkedBlockingQueue:FixedThreadPool 和 SingleThreadExector 的默認隊列,容量為 Integer.MAX_VALUE,可以認為是無界隊列,不會生成多于核心線程數的線程;
- SynchronousQueue:CachedThreadPool的默認隊列,是一種沒有容量的阻塞隊列。與FixedThreadPool正好相反,CachedThreadPool為了盡可能創建新的線程執行任務,它的最大線程數是 Integer.MAX_VALUE,隊列容量為0;
- DelayedWorkQueue:ScheduledThreadPool的默認隊列,可以周期性執行任務或延遲一定時間執行任務,DelayedWorkQueue會按照延遲的時間長短對任務排序,內部數據結構是堆(二叉樹)。
CPU核心數和線程數的關系是什么?
- 如果是CPU密集型任務,例如加密、解密、編譯、壓縮、計算等任務,一般可以考慮線程數為CPU核心數的1~2倍,具體還應該參考壓測結果;
- 如果是IO密集型任務,可參考公式:線程數 = CPU 核心數 *(1 + 線程平均等待時間 / 線程平均工作時間)。
隊列
隊列常見api的區別?
有哪些常見的阻塞隊列?
- ArrayBlockingQueue
- 最典型的有界隊列,其內部是用數組存儲元素的,不會擴容,利用 ReentrantLock 實現線程安全;
- LinkedBlockingQueue
- 內部用鏈表實現的 BlockingQueue,容量默認就為整型的最大值 Integer.MAX_VALUE,一般稱為無界隊列;
- SynchronousQueue
- 容量為0的傳遞隊列,存數據會阻塞,知道有人來存,同理取數據也會阻塞,直到有人來存;
- PriorityBlockingQueue
- 無界的優先級阻塞隊列(有初始容量,可擴容),可以通過自定義類實現 compareTo() 方法來指定元素排序規則,或者初始化時通過構造器參數 Comparator 來指定排序規則,內部的數據結構是堆;
- DelayQueue
- 無界的延遲隊列,DelayQueue 內部使用了 PriorityQueue 的能力來進行排序。
ArrayBlockingQueue的實現原理是什么?
- ArrayBlockingQueue 實現并發同步的原理就是利用 ReentrantLock 和它的兩個 Condition,讀操作和寫操作都需要先獲取到 ReentrantLock 獨占鎖才能進行下一步操作;
- 進行讀操作時如果隊列為空,線程就會進入到讀線程專屬的 notEmpty 的 Condition 的隊列中去排隊,等待寫線程寫入新的元素;
- 同理,如果隊列已滿,這個時候寫操作的線程會進入到寫線程專屬的 notFull 隊列中去排隊,等待讀線程將隊列元素移除并騰出空間。
阻塞隊列和非阻塞隊列在實現上有哪些區別?
- 阻塞隊列最主要是利用了 ReentrantLock 以及它的 Condition 來實現,而非阻塞隊列則是利用 CAS 方法實現線程安全。
多線程工具類
CountDownLatch與CyclicBarrier的區別是什么?
- 作用對象不同:CyclicBarrier 要等固定數量的線程都到達了柵欄位置才能繼續執行,而 CountDownLatch 只需等待數字倒數到 0,也就是說 CountDownLatch 作用于事件,但 CyclicBarrier 作用于線程;
- 可重用性不同:CountDownLatch 在倒數到 0 并且觸發門閂打開后,就不能再次使用了,除非新建一個新的實例;而 CyclicBarrier 可以重復使用,并不需要重建實例。CyclicBarrier 還可以隨時調用 reset 方法進行重置,如果重置時有線程已經調用了 await 方法并開始等待,那么這些線程則會拋出 BrokenBarrierException 異常。
- 執行動作不同:CyclicBarrier 有執行動作 barrierAction,而 CountDownLatch 沒這個功能。
Future
Future與CompletableFuture區別?
- 通過Future接收異步任務時,主線程需要通過get()方法去阻塞輪詢獲取結果,如果是多個任務,則需要等到所有任務完成之后才能做后續操作;
- 而通過CompletableFuture接收異步任務時,無需等待所有任務全部完成,每個任務都可以通過thenAccept、thenApply、thenCompose等方式將前面的結果交給另一個異步事件來處理,最后還可以通過allOf或anyOf等方法來匯總結果。
FutureTask的實現原理是什么?
- 當我們通過Future接收異步任務時,底層是通過其實現類FutureTask的run方法來執行任務的,run方法主要完成了以下流程:
- 檢查線程的狀態,執行用戶定義的call方法;
- 執行結束后,設置返回值或異常,并更新線程狀態,然后喚醒隊列中阻塞等待獲取結果的線程;
- 當其它線程通過future.get獲取結果時:
- 首先根據狀態判斷任務是否完成,如果已經完成則直接返回;
- 如果沒有完成則會阻塞當前線程,并將其加入到阻塞隊列(如果沒有指定阻塞時間則一直阻塞知道任務完成喚醒);
- 當任務完成時,阻塞的線程會被喚醒,拿到結果后返回;
ThreadLocal
ThreadLocal的作用與使用場景是什么?
- ThreadLocal 用作保存每個線程獨享的對象,為每個線程都創建一個副本,這樣每個線程都可以修改自己所擁有的副本, 而不會影響其他線程的副本,確保了線程安全。
- ThreadLocal 用作每個線程內需要獨立保存信息,以便供其他方法更方便地獲取該信息的場景。每個線程獲取到的信息可能都是不一樣的,前面執行的方法保存了信息后,后續方法可以通過 ThreadLocal 直接獲取到,避免了傳參,類似于全局變量的概念。
ThreadLocal與Thread的關系是什么?
一個 Thread 里面只有一個ThreadLocalMap ,而在一個 ThreadLocalMap 里面卻可以有很多的 ThreadLocal,每一個 ThreadLocal 都對應一個 value。因為一個 Thread 是可以調用多個 ThreadLocal 的,所以 Thread 內部就采用了 ThreadLocalMap 這樣 Map 的數據結構來存放 ThreadLocal 和 value。
ThreadLocal與Synchronized的區別是什么?
- ThreadLocal 是通過讓每個線程獨享自己的副本,避免了資源的競爭。
- synchronized 主要用于臨界資源的分配,在同一時刻限制最多只有一個線程能訪問該資源。
- ThreadLocal 并不是用來解決共享資源的多線程訪問的問題,因為每個線程中的資源只是副本,并不共享。因此ThreadLocal適合作為線程上下文變量,簡化線程內傳參。
ThreadLocal為什么可能產生內存泄漏,如何避免?
- 通過ThreadLocalMap的源碼可以看到,Entry中的key被定義為弱引用類型,當發生GC時,key會被直接回收,無需手動清理。
- 而value屬于強引用類型,被當前的Thread對象關聯,所以說value的回收取決于Thread對象的生命周期。
- 如果說一個線程執行完畢,線程Thread隨之被釋放,那么value便不存在內存泄漏的問題。
- 然而,我們一般會通過線程池的方式來復用Thread對象來節省資源,這就會導致一個Thread對象的生命周期會非常長,隨著任務的執行,value就有可能越來越多且無法釋放,最終導致內存泄漏。
- 因此,我們在使用完ThreadLocal變量后,要手動調用remove()方法來清理ThreadLocalMap(一般在finally代碼塊中)。
子線程如何共享主線程的ThreadLocal變量?
- 因為ThreadLocal變量保存在當前線程的成員變量ThreadLocalMap中,新創建子線程后無法再次使用父線程的ThreadLocal變量;
- 為了解決子線程復用主線程ThreadLocal的問題,Thread類中還有另一個ThreadLocalMap:inheritableThreadLocals,里面保存的是InheritableThreadLocal,它是ThreadLocal的子類,Thread類初始化時會默認從父線程繼承inheritableThreadLocals;
- 因此我們可以用InheritableThreadLocal代替ThreadLocal實現父子線程共享線程變量的問題。
總結
以上是生活随笔為你收集整理的Java多线程面试题与答案的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: spring boot报FileSize
- 下一篇: erlang精要(2)-数制