前言
多線程技術是日常開發、面試或者在進行系統優化是一個重難點的技術點,本文通過自己近十幾年開發經驗總結下面問題點,供給需要的夥伴學習與參考,本文主要介紹Java中多線程方面技術。
1. 什麼是進程與線程?
進程:在操作系統中能夠獨立運行,並且作爲資源分配的基本單位。它表示運行中的程序。系統運行一個程序就是一個進程從創建、運行到消亡的過程。
線程:是一個比進程更小的執行單位,能夠完成進程中的一個功能,也被稱爲輕量級進程。一個進程在其執行的過程中可以產生多個線程。
線程與進程不同的是:同類的多個線程共享進程的堆和方法區資源,但每個線程有自己的程序計數器、虛擬機棧和本地方法棧,所以系統在產生一個線程,或是在各個線程之間作切換工作時,負擔要比進程小得多。
2. 併發編程要素是什麼?
-
原子性
原子性指的是一個或者多個操作,要麼全部執行並且在執行的過程中不被其他操作打斷,要麼就全部都不執行。
-
可見性
可見性指多個線程操作一個共享變量時,其中一個線程對變量進行修改後,其他線程可以立即看到修改的結果
-
有序性
有序性,即程序的執行順序按照代碼的先後順序來執行。
3.可見性實現方式是什麼?
synchronized或者Lock:保證同一個時刻只有一個線程獲取鎖執行代碼,鎖釋放之前把最新的值刷新到主內存,實現可見性。
4. 多線程價值是什麼?
- 發揮多核CPU優勢
- 防止阻塞
- 便於建模
5. 創建線程有哪些方式?
-
繼承Thread類調用start方法創建線程
-
實現Runable接口重寫run方法創建線程
-
實現Callable接口和Future創建線程
-
通過線程池創建線程
6. 創建線程方式對比
採用Runable接口、Callable接口方式創建多線程優勢:
線程類只是實現接口,還可以繼承其他類,在這種方式下,多個線程可以共享同一個target對象,所以非常適合多個相同線程來處理同一份資源的情況,從而可以將CPU、代碼和數據分開,形成清晰的模型,較好地體現了面向對象的思想。
侷限性:
編程稍微複雜,如果要訪問當前線程,則必須調用Thread.currentThread()方法。
使用繼承Thread類的方式創建多線程優勢:
編寫簡單,如果需要訪問當前線程,則無需使用Thread.currentThread()方法,直接使用this即可獲得當前線程。
侷限性:
線程類已經繼承了Thread類,所以不能再繼承其他父類。
7. Runable接口與Callable接口區別
Runable接口規定重寫run(),Callable接口規定重寫call();
Runable任務執行無法返回值,Callable任務執行可以返回值;
Runable接口run()不能拋出異常,Callable接口call()可以拋出異常;
運行Callable任務可以拿到一個Future對象,表示異步計算的結果。它提供了檢查計算是否完成的方法,以等待計算的完成,並檢索計算的結果。通過Future對象可以瞭解任務執行情況,可取消任務的執行,還可獲取執行結果。
8. 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()方法,該線程結束生命週期.
9. Java四種線程池的創建
-
newCachedThreadPool創建一個可緩存線程池;
-
newFixedThreadPool 創建一個定長線程池,可控制線程最大併發數;
-
newScheduledThreadPool 創建一個定長線程池,支持定時及週期性任務執行;
-
newSingleThreadExecutor 創建一個單線程化的線程池,它只會用唯一的工作線程來執行任務
10. 線程池的價值
-
重用存在的線程,減少對象創建、銷燬資源開銷;
-
可有效的控制最大併發線程數,提高系統資源的使用率,同時避免過多資源競爭,避免堵塞;
-
提供定時執行、定期執行、單線程、併發數控制等功能
11. 常用併發工具有哪些?
CountDownLatch、CyclicBarrier、Semaphore以及Exchanger。
12.CyclicBarrier和CountDownLatch的區別
-
CountDownLatch簡單的說就是一個線程等待,直到他所等待的其他線程都執行完成並且調用countDown()方法發出通知後,當前線程纔可以繼續執行。
-
cyclicBarrier是所有線程都進行等待,直到所有線程都準備好進入await()方法之後,所有線程同時開始執行!
-
CountDownLatch的計數器只能使用一次。而CyclicBarrier的計數器可以使用reset() 方法重置。所以CyclicBarrier能處理更爲複雜的業務場景,比如如果計算髮生錯誤,可以重置計數器,並讓線程們重新執行一次。
-
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線程需要自旋,到下次循環纔有可能機會執行。
16. CAS引發的問題
-
CAS容易造成ABA問題:
一個線程a將其數據值改爲b,接着再改爲a,此時CAS認爲數據未發生變更,然後實際情況是發生變更,可以採用一個版本號標識,每一次版本號加1,在JAVA5中採用AtomicStampedReference解決。
-
無法保證代碼塊原子性:
CAS機制所保證的只是一個變量的原子性操作,而不能保證整個代碼塊的原子性。比如需要保證3個變量共同進行原子性的更新,就不得不使用synchronized了。
-
CAS造成CPU利用率增加:
CAS裏面是一個循環判斷的過程,如果線程一直沒有獲取到狀態,cpu資源會一直被佔用。
17. 什麼是AQS?
AQS是AbstractQueuedSynchronizer的簡稱,它是一個Java提高的底層同步工具類,用一個int類型的變量表示同步狀態,並提供了一系列的CAS操作來管理這個同步狀態.
AQS是一個用來構建鎖和同步器的框架,使用AQS能簡單且高效地構造出應用廣泛的大量的同步器,比如我們提到的ReentrantLock,Semaphore,其他的諸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基於AQS的。
18. AQS支持同步方式哪兩種?
-
獨佔式
-
共享式
這樣方便使用者實現不同類型的同步組件,獨佔式如ReentrantLock,共享式如Semaphore,CountDownLatch,組合式的如ReentrantReadWriteLock。總之,AQS爲使用提供了底層支撐,如何組裝實現,使用者可以自由發揮。
19. ReadWriteLock是什麼?
ReentrantLock某些時候有侷限。如果使用ReentrantLock,可能本身是爲了防止線程A在寫數據、線程B在讀數據造成的數據不一致,但這樣,如果線程C在讀數據、線程D也在讀數據,讀數據是不會改變數據的,沒有必要加鎖,但是還是加鎖了,降低了程序的性能。
因爲這個,才誕生了讀寫鎖ReadWriteLock。ReadWriteLock是一個讀寫鎖接口,ReentrantReadWriteLock是ReadWriteLock接口的一個具體實現,實現了讀寫的分離,讀鎖是共享的,寫鎖是獨佔的,讀和讀之間不會互斥,讀和寫、寫和讀、寫和寫之間纔會互斥,提升了讀寫的性能。
20. FutureTask是什麼?
FutureTask表示一個異步運算任務。FutureTask可以傳入一個Callable接口具體實現類,可以對這個異步運算的任務的結果進行等待獲取、判斷是否已經完成、取消任務等操作。當然,由於FutureTask也是Runnable接口的實現類,所以FutureTask也可以作用於線程池中。
21. synchronized與ReentrantLock區別?
類目 | synchronized | ReentrantLock |
---|---|---|
語義 | 屬於關鍵字(與if,else,for等類似) | 是類 |
靈活性與拓展性 | 較差 | 更多更靈活的特性,可以被繼承、可以有方法、可以有各種各樣的類變量。可以對獲取鎖的等待時間進行設置,避免死鎖;獲取各種鎖的信息;可以靈活地實現多路通知 |
鎖的機制 | 操作的是對象頭中mark word。底層實現主要依靠Lock-Free的隊列,基本思路是自旋後阻塞,競爭切換後繼續競爭鎖,稍微犧牲了公平性,但獲得了高吞吐量,不需要手動釋放,由jvm進行控制操作。 | 底層調用的是Unsafe的park方法加鎖。需要手動釋放鎖 |
22. 樂觀鎖與悲觀鎖
樂觀鎖:對於併發間操作產生線程安全問題持樂觀狀態,樂觀鎖認爲競爭總不會發生,因此它不需要持有鎖。將比較與替換這兩個動作作爲一個原子操作嘗試去修改內存中的變量,如果失敗則表示發生衝突,那麼就應該有相應的重試邏輯。
悲觀鎖:對於併發間操作產生的線程安全問題持悲觀狀態,悲觀鎖認爲競爭總是會發生,因此每次對某資源進行操作時,都會持有一個獨佔的鎖。與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就是一種以空間換時間的做法,在每個線程中維護了一個開放地址法實現的ThreadLocal.ThreadLocalMap,把數據進行隔離,數據不共享,自然就沒有線程安全方面的問題。
27. wait方法、notify方法以及notifyAll方法要在同步塊中被調用原因
這是jdk強制的,wait()方法和notify()/notifyAll()方法在調用前都必須先獲得對象的鎖。
28.多線程同步幾種方法?
Synchronized關鍵字、Lock鎖實現以及分佈式鎖。
29. 線程的調度策略
線程調度器選擇優先級最高的線程運行,但是如果出現如下幾種情況,就會終止線程。
- 線程體中調用了yield方法讓出了對cpu的佔用權利;
- 線程體中調用了sleep方法使線程進入睡眠狀態;
- 線程由於I/O操作受到阻塞;
- 另外一個優先級更高的線程出現;
- 在支持時間片的系統中,該線程的時間片用完
30. ConcurrentHashMap的併發度是什麼?
ConcurrentHashMap併發度就是segment的大小,默認就是16,意味着可以16個線程同時操作ConcurrentHashMap,這是ConcurrentHashMap相比HashMap的優勢。
31. Java死鎖原因以及避免
Java死鎖是一種編程情況,其中兩個或多個線程被永久阻塞,Java死鎖情況出現至少兩個線程和兩個或更多資源。
Java發生死鎖的根本原因是:在申請鎖時發生了交叉閉環申請。
32. Java死鎖原因分析
-
是多個線程涉及到多個鎖,這些鎖存在着交叉,所以可能會導致了一個鎖依賴的閉環。
-
默認的鎖申請操作是阻塞的。
33. Java中喚醒阻塞線程
如果線程是因爲調用了wait()、sleep()或者join()方法而導致的阻塞,可以中斷線程,並且通過拋出InterruptedException來喚醒它;如果線程遇到了IO阻塞,無能爲力,因爲IO是操作系統實現的,Java代碼並沒有辦法直接接觸到操作系統。
34. 不可變對象對於多線程有什麼幫助?
前面有提到過的一個問題,不可變對象保證了對象的內存可見性,對不可變對象的讀取不需要進行額外的同步手段,提升了代碼執行效率。
35. 多線程的上下文切換
多線程的上下文切換是指CPU控制權由一個已經正在運行的線程切換到另外一個就緒並等待獲取CPU執行權的線程的過程。
36. 線程隊列滿問題
採用無界隊列LinkedBlockingQueue,繼續添加任務到阻塞隊列中等待執行,因爲LinkedBlockingQueue可以近乎認爲是一個無窮大的隊列,可以無限存放任務。
採用有界隊列比如ArrayBlockingQueue,任務首先會被添加到ArrayBlockingQueue中,ArrayBlockingQueue滿了,會根據maximumPoolSize的值增加線程數量,如果增加了線程數量還是處理不過來,ArrayBlockingQueue繼續滿,那麼則會使用拒絕策略RejectedExecutionHandler處理滿了的任務,默認是AbortPolicy。
37. Java用到線程調度算法
搶佔式。一個線程用完CPU之後,操作系統會根據線程優先級、線程飢餓情況等數據算出一個總的優先級並分配下一個時間片給某個線程執行。
38.線程調度器和時間分片
線程調度器是一個操作系統服務,它負責爲Runnable狀態的線程分配CPU時間。一旦我們創建一個線程並啓動它,它的執行便依賴於線程調度器的實現。時間分片是指將可用的CPU時間分配給可用的Runnable線程的過程。分配CPU時間可以基於線程優先級或者線程等待的時間。線程調度並不受到Java虛擬機控制,所以由應用程序來控制它是更好的選擇(也就是說不要讓你的程序依賴於線程的優先級)
39. 什麼是自旋?
在Java中自旋一般是指自旋鎖,指當一個線程在獲取鎖的時候,如果鎖已經被其它線程獲取,那麼該線程將循環等待,然後不斷的判斷鎖是否能夠被成功獲取,直到獲取到鎖纔會退出循環。典型的synchronized。synchronized裏面的代碼只是一些很簡單的代碼,執行時間非常快,此時等待的線程都加鎖可能是一種不太值得的操作,因爲線程阻塞涉及到用戶態和內核態切換的問題。既然synchronized裏面的代碼執行得非常快,不妨讓等待鎖的線程不要被阻塞,而是在synchronized的邊界做忙循環,這就是自旋。如果做了多次忙循環發現還沒有獲得鎖,再阻塞,這樣可能是一種更好的策略。
40. Lock接口是什麼?對比同步有什麼優勢?
Lock接口比同步方法以及同步塊更具擴展性的鎖操作,允許更靈活的結構,可以具有完全不同的性質,並且可以支持多個相關類的條件對象。具備如下優勢
- 可以使鎖更公平;
- 可以使線程在等待鎖時候響應中斷;
- 可以讓線程嘗試獲取鎖,並在無法獲取鎖的時候立即返回或者等待一段時間;
- 可以在不同的範圍,以不同的順序獲取和釋放鎖;
41. Semaphore的作用
Semaphore就是一個信號量,它的作用就是限制某段代碼塊併發數。Semaphore有一個私有構造方法,可以傳一個整數類型(int)參數n,表示某段代碼塊最多多少個線程訪問,如果超出n則等待,等到某個線程執行完畢這段代碼塊,下一個線程再進入。由此可以看出如果Semaphore構造方法中傳入的int型整數n=1,相當於變成了一個synchronized了。
42.Executors類是什麼?
Executors爲Executor,ExecutorService,ScheduledExecutorService,ThreadFactory和Callable類提供了一些工具方法。Executors可以用於方便的創建線程池。
43.線程類的構造方法、靜態塊是被哪個線程調用的?
線程類的構造方法、靜態塊是被new這個線程類所在的線程所調用的,而run方法裏面的代碼纔是被線程自身所調用的。
44.同步方法以及同步塊如何選擇更好?
同步塊,這表明同步塊之外的代碼是異步執行的,這比同步整個方法更提升代碼的效率。請知道一條原則:同步的範圍越小越好。
45. Java線程數過多會造成什麼異常?
-
線程的生命週期開銷非常高
-
消耗過多的CPU資源
如果可運行的線程數量多於可用處理器的數量,那麼有線程將會被閒置。大量空閒的線程會佔用許多內存,給垃圾回收器帶來壓力,而且大量的線程在競爭CPU資源時還將產生其他性能的開銷。
- 降低穩定性
JVM在可創建線程的數量上存在一個限制,這個限制值將隨着平臺的不同而不同,並且承受着多個因素制約,包括JVM的啓動參數、Thread構造函數中請求棧的大小,以及底層操作系統對線程的限制等。如果破壞了這些限制,那麼可能拋出OutOfMemoryError異常。
46.Exchange作用是什麼?
Exchanger(交換者)是一個用於線程間協作的工具類。Exchanger 用於進行 線程間的數據交換。它提供一個同步點,在這個同步點,兩個線程可以交換彼此 的數據。這兩個線程通過 exchange 方法交換數據。
如果第一個線程先執行 exchange()方法,它會一直等待第二個線程也執行 exchange 方法,當兩個線程都 到達同步點時,這兩個線程就可以交換數據,將本線程生產出來的數據傳遞給對方。
47. synchronized 加鎖 this 與class 有什麼區別?
使用 synchronized 加鎖 class 時,無論共享一個對象還是創建多個對象,它們用的都是同一把鎖,而使用 synchronized 加鎖 this 時,只有同一個對象會使用同一把鎖,不同對象之間的鎖是不同的。