最全面的阿里多線程面試題,你能回答幾個?

1、什麼是進程,什麼是線程,爲什麼需要多線程編程?

進程是具有一定獨立功能的程序關於某個數據集合上的一次運行活動,是操作系統進行資源分配和調度的一個獨立單位;

線程是進程的一個實體,是CPU調度和分派的基本單位,是比進程更小的能獨立運行的基本單位。線程的劃分尺度小於進程,這使得多線程程序的併發性高;進程在執行時通常擁有獨立的內存單元,而線程之間可以共享內存。

使用多線程的編程通常能夠帶來更好的性能和用戶體驗,但是多線程的程序對於其他程序是不友好的,因爲它可能佔用了更多的CPU資源。當然,也不是線程越多,程序的性能就越好,因爲線程之間的調度和切換也會浪費CPU時間。時下很時髦的Node.js就採用了單線程異步I/O的工作模式。

2、什麼是線程安全

如果你的代碼在多線程下執行和在單線程下執行永遠都能獲得一樣的結果,那麼你的代碼就是線程安全的。

這個問題有值得一提的地方,就是線程安全也是有幾個級別的:

  • 不可變。像String、Integer、Long這些,都是final類型的類,任何一個線程都改變不了它們的值,要改變除非新創建一個,因此這些不可變對象不需要任何同步手段就可以直接在多線程環境下使用
  • 絕對線程安全。不管運行時環境如何,調用者都不需要額外的同步措施。要做到這一點通常需要付出許多額外的代價,Java中標註自己是線程安全的類,實際上絕大多數都不是線程安全的,不過絕對線程安全的類,Java中也有,比方說CopyOnWriteArrayList、CopyOnWriteArraySet
  • 相對線程安全。相對線程安全也就是我們通常意義上所說的線程安全,像Vector這種,add、remove方法都是原子操作,不會被打斷,但也僅限於此,如果有個線程在遍歷某個Vector、有個線程同時在add這個Vector,99%的情況下都會出現ConcurrentModificationException,也就是fail-fast機制。
  • 線程非安全。這個就沒什麼好說的了,ArrayList、LinkedList、HashMap等都是線程非安全的類

3、編寫多線程程序有幾種實現方式?

Java 5以前實現多線程有兩種實現方法:一種是繼承Thread類;另一種是實現Runnable接口。 兩種方式都要通過重寫run()方法來定義線程的行爲,推薦使用後者,因爲Java中的繼承是單繼承,一個類有一個父類,如果繼承了Thread類就無法再繼承其他類了,顯然使用Runnable接口更爲靈活。

Java 5以後創建線程還有第三種方式:實現Callable接口,該接口中的call方法可以在線程執行結束時產生一個返回值。

4、synchronized關鍵字的用法?

synchronized關鍵字可以將對象或者方法標記爲同步,以實現對對象和方法的互斥訪問,可以用synchronized(對象) { … }定義同步代碼塊,或者在聲明方法時將synchronized作爲方法的修飾符。

5、簡述synchronized 和java.util.concurrent.locks.Lock的異同?

Lock是Java 5以後引入的新的API,和關鍵字synchronized相比主要相同點:Lock 能完成synchronized所實現的所有功能;主要不同點:Lock有比synchronized更精確的線程語義和更好的性能,而且不強制性的要求一定要獲得鎖。synchronized會自動釋放鎖,而Lock一定要求程序員手工釋放,並且最好在finally 塊中釋放(這是釋放外部資源的最好的地方)。

6、當一個線程進入一個對象的synchronized方法A之後,其它線程是否可進入此對象的synchronized方法B?

不能。其它線程只能訪問該對象的非同步方法,同步方法則不能進入。因爲非靜態方法上的synchronized修飾符要求執行方法時要獲得對象的鎖,如果已經進入A方法說明對象鎖已經被取走,那麼試圖進入B方法的線程就只能在等鎖池(注意不是等待池哦)中等待對象的鎖。

7、synchronized和ReentrantLock的區別

synchronized是和if、else、for、while一樣的關鍵字,ReentrantLock是類,這是二者的本質區別。既然ReentrantLock是類,那麼它就提供了比synchronized更多更靈活的特性,可以被繼承、可以有方法、可以有各種各樣的類變量,ReentrantLock比synchronized的擴展性體現在幾點上:

  1. ReentrantLock可以對獲取鎖的等待時間進行設置,這樣就避免了死鎖
  2. ReentrantLock可以獲取各種鎖的信息
  3. ReentrantLock可以靈活地實現多路通知

另外,二者的鎖機制其實也是不一樣的:ReentrantLock底層調用的是Unsafe的park方法加鎖,synchronized操作的應該是對象頭中mark word.

8、舉例說明同步和異步。

如果系統中存在臨界資源(資源數量少於競爭資源的線程數量的資源),例如正在寫的數據以後可能被另一個線程讀到,或者正在讀的數據可能已經被另一個線程寫過了,那麼這些數據就必須進行同步存取(數據庫操作中的排他鎖就是最好的例子)。當應用程序在對象上調用了一個需要花費很長時間來執行的方法,並且不希望讓程序等待方法的返回時,就應該使用異步編程,在很多情況下采用異步途徑往往更有效率。事實上,所謂的同步就是指阻塞式操作,而異步就是非阻塞式操作。

9、啓動一個線程是調用run()還是start()方法?

啓動一個線程是調用start()方法,使線程所代表的虛擬處理機處於可運行狀態,這意味着它可以由JVM 調度並執行,這並不意味着線程就會立即運行。run()方法是線程啓動後要進行回調(callback)的方法。

10、爲什麼需要run()和start()方法,我們可以只用run()方法來完成任務嗎?

我們需要run()&start()這兩個方法是因爲JVM創建一個單獨的線程不同於普通方法的調用,所以這項工作由線程的start方法來完成,start由本地方法實現,需要顯示地被調用,使用這倆個方法的另外一個好處是任何一個對象都可以作爲線程運行,只要實現了Runnable接口,這就避免因繼承了Thread類而造成的Java的多繼承問題。

11、什麼是線程池(thread pool)?

在面向對象編程中,創建和銷燬對象是很費時間的,因爲創建一個對象要獲取內存資源或者其它更多資源。

在Java中更是如此,虛擬機將試圖跟蹤每一個對象,以便能夠在對象銷燬後進行垃圾回收。所以提高服務程序效率的一個手段就是儘可能減少創建和銷燬對象的次數,特別是一些很耗資源的對象創建和銷燬,這就是“池化資源”技術產生的原因。線程池顧名思義就是事先創建若干個可執行的線程放入一個池(容器)中,需要的時候從池中獲取線程不用自行創建,使用完畢不需要銷燬線程而是放回池中,從而減少創建和銷燬線程對象的開銷。

Java 5+中的Executor接口定義一個執行線程的工具。它的子類型即線程池接口是ExecutorService。要配置一個線程池是比較複雜的,尤其是對於線程池的原理不是很清楚的情況下,因此在工具類Executors面提供了一些靜態工廠方法,生成一些常用的線程池,如下所示:

  • newSingleThreadExecutor:創建一個單線程的線程池。這個線程池只有一個線程在工作,也就是相當於單線程串行執行所有任務。如果這個唯一的線程因爲異常結束,那麼會有一個新的線程來替代它。此線程池保證所有任務的執行順序按照任務的提交順序執行。
  • newFixedThreadPool:創建固定大小的線程池。每次提交一個任務就創建一個線程,直到線程達到線程池的最大大小。線程池的大小一旦達到最大值就會保持不變,如果某個線程因爲執行異常而結束,那麼線程池會補充一個新線程。
  • newCachedThreadPool:創建一個可緩存的線程池。如果線程池的大小超過了處理任務所需要的線程,那麼就會回收部分空閒(60秒不執行任務)的線程,當任務數增加時,此線程池又可以智能的添加新線程來處理任務。此線程池不會對線程池大小做限制,線程池大小完全依賴於操作系統(或者說JVM)能夠創建的最大線程大小。
  • newScheduledThreadPool:創建一個大小無限的線程池。此線程池支持定時以及週期性執行任務的需求。
  • newSingleThreadExecutor:創建一個單線程的線程池。此線程池支持定時以及週期性執行任務的需求。

12、線程的基本狀態以及狀態之間的關係?

其中Running表示運行狀態;Runnable表示就緒狀態(萬事俱備,只欠CPU);Blocked表示阻塞狀態;阻塞狀態又有多種情況,可能是因爲調用wait()方法進入等待池,也可能是執行同步方法或同步代碼塊進入等鎖池,或者是調用了sleep()方法或join()方法等待休眠或其他線程結束,或是因爲發生了I/O中斷。

13、Java中如何實現序列化,有什麼意義?

序列化就是一種用來處理對象流的機制,所謂對象流也就是將對象的內容進行流化。可以對流化後的對象進行讀寫操作,也可將流化後的對象傳輸於網絡之間。序列化是爲了解決對象流讀寫操作時可能引發的問題(如果不進行序列化可能會存在數據亂序的問題)。

要實現序列化,需要讓一個類實現Serializable接口,該接口是一個標識性接口,標註該類對象是可被序列化的,然後使用一個輸出流來構造一個對象輸出流並通過writeObject(Object)方法就可以將實現對象寫出(即保存其狀態);如果需要反序列化則可以用一個輸入流建立對象輸入流,然後通過readObject方法從流中讀取對象。序列化除了能夠實現對象的持久化之外,還能夠用於對象的深度克隆。

14、產生死鎖的條件

  1. 互斥條件:一個資源每次只能被一個進程使用。
  2. 請求與保持條件:一個進程因請求資源而阻塞時,對已獲得的資源保持不放
  3. 不剝奪條件:進程已獲得的資源,在末使用完之前,不能強行剝奪。
  4. 循環等待條件:若干進程之間形成一種頭尾相接的循環等待資源關係。

15、什麼是線程餓死,什麼是活鎖?

線程餓死和活鎖雖然不想是死鎖一樣的常見問題,但是對於併發編程的設計者來說就像一次邂逅一樣。

當所有線程阻塞,或者由於需要的資源無效而不能處理,不存在非阻塞線程使資源可用。JavaAPI中線程活鎖可能發生在以下情形:

  • 當所有線程在程序中執行Object.wait(0),參數爲0的wait方法。程序將發生活鎖直到在相應的對象上有線程調用Object.notify()或者Object.notifyAll()。
  • 當所有線程卡在無限循環中。

16、什麼導致線程阻塞

阻塞指的是暫停一個線程的執行以等待某個條件發生(如某資源就緒),學過操作系統的同學對它一定已經很熟悉了。Java 提供了大量方法來支持阻塞,下面讓我們逐一分析。

方法說明sleep()sleep() 允許 指定以毫秒爲單位的一段時間作爲參數,它使得線程在指定的時間內進入阻塞狀態,不能得到CPU 時間,指定的時間一過,線程重新進入可執行狀態。 典型地,sleep() 被用在等待某個資源就緒的情形:測試發現條件不滿足後,讓線程阻塞一段時間後重新測試,直到條件滿足爲止suspend() 和 resume()兩個方法配套使用,suspend()使得線程進入阻塞狀態,並且不會自動恢復,必須其對應的resume() 被調用,才能使得線程重新進入可執行狀態。典型地,suspend() 和 resume() 被用在等待另一個線程產生的結果的情形:測試發現結果還沒有產生後,讓線程阻塞,另一個線程產生了結果後,調用 resume() 使其恢復。yield()yield() 使當前線程放棄當前已經分得的CPU 時間,但不使當前線程阻塞,即線程仍處於可執行狀態,隨時可能再次分得 CPU 時間。調用 yield() 的效果等價於調度程序認爲該線程已執行了足夠的時間從而轉到另一個線程。wait() 和 notify()兩個方法配套使用,wait() 使得線程進入阻塞狀態,它有兩種形式,一種允許 指定以毫秒爲單位的一段時間作爲參數,另一種沒有參數,前者當對應的 notify() 被調用或者超出指定時間時線程重新進入可執行狀態,後者則必須對應的 notify() 被調用.

17、怎麼檢測一個線程是否持有對象監視器

Thread類提供了一個holdsLock(Object obj)方法,當且僅當對象obj的監視器被某條線程持有的時候纔會返回true,注意這是一個static方法,這意味着”某條線程”指的是當前線程。

18、請說出與線程同步以及線程調度相關的方法。

  • wait():使一個線程處於等待(阻塞)狀態,並且釋放所持有的對象的鎖;
  • sleep():使一個正在運行的線程處於睡眠狀態,是一個靜態方法,調用此方法要處理InterruptedException異常;
  • notify():喚醒一個處於等待狀態的線程,當然在調用此方法的時候,並不能確切的喚醒某一個等待狀態的線程,而是由JVM確定喚醒哪個線程,而且與優先級無關;
  • notityAll():喚醒所有處於等待狀態的線程,該方法並不是將對象的鎖給所有線程,而是讓它們競爭,只有獲得鎖的線程才能進入就緒狀態;

19、sleep() 、join()、yield()有什麼區別

  • sleep()方法給其他線程運行機會時不考慮線程的優先級,因此會給低優先級的線程以運行的機會;yield()方法只會給相同優先級或更高優先級的線程以運行的機會;
  • 線程執行sleep()方法後轉入阻塞(blocked)狀態,而執行yield()方法後轉入就緒(ready)狀態;
  • sleep()方法聲明拋出InterruptedException,而yield()方法沒有聲明任何異常;
  • sleep()方法比yield()方法(跟操作系統CPU調度相關)具有更好的可移植性。

20、wait(),notify()和suspend(),resume()之間的區別

初看起來它們與 suspend() 和 resume() 方法對沒有什麼分別,但是事實上它們是截然不同的。區別的核心在於,前面敘述的所有方法,阻塞時都不會釋放佔用的鎖(如果佔用了的話),而這一對方法則相反。上述的核心區別導致了一系列的細節上的區別。

首先,前面敘述的所有方法都隸屬於 Thread 類,但是這一對卻直接隸屬於 Object 類,也就是說,所有對象都擁有這一對方法。初看起來這十分不可思議,但是實際上卻是很自然的,因爲這一對方法阻塞時要釋放佔用的鎖,而鎖是任何對象都具有的,調用任意對象的 wait() 方法導致線程阻塞,並且該對象上的鎖被釋放。而調用 任意對象的notify()方法則導致從調用該對象的 wait() 方法而阻塞的線程中隨機選擇的一個解除阻塞(但要等到獲得鎖後才真正可執行)。

其次,前面敘述的所有方法都可在任何位置調用,但是這一對方法卻必須在 synchronized 方法或塊中調用,理由也很簡單,只有在synchronized 方法或塊中當前線程才佔有鎖,纔有鎖可以釋放。同樣的道理,調用這一對方法的對象上的鎖必須爲當前線程所擁有,這樣纔有鎖可以釋放。因此,這一對方法調用必須放置在這樣的 synchronized 方法或塊中,該方法或塊的上鎖對象就是調用這一對方法的對象。若不滿足這一條件,則程序雖然仍能編譯,但在運行時會出現IllegalMonitorStateException 異常。

wait() 和 notify() 方法的上述特性決定了它們經常和synchronized關鍵字一起使用,將它們和操作系統進程間通信機制作一個比較就會發現它們的相似性:synchronized方法或塊提供了類似於操作系統原語的功能,它們的執行不會受到多線程機制的干擾,而這一對方法則相當於 block 和wakeup 原語(這一對方法均聲明爲 synchronized)。它們的結合使得我們可以實現操作系統上一系列精妙的進程間通信的算法(如信號量算法),並用於解決各種複雜的線程間通信問題。

關於 wait() 和 notify() 方法最後再說明兩點:

第一:調用 notify() 方法導致解除阻塞的線程是從因調用該對象的 wait() 方法而阻塞的線程中隨機選取的,我們無法預料哪一個線程將會被選擇,所以編程時要特別小心,避免因這種不確定性而產生問題。

第二:除了 notify(),還有一個方法 notifyAll() 也可起到類似作用,唯一的區別在於,調用 notifyAll() 方法將把因調用該對象的 wait() 方法而阻塞的所有線程一次性全部解除阻塞。當然,只有獲得鎖的那一個線程才能進入可執行狀態。

談到阻塞,就不能不談一談死鎖,略一分析就能發現,suspend() 方法和不指定超時期限的 wait() 方法的調用都可能產生死鎖。遺憾的是,Java 並不在語言級別上支持死鎖的避免,我們在編程中必須小心地避免死鎖。

以上我們對 Java 中實現線程阻塞的各種方法作了一番分析,我們重點分析了 wait() 和 notify() 方法,因爲它們的功能最強大,使用也最靈活,但是這也導致了它們的效率較低,較容易出錯。實際使用中我們應該靈活使用各種方法,以便更好地達到我們的目的。

21、爲什麼wait()方法和notify()/notifyAll()方法要在同步塊中被調用

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

22、wait()方法和notify()/notifyAll()方法在放棄對象監視器時有什麼區別

wait()方法和notify()/notifyAll()方法在放棄對象監視器的時候的區別在於:wait()方法立即釋放對象監視器,notify()/notifyAll()方法則會等待線程剩餘代碼執行完畢纔會放棄對象監視器。

23、Runnable和Callable的區別

Runnable接口中的run()方法的返回值是void,它做的事情只是純粹地去執行run()方法中的代碼而已;Callable接口中的call()方法是有返回值的,是一個泛型,和Future、FutureTask配合可以用來獲取異步執行的結果。

這其實是很有用的一個特性,因爲多線程相比單線程更難、更復雜的一個重要原因就是因爲多線程充滿着未知性,某條線程是否執行了?某條線程執行了多久?某條線程執行的時候我們期望的數據是否已經賦值完畢?無法得知,我們能做的只是等待這條多線程的任務執行完畢而已。而Callable+Future/FutureTask卻可以方便獲取多線程運行的結果,可以在等待時間太長沒獲取到需要的數據的情況下取消該線程的任務。

24、Thread類的sleep()方法和對象的wait()方法都可以讓線程暫停執行,它們有什麼區別?

sleep()方法(休眠)是線程類(Thread)的靜態方法,調用此方法會讓當前線程暫停執行指定的時間,將執行機會(CPU)讓給其他線程,但是對象的鎖依然保持,因此休眠時間結束後會自動恢復。

wait()是Object類的方法,調用對象的wait()方法導致當前線程放棄對象的鎖(線程暫停執行),進入對象的等待池(wait pool),只有調用對象的notify()方法(或notifyAll()方法)時才能喚醒等待池中的線程進入等鎖池(lock pool),如果線程重新獲得對象的鎖就可以進入就緒狀態。

25、線程的sleep()方法和yield()方法有什麼區別?

  1. sleep()方法給其他線程運行機會時不考慮線程的優先級,因此會給低優先級的線程以運行的機會;yield()方法只會給相同優先級或更高優先級的線程以運行的機會;
  2. 線程執行sleep()方法後轉入阻塞(blocked)狀態,而執行yield()方法後轉入就緒(ready)狀態;
  3. sleep()方法聲明拋出InterruptedException,而yield()方法沒有聲明任何異常;
  4. sleep()方法比yield()方法(跟操作系統CPU調度相關)具有更好的可移植性。

26、爲什麼wait,nofity和nofityAll這些方法不放在Thread類當中

一個很明顯的原因是JAVA提供的鎖是對象級的而不是線程級的,每個對象都有鎖,通過線程獲得。如果線程需要等待某些鎖那麼調用對象中的wait()方法就有意義了。如果wait()方法定義在Thread類中,線程正在等待的是哪個鎖就不明顯了。簡單的說,由於wait,notify和notifyAll都是鎖級別的操作,所以把他們定義在Object類中因爲鎖屬於對象。

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

如果線程是因爲調用了wait()、sleep()或者join()方法而導致的阻塞,可以中斷線程,並且通過拋出InterruptedException來喚醒它;如果線程遇到了IO阻塞,無能爲力,因爲IO是操作系統實現的,Java代碼並沒有辦法直接接觸到操作系統。

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

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

29、FutureTask是什麼

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

30、一個線程如果出現了運行時異常怎麼辦?

如果這個異常沒有被捕獲的話,這個線程就停止執行了。另外重要的一點是:如果這個線程持有某個某個對象的監視器,那麼這個對象監視器會被立即釋放

31、Java當中有哪幾種鎖

自旋鎖: 自旋鎖在JDK1.6之後就默認開啓了。基於之前的觀察,共享數據的鎖定狀態只會持續很短的時間,爲了這一小段時間而去掛起和恢復線程有點浪費,所以這裏就做了一個處理,讓後面請求鎖的那個線程在稍等一會,但是不放棄處理器的執行時間,看看持有鎖的線程能否快速釋放。爲了讓線程等待,所以需要讓線程執行一個忙循環也就是自旋操作。在jdk6之後,引入了自適應的自旋鎖,也就是等待的時間不再固定了,而是由上一次在同一個鎖上的自旋時間及鎖的擁有者狀態來決定

偏向鎖: 在JDK1.之後引入的一項鎖優化,目的是消除數據在無競爭情況下的同步原語。進一步提升程序的運行性能。 偏向鎖就是偏心的偏,意思是這個鎖會偏向第一個獲得他的線程,如果接下來的執行過程中,改鎖沒有被其他線程獲取,則持有偏向鎖的線程將永遠不需要再進行同步。偏向鎖可以提高帶有同步但無競爭的程序性能,也就是說他並不一定總是對程序運行有利,如果程序中大多數的鎖都是被多個不同的線程訪問,那偏向模式就是多餘的,在具體問題具體分析的前提下,可以考慮是否使用偏向鎖。

輕量級鎖: 爲了減少獲得鎖和釋放鎖所帶來的性能消耗,引入了“偏向鎖”和“輕量級鎖”,所以在Java SE1.6裏鎖一共有四種狀態,無鎖狀態,偏向鎖狀態,輕量級鎖狀態和重量級鎖狀態,它會隨着競爭情況逐漸升級。鎖可以升級但不能降級,意味着偏向鎖升級成輕量級鎖後不能降級成偏向鎖

32、如何在兩個線程間共享數據

通過在線程之間共享對象就可以了,然後通過wait/notify/notifyAll、await/signal/signalAll進行喚起和等待,比方說阻塞隊列BlockingQueue就是爲線程之間共享數據而設計的

33、如何正確的使用wait()?使用if還是while?

wait() 方法應該在循環調用,因爲當線程獲取到 CPU 開始執行的時候,其他條件可能還沒有滿足,所以在處理前,循環檢測條件是否滿足會更好。下面是一段標準的使用 wait 和 notify 方法的代碼:

synchronized (obj) {
   while (condition does not hold)
     obj.wait(); // (Releases lock, and reacquires on wakeup)
     ... // Perform action appropriate to condition
}

34、什麼是線程局部變量ThreadLocal

線程局部變量是侷限於線程內部的變量,屬於線程自身所有,不在多個線程間共享。Java提供ThreadLocal類來支持線程局部變量,是一種實現線程安全的方式。但是在管理環境下(如 web 服務器)使用線程局部變量的時候要特別小心,在這種情況下,工作線程的生命週期比任何應用變量的生命週期都要長。任何線程局部變量一旦在工作完成後沒有釋放,Java 應用就存在內存泄露的風險。

35、ThreadLoal的作用是什麼?

簡單說ThreadLocal就是一種以空間換時間的做法在每個Thread裏面維護了一個ThreadLocal.ThreadLocalMap把數據進行隔離,數據不共享,自然就沒有線程安全方面的問題了.

36、ThreadLocal 原理分析

ThreadLocal爲解決多線程程序的併發問題提供了一種新的思路。ThreadLocal,顧名思義是線程的一個本地化對象,當工作於多線程中的對象使用ThreadLocal維護變量時,ThreadLocal爲每個使用該變量的線程分配一個獨立的變量副本,所以每一個線程都可以獨立的改變自己的副本,而不影響其他線程所對應的副本。從線程的角度看,這個變量就像是線程的本地變量。

ThreadLocal類非常簡單好用,只有四個方法,能用上的也就是下面三個方法:

  • void set(T value):設置當前線程的線程局部變量的值。
  • T get():獲得當前線程所對應的線程局部變量的值。
  • void remove():刪除當前線程中線程局部變量的值。

ThreadLocal是如何做到爲每一個線程維護一份獨立的變量副本的呢?在ThreadLocal類中有一個Map,鍵爲線程對象,值是其線程對應的變量的副本,自己要模擬實現一個ThreadLocal類其實並不困難,代碼如下所示:

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
public class MyThreadLocal<T> {
    private Map<Thread, T> map = Collections.synchronizedMap(new HashMap<Thread, T>());
    public void set(T newValue) {
        map.put(Thread.currentThread(), newValue);
    }
    public T get() {
        return map.get(Thread.currentThread());
    }
    public void remove() {
        map.remove(Thread.currentThread());
    }
}

37、如果你提交任務時,線程池隊列已滿,這時會發生什麼

如果你使用的LinkedBlockingQueue,也就是無界隊列的話,沒關係,繼續添加任務到阻塞隊列中等待執行,因爲LinkedBlockingQueue可以近乎認爲是一個無窮大的隊列,可以無限存放任務;如果你使用的是有界隊列比方說ArrayBlockingQueue的話,任務首先會被添加到ArrayBlockingQueue中,ArrayBlockingQueue滿了,則會使用拒絕策略RejectedExecutionHandler處理滿了的任務,默認是AbortPolicy。

38、爲什麼要使用線程池

避免頻繁地創建和銷燬線程,達到線程對象的重用。另外,使用線程池還可以根據項目靈活地控制併發的數目。

39、java中用到的線程調度算法是什麼

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

40、Thread.sleep(0)的作用是什麼

由於Java採用搶佔式的線程調度算法,因此可能會出現某條線程常常獲取到CPU控制權的情況,爲了讓某些優先級比較低的線程也能獲取到CPU控制權,可以使用Thread.sleep(0)手動觸發一次操作系統分配時間片的操作,這也是平衡CPU控制權的一種操作。

41、什麼是CAS

CAS,全稱爲Compare and Swap,即比較-替換。假設有三個操作數:內存值V、舊的預期值A、要修改的值B,當且僅當預期值A和內存值V相同時,纔會將內存值修改爲B並返回true,否則什麼都不做並返回false。當然CAS一定要volatile變量配合,這樣才能保證每次拿到的變量是主內存中最新的那個值,否則舊的預期值A對某條線程來說,永遠是一個不會變的值A,只要某次CAS操作失敗,永遠都不可能成功

42、什麼是樂觀鎖和悲觀鎖

樂觀鎖:樂觀鎖認爲競爭不總是會發生,因此它不需要持有鎖,將比較-替換這兩個動作作爲一個原子操作嘗試去修改內存中的變量,如果失敗則表示發生衝突,那麼就應該有相應的重試邏輯。

悲觀鎖:悲觀鎖認爲競爭總是會發生,因此每次對某資源進行操作時,都會持有一個獨佔的鎖,就像synchronized,不管三七二十一,直接上了鎖就操作資源了。

43、ConcurrentHashMap的併發度是什麼?

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

44、ConcurrentHashMap的工作原理

ConcurrentHashMap在jdk 1.6和jdk 1.8實現原理是不同的.

jdk 1.6: ConcurrentHashMap是線程安全的,但是與Hashtablea相比,實現線程安全的方式不同。Hashtable是通過對hash表結構進行鎖定,是阻塞式的,當一個線程佔有這個鎖時,其他線程必須阻塞等待其釋放鎖。ConcurrentHashMap是採用分離鎖的方式,它並沒有對整個hash表進行鎖定,而是局部鎖定,也就是說當一個線程佔有這個局部鎖時,不影響其他線程對hash表其他地方的訪問。 具體實現:ConcurrentHashMap內部有一個Segment

jdk 1.8 在jdk 8中,ConcurrentHashMap不再使用Segment分離鎖,而是採用一種樂觀鎖CAS算法來實現同步問題,但其底層還是“數組+鏈表->紅黑樹”的實現。

45、CyclicBarrier和CountDownLatch區別

這兩個類非常類似,都在java.util.concurrent下,都可以用來表示代碼運行到某個點上,二者的區別在於:

CyclicBarrier的某個線程運行到某個點上之後,該線程即停止運行,直到所有的線程都到達了這個點,所有線程才重新運行;CountDownLatch則不是,某線程運行到某個點上之後,只是給某個數值-1而已,該線程繼續運行

CyclicBarrier只能喚起一個任務,CountDownLatch可以喚起多個任務

CyclicBarrier可重用,CountDownLatch不可重用,計數值爲0該CountDownLatch就不可再用了

46、java中的++操作符線程安全麼?

不是線程安全的操作。它涉及到多個指令,如讀取變量值,增加,然後存儲回內存,這個過程可能會出現多個線程交差

47、有三個線程T1,T2,T3,怎麼確保它們按順序執行?

在多線程中有多種方法讓線程按特定順序執行,你可以用線程類的join()方法在一個線程中啓動另一個線程,另外一個線程完成該線程繼續執行。爲了確保三個線程的順序你應該先啓動最後一個(T3調用T2,T2調用T1),這樣T1就會先完成而T3最後完成。

48、如何在Java中創建Immutable對象?

這個問題看起來和多線程沒什麼關係, 但不變性有助於簡化已經很複雜的併發程序。Immutable對象可以在沒有同步的情況下共享,降低了對該對象進行併發訪問時的同步化開銷。可是Java沒有@Immutable這個註解符,要創建不可變類,要實現下面幾個步驟:通過構造方法初始化所有成員、對變量不要提供setter方法、將所有的成員聲明爲私有的,這樣就不允許直接訪問這些成員、在getter方法中,不要直接返回對象本身,而是克隆對象,並返回對象的拷貝。

49、你有哪些多線程開發良好的實踐?

  • 給線程命名
  • 最小化同步範圍
  • 優先使用volatile
  • 儘可能使用更高層次的併發工具而非wait和notify()來實現線程通信,如BlockingQueue,Semeaphore
  • 優先使用併發容器而非同步容器.
  • 考慮使用線程池

50、可以創建Volatile數組嗎?

Java 中可以創建 volatile類型數組,不過只是一個指向數組的引用,而不是整個數組。如果改變引用指向的數組,將會受到volatile 的保護,但是如果多個線程同時改變數組的元素,volatile標示符就不能起到之前的保護作用了

51、Volatile關鍵字的作用

一個非常重要的問題,是每個學習、應用多線程的Java程序員都必須掌握的。理解volatile關鍵字的作用的前提是要理解Java內存模型,這裏就不講Java內存模型了,可以參見第31點,volatile關鍵字的作用主要有兩個:

  • 多線程主要圍繞可見性和原子性兩個特性而展開,使用volatile關鍵字修飾的變量,保證了其在多線程之間的可見性,即每次讀取到volatile變量,一定是最新的數據
  • 代碼底層執行不像我們看到的高級語言—-Java程序這麼簡單,它的執行是Java代碼–>字節碼–>根據字節碼執行對應的C/C++代碼–>C/C++代碼被編譯成彙編語言–>和硬件電路交互,現實中,爲了獲取更好的性能JVM可能會對指令進行重排序,多線程下可能會出現一些意想不到的問題。使用volatile則會對禁止語義重排序,當然這也一定程度上降低了代碼執行效率

從實踐角度而言,volatile的一個重要作用就是和CAS結合,保證了原子性,詳細的可以參見java.util.concurrent.atomic包下的類,比如AtomicInteger。

52、volatile能使得一個非原子操作變成原子操作嗎?

一個典型的例子是在類中有一個 long 類型的成員變量。如果你知道該成員變量會被多個線程訪問,如計數器、價格等,你最好是將其設置爲 volatile。爲什麼?因爲 Java 中讀取 long 類型變量不是原子的,需要分成兩步,如果一個線程正在修改該 long 變量的值,另一個線程可能只能看到該值的一半(前 32 位)。但是對一個 volatile 型的 long 或 double 變量的讀寫是原子。

一種實踐是用 volatile 修飾 long 和 double 變量,使其能按原子類型來讀寫。double 和 long 都是64位寬,因此對這兩種類型的讀是分爲兩部分的,第一次讀取第一個 32 位,然後再讀剩下的 32 位,這個過程不是原子的,但 Java 中 volatile 型的 long 或 double 變量的讀寫是原子的。volatile 修復符的另一個作用是提供內存屏障(memory barrier),例如在分佈式框架中的應用。簡單的說,就是當你寫一個 volatile 變量之前,Java 內存模型會插入一個寫屏障(write barrier),讀一個 volatile 變量之前,會插入一個讀屏障(read barrier)。意思就是說,在你寫一個 volatile 域時,能保證任何線程都能看到你寫的值,同時,在寫之前,也能保證任何數值的更新對所有線程是可見的,因爲內存屏障會將其他所有寫的值更新到緩存。

53、volatile類型變量提供什麼保證?

volatile 主要有兩方面的作用:1.避免指令重排2.可見性保證.例如,JVM 或者 JIT爲了獲得更好的性能會對語句重排序,但是 volatile 類型變量即使在沒有同步塊的情況下賦值也不會與其他語句重排序。 volatile 提供 happens-before 的保證,確保一個線程的修改能對其他線程是可見的。某些情況下,volatile 還能提供原子性,如讀 64 位數據類型,像 long 和 double 都不是原子的(低32位和高32位),但 volatile 類型的 double 和 long 就是原子的.

54、Java 中,編寫多線程程序的時候你會遵循哪些最佳實踐?

這是我在寫Java 併發程序的時候遵循的一些最佳實踐:

  • 給線程命名,這樣可以幫助調試。
  • 最小化同步的範圍,而不是將整個方法同步,只對關鍵部分做同步。
  • 如果可以,更偏向於使用 volatile 而不是 synchronized。
  • 使用更高層次的併發工具,而不是使用 wait() 和 notify() 來實現線程間通信,如 BlockingQueue,CountDownLatch 及 Semeaphore。
  • 優先使用併發集合,而不是對集合進行同步。併發集合提供更好的可擴展性。

55、說出至少 5 點在 Java 中使用線程的最佳實踐。

這個問題與之前的問題類似,你可以使用上面的答案。對線程來說,你應該:

  • 對線程命名
  • 將線程和任務分離,使用線程池執行器來執行 Runnable 或 Callable。
  • 使用線程池

56、Java中如何獲取到線程dump文件

死循環、死鎖、阻塞、頁面打開慢等問題,打線程dump是最好的解決問題的途徑。所謂線程dump也就是線程堆棧,獲取到線程堆棧有兩步:

  • 獲取到線程的pid,可以通過使用jps命令,在Linux環境下還可以使用ps -ef | grep java
  • 打印線程堆棧,可以通過使用jstack pid命令,在Linux環境下還可以使用kill -3 pid

另外提一點,Thread類提供了一個getStackTrace()方法也可以用於獲取線程堆棧。這是一個實例方法,因此此方法是和具體線程實例綁定的,每次獲取獲取到的是具體某個線程當前運行的堆棧。

57、高併發、任務執行時間短的業務怎樣使用線程池?併發不高、任務執行時間長的業務怎樣使用線程池?併發高、業務執行時間長的業務怎樣使用線程池?

這是我在併發編程網上看到的一個問題,把這個問題放在最後一個,希望每個人都能看到並且思考一下,因爲這個問題非常好、非常實際、非常專業。關於這個問題,個人看法是:

  1. 高併發、任務執行時間短的業務,線程池線程數可以設置爲CPU核數+1,減少線程上下文的切換
  2. 併發不高、任務執行時間長的業務要區分開看:
  • 假如是業務時間長集中在IO操作上,也就是IO密集型的任務,因爲IO操作並不佔用CPU,所以不要讓所有的CPU閒下來,可以加大線程池中的線程數目,讓CPU處理更多的業務
  • 假如是業務時間長集中在計算操作上,也就是計算密集型任務,這個就沒辦法了,和(1)一樣吧,線程池中的線程數設置得少一些,減少線程上下文的切換
  1. 併發高、業務執行時間長,解決這種類型任務的關鍵不在於線程池而在於整體架構的設計,看看這些業務裏面某些數據是否能做緩存是第一步,增加服務器是第二步,至於線程池的設置,設置參考(2)。
  2. 業務執行時間長的問題,也可能需要分析一下,看看能不能使用中間件對任務進行拆分和解耦。

58、作業(進程)調度算法

  1. 先來先服務調度算法(FCFS) 每次調度都是從後備作業隊列中選擇一個或多個最先進入該隊列的作業,將它們調入內存,爲它們分配資源、創建進程,然後放入就緒隊列。
  2. 短作業(進程)優先調度算法(SPF) 短作業優先(SJF)的調度算法是從後備隊列中選擇一個或若干個估計運行時間最短的作業,將它們調入內存運行。缺點:長作業的運行得不到保證
  3. 優先權調度算法(HPF) 當把該算法用於作業調度時,系統將從後備隊列中選擇若干個優先權最高的作業裝入內存。當用於進程調度時,該算法是把處理機分配給就緒隊列中優先權最高的進程,這時,又可進一步把該算法分成如下兩種。 可以分爲:
  • 非搶佔式優先權算法
  • 搶佔式優先權調度算法
  1. 高響應比優先調度算法(HRN) 每次選擇高響應比最大的作業執行,響應比=(等待時間+要求服務時間)/要求服務時間。該算法同時考慮了短作業優先和先來先服務。
  • 如果作業的等待時間相同,則要求服務的時間愈短,其優先權愈高,因而該算法有利於短作業。
  • 當要求服務的時間相同時,作業的優先權決定於其等待時間,等待時間愈長,其優先權愈高,因而它實現的是先來先服務。
  • 對於長作業,作業的優先級可以隨等待時間的增加而提高,當其等待時間足夠長時,其優先級便可升到很高,從而也可獲得處理機。簡言之,該算法既照顧了短作業,又考慮了作業到達的先後次序,不會使長作業長期得不到服務。因此,該算法實現了一種較好的折衷。當然,在利用該算法時,每要進行調度之前,都須先做響應比的計算,這會增加系統開銷。
  1. 時間片輪轉法(RR) 在早期的時間片輪轉法中,系統將所有的就緒進程按先來先服務的原則排成一個隊列,每次調度時,把CPU分配給隊首進程,並令其執行一個時間片。時間片的大小從幾ms到幾百ms。當執行的時間片用完時,由一個計時器發出時鐘中斷請求,調度程序便據此信號來停止該進程的執行,並將它送往就緒隊列的末尾;然後,再把處理機分配給就緒隊列中新的隊首進程,同時也讓它執行一個時間片。這樣就可以保證就緒隊列中的所有進程在一給定的時間內均能獲得一時間片的處理機執行時間。換言之,系統能在給定的時間內響應所有用戶的請求。
  2. 多級反饋隊列調度算法 它是目前被公認的一種較好的進程調度算法。
  • 應設置多個就緒隊列,併爲各個隊列賦予不同的優先級。第一個隊列的優先級最高,第二個隊列次之,其餘各隊列的優先權逐個降低。該算法賦予各個隊列中進程執行時間片的大小也各不相同,在優先權愈高的隊列中,爲每個進程所規定的執行時間片就愈小。例如,第二個隊列的時間片要比第一個隊列的時間片長一倍,……,第i+1個隊列的時間片要比第i個隊列的時間片長一倍。
  • 當一個新進程進入內存後,首先將它放入第一隊列的末尾,按FCFS原則排隊等待調度。當輪到該進程執行時,如它能在該時間片內完成,便可準備撤離系統;如果它在一個時間片結束時尚未完成,調度程序便將該進程轉入第二隊列的末尾,再同樣地按FCFS原則等待調度執行;如果它在第二隊列中運行一個時間片後仍未完成,再依次將它放入第三隊列,……,如此下去,當一個長作業(進程)從第一隊列依次降到第n隊列後,在第n 隊列便採取按時間片輪轉的方式運行。
  • 僅當第一隊列空閒時,調度程序才調度第二隊列中的進程運行;僅當第1~(i-1)隊列均空時,纔會調度第i隊列中的進程運行。如果處理機正在第i隊列中爲某進程服務時,又有新進程進入優先權較高的隊列(第1~(i-1)中的任何一個隊列),則此時新進程將搶佔正在運行進程的處理機,即由調度程序把正在運行的進程放回到第i隊列的末尾,把處理機分配給新到的高優先權進程。

59、講講線程池的實現原理

首先要明確爲什麼要使用線程池,使用線程池會帶來什麼好處?

  • 線程是稀缺資源,不能頻繁的創建。
  • 應當將其放入一個池子中,可以給其他任務進行復用。
  • 解耦作用,線程的創建於執行完全分開,方便維護。

60、創建一個線程池

以一個使用較多的

ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler);

爲例:

  • 其中的 corePoolSize 爲線程池的基本大小。
  • maximumPoolSize 爲線程池最大線程大小。
  • keepAliveTime 和 unit 則是線程空閒後的存活時間。
  • workQueue 用於存放任務的阻塞隊列。
  • handler 當隊列和最大線程池都滿了之後的飽和策略。

61、處理流程

當提交一個任務到線程池時它的執行流程是怎樣的呢?

首先第一步會判斷核心線程數有沒有達到上限,如果沒有則創建線程(會獲取全局鎖),滿了則會將任務丟進阻塞隊列。

如果隊列也滿了則需要判斷最大線程數是否達到上限,如果沒有則創建線程(獲取全局鎖),如果最大線程數也滿了則會根據飽和策略處理。

常用的飽和策略有:

  • 直接丟棄任務。
  • 調用者線程處理。
  • 丟棄隊列中的最近任務,執行當前任務。

所以當線程池完成預熱之後都是將任務放入隊列,接着由工作線程一個個從隊列裏取出執行。

62、合理配置線程池

線程池並不是配置越大越好,而是要根據任務的熟悉來進行劃分: 如果是 CPU 密集型任務應當分配較少的線程,比如 CPU 個數相當的大小。

如果是 IO 密集型任務,由於線程並不是一直在運行,所以可以儘可能的多配置線程,比如 CPU 個數 * 2 。

當是一個混合型任務,可以將其拆分爲 CPU 密集型任務以及 IO 密集型任務,這樣來分別配置。

63、synchronize 實現原理

衆所周知 Synchronize 關鍵字是解決併發問題常用解決方案,有以下三種使用方式:

  • 同步普通方法,鎖的是當前對象。
  • 同步靜態方法,鎖的是當前 Class 對象。
  • 同步塊,鎖的是 {} 中的對象。

實現原理: JVM 是通過進入、退出對象監視器( Monitor )來實現對方法、同步塊的同步的。

具體實現是在編譯之後在同步方法調用前加入一個 monitor.enter 指令,在退出方法和異常處插入 monitor.exit 的指令。

其本質就是對一個對象監視器( Monitor )進行獲取,而這個獲取過程具有排他性從而達到了同一時刻只能一個線程訪問的目的。

而對於沒有獲取到鎖的線程將會阻塞到方法入口處,直到獲取鎖的線程 monitor.exit 之後才能嘗試繼續獲取鎖。

流程圖如下:

synchronize 很多都稱之爲重量鎖,JDK1.6 中對 synchronize 進行了各種優化,爲了能減少獲取和釋放鎖帶來的消耗引入了偏向鎖和輕量鎖。

64、輕量鎖

當代碼進入同步塊時,如果同步對象爲無鎖狀態時,當前線程會在棧幀中創建一個鎖記錄(Lock Record)區域,同時將鎖對象的對象頭中 Mark Word 拷貝到鎖記錄中,再嘗試使用 CAS 將 Mark Word 更新爲指向鎖記錄的指針。

如果更新成功,當前線程就獲得了鎖。

如果更新失敗 JVM 會先檢查鎖對象的 Mark Word 是否指向當前線程的鎖記錄。

如果是則說明當前線程擁有鎖對象的鎖,可以直接進入同步塊。

不是則說明有其他線程搶佔了鎖,如果存在多個線程同時競爭一把鎖,輕量鎖就會膨脹爲重量鎖。

65、解鎖

輕量鎖的解鎖過程也是利用 CAS 來實現的,會嘗試鎖記錄替換回鎖對象的 Mark Word 。如果替換成功則說明整個同步操作完成,失敗則說明有其他線程嘗試獲取鎖,這時就會喚醒被掛起的線程(此時已經膨脹爲重量鎖)

輕量鎖能提升性能的原因是:

認爲大多數鎖在整個同步週期都不存在競爭,所以使用 CAS 比使用互斥開銷更少。但如果鎖競爭激烈,輕量鎖就不但有互斥的開銷,還有 CAS 的開銷,甚至比重量鎖更慢。

66、偏向鎖

爲了進一步的降低獲取鎖的代價,JDK1.6 之後還引入了偏向鎖。

偏向鎖的特徵是:鎖不存在多線程競爭,並且應由一個線程多次獲得鎖。

當線程訪問同步塊時,會使用 CAS 將線程 ID 更新到鎖對象的 Mark Word 中,如果更新成功則獲得偏向鎖,並且之後每次進入這個對象鎖相關的同步塊時都不需要再次獲取鎖了。

67、釋放鎖

當有另外一個線程獲取這個鎖時,持有偏向鎖的線程就會釋放鎖,釋放時會等待全局安全點(這一時刻沒有字節碼運行),接着會暫停擁有偏向鎖的線程,根據鎖對象目前是否被鎖來判定將對象頭中的 Mark Word 設置爲無鎖或者是輕量鎖狀態。

偏向鎖可以提高帶有同步卻沒有競爭的程序性能,但如果程序中大多數鎖都存在競爭時,那偏向鎖就起不到太大作用。可以使用 -XX:-userBiasedLocking=false 來關閉偏向鎖,並默認進入輕量鎖。

本人免費整理了Java高級資料,涵蓋了Java、Redis、MongoDB、MySQL、Zookeeper、Spring Cloud、Dubbo高併發分佈式等教程,一共30G,需要自己領取。
傳送門:https://mp.weixin.qq.com/s/JzddfH-7yNudmkjT0IRL8Q

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章