1. 程式人生 > >Java多線程面試題整理

Java多線程面試題整理

exe tostring .com lock 環境 作用範圍 基於 key 顯示

線程是操作系統能夠進行運算調度的最小單位,它被包含在進程之中,是進程中的實際運作單位。程序員可以通過它進行多處理器編程,你可以使用多線程對運算密集型任務提速。比如,如果一個線程完成一個任務要100毫秒,那麽用十個線程完成改任務只需10毫秒。Java在語言層面對多線程提供了卓越的支持,它也是一個很好的賣點。

2) 線程和進程有什麽區別?

線程是進程的子集,一個進程可以有很多線程,每條線程並行執行不同的任務。不同的進程使用不同的內存空間,而所有的線程共享一片相同的內存空間。別把它和棧內存搞混,每個線程都擁有單獨的棧內存用來存儲本地數據。

3) 如何在Java中實現線程?

1)java.lang.Thread 類的實例就是一個線程但是它需要調用java.lang.Runnable接口來執行,

2)由於線程類本身就是調用的Runnable接口所以你可以繼承java.lang.Thread 類或者直接調用Runnable接口來重寫run()方法實現線程。

3).實現Callable接口通過FutureTask包裝器來創建Thread線程

Callable接口(也只有一個方法)定義如下:

public interface Callable<V>   { 
  V call() throws Exception;   } 
技術分享圖片 技術分享圖片
public class SomeCallable<V> extends OtherClass implements Callable<V> {

    @Override
    public V call() throws Exception {
        // TODO Auto-generated method stub
        return null;
    }

}

Callable<V> oneCallable = new SomeCallable<V>();   
//由Callable<Integer>創建一個FutureTask<Integer>對象:   
FutureTask<V> oneTask = new FutureTask<V>(oneCallable);   
//註釋:FutureTask<Integer>是一個包裝器,它通過接受Callable<Integer>來創建,它同時實現了Future和Runnable接口。 
  //由FutureTask<Integer>創建一個Thread對象:   
Thread oneThread = new Thread(oneTask);   
oneThread.start();   
//至此,一個線程就創建完成了。
技術分享圖片 4).使用ExecutorService、Callable、Future實現有返回結果的線程

ExecutorService、Callable、Future三個接口實際上都是屬於Executor框架。返回結果的線程是在JDK1.5中引入的新特征,有了這種特征就不需要再為了得到返回值而大費周折了。而且自己實現了也可能漏洞百出。

可返回值的任務必須實現Callable接口。類似的,無返回值的任務必須實現Runnable接口。

執行Callable任務後,可以獲取一個Future的對象,在該對象上調用get就可以獲取到Callable任務返回的Object了。

註意:get方法是阻塞的,即:線程無返回結果,get方法會一直等待。

再結合線程池接口ExecutorService就可以實現傳說中有返回結果的多線程了。

5).Thread 類中的start() 和 run() 方法有什麽區別

1.start()方法來啟動線程,真正實現了多線程運行。這時無需等待run方法體代碼執行完畢,可以直接繼續執行下面的代碼;通過調用Thread類的start()方法來啟動一個線程, 這時此線程是處於就緒狀態, 並沒有運行。 然後通過此Thread類調用方法run()來完成其運行操作的, 這裏方法run()稱為線程體,它包含了要執行的這個線程的內容, Run方法運行結束, 此線程終止。然後CPU再調度其它線程。
2.run()方法當作普通方法的方式調用。程序還是要順序執行,要等待run方法體執行完畢後,才可繼續執行下面的代碼; 程序中只有主線程——這一個線程, 其程序執行路徑還是只有一條, 這樣就沒有達到寫線程的目的。

記住:多線程就是分時利用CPU,宏觀上讓所有線程一起執行 ,也叫並發

6).Java中CyclicBarrier 和 CountDownLatch有什麽不同

CountDownLatch CyclicBarrier
減計數方式 加計數方式
計算為0時釋放所有等待的線程 計數達到指定值時釋放所有等待線程
計數為0時,無法重置 計數達到指定值時,計數置為0重新開始
調用countDown()方法計數減一,調用await()方法只進行阻塞,對計數沒任何影響 調用await()方法計數加1,若加1後的值不等於構造方法的值,則線程阻塞
不可重復利用 可重復利用

7).Java內存模型是什麽?

Java內存模型規定和指引Java程序在不同的內存架構、CPU和操作系統間有確定性地行為。它在多線程的情況下尤其重要。Java內存模型對一個線程所做的變動能被其它線程可見提供了保證,它們之間是先行發生關系。這個關系定義了一些規則讓程序員在並發編程時思路更清晰。比如,先行發生關系確保了:

  • 線程內的代碼能夠按先後順序執行,這被稱為程序次序規則。
  • 對於同一個鎖,一個解鎖操作一定要發生在時間上後發生的另一個鎖定操作之前,也叫做管程鎖定規則。
  • 前一個對volatile的寫操作在後一個volatile的讀操作之前,也叫volatile變量規則。
  • 一個線程內的任何操作必需在這個線程的start()調用之後,也叫作線程啟動規則。
  • 一個線程的所有操作都會在線程終止之前,線程終止規則。
  • 一個對象的終結操作必需在這個對象構造完成之後,也叫對象終結規則。
  • 可傳遞性

8).Java中的volatile 變量是什麽

可見性,是指線程之間的可見性,一個線程修改的狀態對另一個線程是可見的。也就是一個線程修改的結果。另一個線程馬上就能看到。比如:用volatile修飾的變量,就會具有可見性。volatile修飾的變量不允許線程內部緩存和重排序,即直接修改內存。所以對其他線程是可見的。但是這裏需要註意一個問題,volatile只能讓被他修飾內容具有可見性,但不能保證它具有原子性。比如 volatile int a = 0;之後有一個操作 a++;這個變量a具有可見性,但是a++ 依然是一個非原子操作,也就是這個操作同樣存在線程安全問題。

Java語言提供了一種稍弱的同步機制,即volatile變量,用來確保將變量的更新操作通知到其他線程。當把變量聲明為volatile類型後,編譯器與運行時都會註意到這個變量是共享的,因此不會將該變量上的操作與其他內存操作一起重排序。volatile變量不會被緩存在寄存器或者對其他處理器不可見的地方,因此在讀取volatile類型的變量時總會返回最新寫入的值。在訪問volatile變量時不會執行加鎖操作,因此也就不會使執行線程阻塞,因此volatile變量是一種比sychronized關鍵字更輕量級的同步機制。

當對非 volatile 變量進行讀寫的時候,每個線程先從內存拷貝變量到CPU緩存中。如果計算機有多個CPU,每個線程可能在不同的CPU上被處理,這意味著每個線程可以拷貝到不同的 CPU cache 中。而聲明變量是 volatile 的,JVM 保證了每次讀變量都從內存中讀,跳過 CPU cache 這一步。

當一個變量定義為 volatile 之後,將具備兩種特性:保證此變量對所有的線程的可見性;禁止指令重排序優化。有volatile修飾的變量,賦值後多執行了一個“load addl $0x0, (%esp)”操作,這個操作相當於一個內存屏障(指令重排序時不能把後面的指令重排序到內存屏障之前的位置),只有一個CPU訪問內存時,並不需要內存屏障;(什麽是指令重排序:是指CPU采用了允許將多條指令不按程序規定的順序分開發送給各相應電路單元處理)。

9).什麽是線程安全?Vector是一個線程安全類嗎?

如果你的代碼所在的進程中有多個線程在同時運行,而這些線程可能會同時運行這段代碼。如果每次運行結果和單線程運行的結果是一樣的,而且其他的變量的值也和預期的是一樣的,就是線程安全的。一個線程安全的計數器類的同一個實例對象在被多個線程使用的情況下也不會出現計算失誤。很顯然你可以將集合類分成兩組,線程安全和非線程安全的。Vector 是用同步方法來實現線程安全的, 而和它相似的ArrayList不是線程安全的。

10).Java中什麽是競態條件? 舉個例子說明。

當某個計算正確性取決於多個線程的交替執行時序時, 就會發生靜態條件,即爭取的結果要取決於運氣, 最常見的靜態條件就是"先檢查後執行",通過一個可能失效的觀測結果來決定下一步的動作.

例如:

class Counter { protected long count = 0; public void add(long value) { this.count = this.count + value; } }

觀察線程A和B交錯執行會發生什麽,兩個線程分別加了2和3到count變量上,兩個線程執行結束後count變量的值應該等於5。然而由於兩個線程是交叉執行的,兩個線程從內存中讀出的初始值都是0。然後各自加了2和3,並分別寫回內存。最終的值並不是期望的5,而是最後寫回內存的那個線程的值,上面例子中最後寫回內存的是線程A,但實際中也可能是線程B。如果沒有采用合適的同步機制,線程間的交叉執行情況就無法預料。
add()方法就是一個臨界區,它會產生競態條件。

11).一個線程運行時發生異常會怎樣

所以這裏存在兩種情形:
① 如果該異常被捕獲或拋出,則程序繼續運行。
② 如果異常沒有被捕獲該線程將會停止執行。
Thread.UncaughtExceptionHandler是用於處理未捕獲異常造成線程突然中斷情況的一個內嵌接口。當一個未捕獲異常將造成線程中斷的時候JVM會使用Thread.getUncaughtExceptionHandler()來查詢線程的UncaughtExceptionHandler,並將線程和異常作為參數傳遞給handler的uncaughtException()方法進行處理。

12).線程間如何通信,進程間如何通信?

多線程間的通信: 1). 共享變量; 2),wait, notify; 3)Lock/Condition機制; 4).管道機制,創建管道輸出流PipedOutputStream pos和管道輸入流PipedInputStream pis,將pos和pis匹配,pos.connect(pis),將pos賦給信息輸入線程,pis賦給信息獲取線程,就可以實現線程間的通訊了.

管道流雖然使用起來方便,但是也有一些缺點

1)管道流只能在兩個線程之間傳遞數據

線程consumer1和consumer2同時從pis中read數據,當線程producer往管道流中寫入一段數據後,每一個時刻只有一個線程能獲取到數據,並不是兩個線程都能獲取到producer發送來的數據,因此一個管道流只能用於兩個線程間的通訊。不僅僅是管道流,其他IO方式都是一對一傳輸。

2)管道流只能實現單向發送,如果要兩個線程之間互通訊,則需要兩個管道流.

進程間通信:

1)管道(Pipe):管道可用於具有親緣關系進程間的通信,允許一個進程和另一個與它有共同祖先的進程之間進行通信。
(2)命名管道(named pipe):命名管道克服了管道沒有名字的限制,因此,除具有管道所具有的功能外,它還允許無親緣關 系 進程間的通信。命名管道在文件系統中有對應的文件名。命名管道通過命令mkfifo或系統調用mkfifo來創建。
(3)信號(Signal):信號是比較復雜的通信方式,用於通知接受進程有某種事件發生,除了用於進程間通信外,進程還可以發送 信號給進程本身;linux除了支持Unix早期信號語義函數sigal外,還支持語義符合Posix.1標準的信號函數sigaction(實際上,該函數是基於BSD的,BSD為了實現可靠信號機制,又能夠統一對外接口,用sigaction函數重新實現了signal函數)。
(4)消息(Message)隊列:消息隊列是消息的鏈接表,包括Posix消息隊列system V消息隊列。有足夠權限的進程可以向隊列中添加消息,被賦予讀權限的進程則可以讀走隊列中的消息。消息隊列克服了信號承載信息量少,管道只能承載無格式字節流以及緩沖區大小受限等缺
(5)共享內存:使得多個進程可以訪問同一塊內存空間,是最快的可用IPC形式。是針對其他通信機制運行效率較低而設計的。往往與其它通信機制,如信號量結合使用,來達到進程間的同步及互斥。
(6)內存映射(mapped memory):內存映射允許任何多個進程間通信,每一個使用該機制的進程通過把一個共享的文件映射到自己的進程地址空間來實現它。
(7)信號量(semaphore):主要作為進程間以及同一進程不同線程之間的同步手段。

(8)套接口(Socket):更為一般的進程間通信機制,可用於不同機器之間的進程間通信。起初是由Unix系統的BSD分支開發出來的,但現在一般可以移植到其它類Unix系統上:Linux和System V的變種都支持套接字。

13).Java中notify 和 notifyAll有什麽區別?

notify()&notifyall()的共同點:均能喚醒正在等待的線程,並且均是最後只有一個線程獲取資源對象的鎖。

不同點:notify() 只能喚醒一個線程,而notifyall()能夠喚醒所有的線程,當線程被喚醒以後所有被喚醒的線程競爭獲取資源對象的鎖,其中只有一個能夠得到對象鎖,執行代碼。

註意:wait()方法並不是在等待資源的鎖,而是在等待被喚醒(notify()),一旦被喚醒後,被喚醒的線程就具備了資源鎖(因為無需競爭),直至再次執行wait()方法或者synchronized代碼塊執行完畢。

14).為什麽wait, notify 和 notifyAll這些方法不在thread類裏面?

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

15).什麽是ThreadLocal變量?

ThreadLocal一般稱為線程本地變量,它是一種特殊的線程綁定機制,將變量與線程綁定在一起,為每一個線程維護一個獨立的變量副本。通過ThreadLocal可以將對象的可見範圍限制在同一個線程內。

跳出誤區

  需要重點強調的的是,不要拿ThreadLocal和synchronized做類比,因為這種比較壓根就是無意義的!sysnchronized是一種互斥同步機制,是為了保證在多線程環境下對於共享資源的正確訪問。而ThreadLocal從本質上講,無非是提供了一個“線程級”變量作用域,它是一種線程封閉(每個線程獨享變量)技術,更直白點講,ThreadLocal可以理解為將對象的作用範圍限制在一個線程上下文中,使得變量的作用域為“線程級”。

  沒有ThreadLocal的時候,一個線程在其聲明周期內,可能穿過多個層級,多個方法,如果有個對象需要在此線程周期內多次調用,且是跨層級的(線程內共享),通常的做法是通過參數進行傳遞;而ThreadLocal將變量綁定在線程上,在一個線程周期內,無論“你身處何地”,只需通過其提供的get方法就可輕松獲取到對象。極大地提高了對於“線程級變量”的訪問便利性。

16).Java中ThreadLocal變量, volatile變量, synchronized的區別

volatile主要是用來在多線程中同步變量。
在一般情況下,為了提升性能,每個線程在運行時都會將主內存中的變量保存一份在自己的內存中作為變量副本,但是這樣就很容易出現多個線程中保存的副本變量不一致,或與主內存的中的變量值不一致的情況。
而當一個變量被volatile修飾後,該變量就不能被緩存到線程的內存中,它會告訴編譯器不要進行任何移出讀取和寫入操作的優化,換句話說就是不允許有不同於“主”內存區域的變量拷貝,所以當該變量有變化時,所有調用該變量的線程都會獲得相同的值,這就確保了該變量在應用中的可視性(當一個任務做出了修改在應用中必須是可視的),同時性能也相應的降低了(還是比synchronized高)。
但需要註意volatile只能確保操作的是同一塊內存,並不能保證操作的原子性。所以volatile一般用於聲明簡單類型變量,使得這些變量具有原子性,即一些簡單的賦值與返回操作將被確保不中斷。但是當該變量的值由自身的上一個決定時,volatile的作用就將失效,這是由volatile關鍵字的性質所決定的。
所以在volatile時一定要謹慎,千萬不要以為用volatile修飾後該變量的所有操作都是原子操作,不再需要synchronized關鍵字了。

ThreadLocal是一個線程的局部變量(其實就是一個Map),ThreadLocal會為每個使用該變量的線程提供獨立的變量副本,所以每一個線程都可以獨立地改變自己的副本,而不會影響其它線程所對應的副本。這樣做其實就是以空間換時間的方式(與synchronized相反),以耗費內存為代價,單大大減少了線程同步(如synchronized)所帶來性能消耗以及減少了線程並發控制的復雜度。

synchronized關鍵字是Java利用鎖的機制自動實現的,一般有同步方法和同步代碼塊兩種使用方式。Java中所有的對象都自動含有單一的鎖(也稱為監視器),當在對象上調用其任意的synchronized方法時,此對象被加鎖(一個任務可以多次獲得對象的鎖,計數會遞增),同時在線程從該方法返回之前,該對象內其他所有要調用類中被標記為synchronized的方法的線程都會被阻塞。

17).什麽是Future, FutureTask?

 Future就是對於具體的Runnable或者Callable任務的執行結果進行取消、查詢是否完成、獲取結果。必要時可以通過get方法獲取執行結果,該方法會阻塞直到任務返回結果。

  Future類位於java.util.concurrent包下,它是一個接口:

1 2 3 4 5 6 7 8 public interface Future<V> { boolean cancel(boolean mayInterruptIfRunning); boolean isCancelled(); boolean isDone(); V get() throws InterruptedException, ExecutionException; V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException; }

  在Future接口中聲明了5個方法,下面依次解釋每個方法的作用:

  • cancel方法用來取消任務,如果取消任務成功則返回true,如果取消任務失敗則返回false。參數mayInterruptIfRunning表示是否允許取消正在執行卻沒有執行完畢的任務,如果設置true,則表示可以取消正在執行過程中的任務。如果任務已經完成,則無論mayInterruptIfRunning為true還是false,此方法肯定返回false,即如果取消已經完成的任務會返回false;如果任務正在執行,若mayInterruptIfRunning設置為true,則返回true,若mayInterruptIfRunning設置為false,則返回false;如果任務還沒有執行,則無論mayInterruptIfRunning為true還是false,肯定返回true。
  • isCancelled方法表示任務是否被取消成功,如果在任務正常完成前被取消成功,則返回 true。
  • isDone方法表示任務是否已經完成,若任務完成,則返回true;
  • get()方法用來獲取執行結果,這個方法會產生阻塞,會一直等到任務執行完畢才返回;
  • get(long timeout, TimeUnit unit)用來獲取執行結果,如果在指定時間內,還沒獲取到結果,就直接返回null。

  也就是說Future提供了三種功能:

  1)判斷任務是否完成;

  2)能夠中斷任務;

  3)能夠獲取任務執行結果。

public class FutureTask<V> implements RunnableFuture<V>

  FutureTask類實現了RunnableFuture接口,我們看一下RunnableFuture接口的實現:

1 2 3 public interface RunnableFuture<V> extends Runnable, Future<V> { void run(); }

在Java並發程序中FutureTask表示一個可以取消的異步運算。它有啟動和取消運算、查詢運算是否完成和取回運算結果等方法。只有當運算完成的時候結果才能取回,如果運算尚未完成get方法將會阻塞。一個FutureTask對象可以對調用了Callable和Runnable的對象進行包裝,由於FutureTask也是調用了Runnable接口所以它可以提交給Executor來執行。

18).Java中interrupted 和 isInterruptedd方法的區別?

interrupted()isInterrupted()的主要區別是前者會將中斷狀態清除而後者不會。Java多線程的中斷機制是用內部標識來實現的,調用Thread.interrupt()來中斷一個線程就會設置中斷標識為true。當中斷線程調用靜態方法Thread.interrupted()來檢查中斷狀態時,中斷狀態會被清零。而非靜態方法isInterrupted()用來查詢其它線程的中斷狀態且不會改變中斷狀態標識。簡單的說就是任何拋出InterruptedException異常的方法都會將中斷狀態清零。無論如何,一個線程的中斷狀態有有可能被其它線程調用中斷來改變。

interrupt方法是用於中斷線程的,調用該方法的線程的狀態將被置為"中斷"狀態。註意:線程中斷僅僅是設置線程的中斷狀態位,不會停止線程。需要用戶自己去監視線程的狀態為並做處理。支持線程中斷的方法(也就是線程中斷後會拋出InterruptedException的方法,比如這裏的sleep,以及Object.wait等方法)就是在監視線程的中斷狀態,一旦線程的中斷狀態被置為“中斷狀態”,就會拋出中斷異常。

interrupted方法的實現:

  1. public static boolean interrupted() {
  2. return currentThread().isInterrupted(true);
  3. }


和isInterrupted的實現

  1. public boolean isInterrupted() {
  2. return isInterrupted(false);
  3. }

這兩個方法一個是static的,一個不是,但實際上都是在調用同一個方法,只是interrupted方法傳入的參數為true,而inInterrupted傳入的參數為false。這是一個native方法,看不到源碼沒有關系,參數名字ClearInterrupted已經清楚的表達了該參數的作用----是否清除中斷狀態。方法的註釋也清晰的表達了“中斷狀態將會根據傳入的ClearInterrupted參數值確定是否重置”。所以,靜態方法interrupted將會清除中斷狀態(傳入的參數ClearInterrupted為true),而實例方法isInterrupted則不會(傳入的參數ClearInterrupted為false)。

19).Java中volatile和原子類?

如果一個變量加了volatile關鍵字,就會告訴編譯器和JVM的內存模型:這個變量是對所有線程共享的、可見的,每次jvm都會讀取最新寫入的值並使其最新值在所有CPU可見。volatile似乎是有時候可以代替簡單的鎖,似乎加了volatile關鍵字就省掉了鎖。但又說volatile不能保證原子性(java程序員很熟悉這句話:volatile僅僅用來保證該變量對所有線程的可見性,但不保證原子性)。如果你的字段是volatile,Java內存模型將在寫操作後插入一個寫屏障指令,在讀操作前插入一個讀屏障指令。這意味著如果你對一個volatile字段進行寫操作,你必須知道:1、一旦你完成寫入,任何訪問這個字段的線程將會得到最新的值。2、在你寫入前,會保證所有之前發生的事已經發生,並且任何更新過的數據值也是可見的,因為內存屏障會把之前的寫入值都刷新到緩存。

volatile為什麽沒有原子性?

明白了內存屏障(memory barrier)這個CPU指令,回到前面的JVM指令:從Load到store到內存屏障,一共4步,其中最後一步jvm讓這個最新的變量的值在所有線程可見,也就是最後一步讓所有的CPU內核都獲得了最新的值,但中間的幾步(從Load到Store)是不安全的,中間如果其他的CPU修改了值將會丟失。

原子類保證了解決了上述的volatile的原子性沒有保證的問題, 用到了CAS操作,

因為CAS是基於樂觀鎖的,也就是說當寫入的時候,如果寄存器舊值已經不等於現值,說明有其他CPU在修改,那就繼續嘗試。所以這就保證了操作的原子性。

技術分享圖片

19).為什麽wait和notify方法要在同步塊中調用?

主要是因為Java API強制要求這樣做,如果你不這麽做,你的代碼會拋出IllegalMonitorStateException異常。還有一個原因是為了避免wait和notify之間產生競態條件。

20).Java中的同步集合與並發集合有什麽區別

Synchronized vs Concurrent Collections

不管是同步集合還是並發集合他們都支持線程安全,他們之間主要的區別體現在性能可擴展性,還有他們如何實現的線程安全。同步HashMap, Hashtable, HashSet, Vector, ArrayList 相比他們並發的實現(比如:ConcurrentHashMap, CopyOnWriteArrayList, CopyOnWriteHashSet)會慢得多。造成如此慢的主要原因是, 同步集合會把整個Map或List鎖起來,而並發集合不會。並發集合實現線程安全是通過使用先進的和成熟的技術像鎖剝離。比如ConcurrentHashMap 會把整個Map 劃分成幾個片段,只對相關的幾個片段上鎖,同時允許多線程訪問其他未上鎖的片段。

同樣的,CopyOnWriteArrayList 允許多個線程以非同步的方式讀,當有線程寫的時候它會將整個List復制一個副本給它。

如果在讀多寫少這種對並發集合有利的條件下使用並發集合,這會比使用同步集合更具有可伸縮性。

同步集合與並發集合都為多線程和並發提供了合適的線程安全的集合,不過並發集合的可擴展性更高。在Java1.5之前程序員們只有同步集合來用且在多線程並發的時候會導致爭用,阻礙了系統的擴展性。Java5介紹了並發集合像ConcurrentHashMap,不僅提供線程安全還用鎖分離和內部分區等現代技術提高了可擴展性。

21).Java中堆和棧有什麽不同?

因為棧是一塊和線程緊密相關的內存區域。每個線程都有自己的棧內存,用於存儲本地變量,方法參數和棧調用,一個線程中存儲的變量對其它線程是不可見的。而堆是所有線程共享的一片公用內存區域。對象都在堆裏創建,為了提升效率線程會從堆中弄一個緩存到自己的棧,如果多個線程使用該變量就可能引發問題,這時volatile 變量就可以發揮作用了,它要求線程從主存中讀取變量的值。

22).什麽是線程池? 為什麽要使用它?

創建線程要花費昂貴的資源和時間,如果任務來了才創建線程那麽響應時間會變長,而且一個進程能創建的線程數有限。為了避免這些問題,在程序啟動的時候就創建若幹線程來響應處理,它們被稱為線程池,裏面的線程叫工作線程。從JDK1.5開始,Java API提供了Executor框架讓你可以創建不同的線程池。比如單線程池,每次處理一個任務;數目固定的線程池或者是緩存線程池(一個適合很多生存期短的任務的程序的可擴展線程池)。

22).實現生產者消費者模式

技術分享圖片
package ProducterAndConsumer.Version1;

import java.util.Random;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 生產者

 * 生產者消費者模型
 */

public class Producer implements Runnable {
    private volatile boolean isRunning = true;
    private BlockingQueue<PCData> queue;// 內存緩沖區
    private static AtomicInteger count = new AtomicInteger();// 總數 原子操作
    private static final int SLEEPTIME = 1000;

    public Producer(BlockingQueue<PCData> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        PCData data = null;
        Random r = new Random();
        System.out.println("start producting id:" + Thread.currentThread().getId());
        try {
            while (isRunning) {
                Thread.sleep(r.nextInt(SLEEPTIME));
                data = new PCData(count.incrementAndGet());
                System.out.println(data + " 加入隊列");
                if (!queue.offer(data, 2, TimeUnit.SECONDS)) {
                    System.err.println(" 加入隊列失敗");
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
            Thread.currentThread().interrupt();
        }

    }

    public void stop() {
        isRunning = false;
    }
}
技術分享圖片 技術分享圖片 技術分享圖片 技術分享圖片
package ProducterAndConsumer.Version1;
/**
 * 消費者
 * 
 */
import java.text.MessageFormat;
import java.util.Random;
import java.util.concurrent.BlockingQueue;

public class Consumer implements Runnable{
    private BlockingQueue<PCData> queue;
    private static final int SLEEPTIME = 1000;
    public Consumer(BlockingQueue<PCData> queue){
        this.queue = queue;
    }

    @Override
    public void run() {
        System.out.println("start Consumer id :"+Thread.currentThread().getId());
        Random r = new Random();
        try{
            while(true){
                PCData data = queue.take();
                if(data != null)
                {
                    int re = data.getData() * data.getData();
                    System.out.println(MessageFormat.format("{0}*{1}={2}", data.getData(),data.getData(),re));
                    Thread.sleep(r.nextInt(SLEEPTIME));
                }
            }
        }catch (InterruptedException e) {
            e.printStackTrace();
            Thread.currentThread().interrupt();
        }
    }

}
技術分享圖片 技術分享圖片 技術分享圖片 技術分享圖片
package ProducterAndConsumer.Version1;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingDeque;

/**
 * 主函數
 * @author ctk
 *
 */
public class Main {
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<PCData> queue = new LinkedBlockingDeque<>(10);
        Producer p1 = new Producer(queue);
        Producer p2 = new Producer(queue);
        Producer p3 = new Producer(queue);
        Consumer c1 = new Consumer(queue);
        Consumer c2 = new Consumer(queue);
        Consumer c3 = new Consumer(queue);
        ExecutorService service = Executors.newCachedThreadPool();
        service.execute(p1);
        service.execute(p2);
        service.execute(p3);
        service.execute(c1);
        service.execute(c2);
        service.execute(c3);
        Thread.sleep(10*1000);
        p1.stop();
        p2.stop();
        p3.stop();
        Thread.sleep(3000);
        service.shutdown();
    }
}
技術分享圖片 技術分享圖片 技術分享圖片 技術分享圖片
package ProducterAndConsumer.Version1;
/**
 * 容器數據類型
 * @author ctk
 *
 */
public class PCData {
    private final int intData;
    public PCData(int d){
        intData = d;
    }
    public PCData(String d){
        intData = Integer.valueOf(d);
    }
    public int getData(){
        return intData;
    }
    @Override
    public String toString(){
        return "data:"+intData;
    }
}
技術分享圖片 技術分享圖片

23).如何避免死鎖?

Java多線程中的死鎖
死鎖是指兩個或兩個以上的進程在執行過程中,因爭奪資源而造成的一種互相等待的現象,若無外力作用,它們都將無法推進下去。這是一個嚴重的問題,因為死鎖會讓你的程序掛起無法完成任務,死鎖的發生必須滿足以下四個條件:

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

避免死鎖最簡單的方法就是阻止循環等待條件,將系統中所有的資源設置標誌位、排序,規定所有的進程申請資源必須以一定的順序(升序或降序)做操作來避免死鎖。

24).Java中活鎖和死鎖有什麽區別?

活鎖和死鎖類似,不同之處在於處於活鎖的線程或進程的狀態是不斷改變的,活鎖可以認為是一種特殊的饑餓。一個現實的活鎖例子是兩個人在狹小的走廊碰到,兩個人都試著避讓對方好讓彼此通過,但是因為避讓的方向都一樣導致最後誰都不能通過走廊。簡單的說就是,活鎖和死鎖的主要區別是前者進程的狀態可以改變但是卻不能繼續執行。

25)怎麽檢測一個線程是否擁有鎖?

在java.lang.Thread中有一個方法叫holdsLock(),它返回true如果當且僅當當前線程擁有某個具體對象的鎖。

26).JVM中哪個參數是用來控制線程的棧堆棧小的

-Xss參數用來控制線程的堆棧大小。

27).Java中synchronized 和 ReentrantLock 有什麽不同?

相似點:

這兩種同步方式有很多相似之處,它們都是加鎖方式同步,而且都是阻塞式的同步,也就是說當如果一個線程獲得了對象鎖,進入了同步塊,其他訪問該同步塊的線程都必須阻塞在同步塊外面等待,而進行線程阻塞和喚醒的代價是比較高的(操作系統需要在用戶態與內核態之間來回切換,代價很高,不過可以通過對鎖優化進行改善)。

區別:

這兩種方式最大區別就是對於Synchronized來說,它是java語言的關鍵字,是原生語法層面的互斥,需要jvm實現。而ReentrantLock它是JDK 1.5之後提供的API層面的互斥鎖,需要lock()和unlock()方法配合try/finally語句塊來完成。

Synchronized進過編譯,會在同步塊的前後分別形成monitorenter和monitorexit這個兩個字節碼指令。在執行monitorenter指令時,首先要嘗試獲取對象鎖。如果這個對象沒被鎖定,或者當前線程已經擁有了那個對象鎖,把鎖的計算器加1,相應的,在執行monitorexit指令時會將鎖計算器就減1,當計算器為0時,鎖就被釋放了。如果獲取對象鎖失敗,那當前線程就要阻塞,直到對象鎖被另一個線程釋放為止。

由於ReentrantLock是java.util.concurrent包下提供的一套互斥鎖,相比Synchronized,ReentrantLock類提供了一些高級功能,主要有以下3項:

1.等待可中斷,持有鎖的線程長期不釋放的時候,正在等待的線程可以選擇放棄等待,這相當於Synchronized來說可以避免出現死鎖的情況。

2.公平鎖,多個線程等待同一個鎖時,必須按照申請鎖的時間順序獲得鎖,Synchronized鎖非公平鎖,ReentrantLock默認的構造函數是創建的非公平鎖,可以通過參數true設為公平鎖,但公平鎖表現的性能不是很好。

3.鎖綁定多個條件,一個ReentrantLock對象可以同時綁定對個對象。

28).有三個線程T1,T2,T3,怎麽確保它們按順序執行?

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

29).Thread類中的yield方法有什麽作用?

yield()方法可以讓當前正在執行的線程暫停,但它不會阻塞該線程,它只是將該線程從運行狀態轉入就緒狀態

只是讓當前的線程暫停一下,讓系統的線程調度器重新調度一次。

很有可能,當某個線程調用了yield()方法暫停之後進入就緒狀態,它又馬上搶占了CPU的執行權,繼續執行。

實際上,當某個線程調用了yield()方法暫停之後,只有優先級與當前線程相同,或者優先級比當前線程更高的處於就緒狀態的線程才會獲得執行的機會。

【關於sleep和yield的區別】

1.sleep()方法暫停當前線程後,會給其他線程執行機會,線程優先級對此沒有影響。

yield()方法會給優先級相同或更高的線程更高的執行機會。

2.sleep()方法會將線程轉入阻塞狀態,直到阻塞時間結束,才會轉入就緒狀態。

yield()方法會將當前線程直接轉入就緒狀態。

3.sleep()方法聲明拋出了InterruptedException異常,所以調用sleep()方法時要麽捕捉該異常,要麽顯示聲明拋出該異常。

yield()方法則沒有聲明拋出任何異常。

4.sleep()方法比yield()方法有更好的移植性,通常不建議使用yield()方法來控制並發線程的執行.

30).Java中Semaphore是什麽?

Java中的Semaphore是一種新的同步類,它是一個計數信號。從概念上講,從概念上講,信號量維護了一個許可集合。如有必要,在許可可用前會阻塞每一個 acquire(),然後再獲取該許可。每個 release()添加一個許可,從而可能釋放一個正在阻塞的獲取者。但是,不使用實際的許可對象,Semaphore只對可用許可的號碼進行計數,並采取相應的行動。信號量常常用於多線程的代碼中,比如數據庫連接池。

31).如果你提交任務時,線程池隊列已滿。會時發會生什麽?

如果一個任務不能被調度執行那麽ThreadPoolExecutor’s submit()方法將會拋出一個RejectedExecutionException異常。

32).Java線程池中submit() 和 execute()方法有什麽區別?

1.對返回值的處理不同
execute方法不關心返回值。
submit方法有返回值,Future.
2.對異常的處理不同
excute方法會拋出異常。
sumbit方法不會拋出異常。除非你調用Future.get()

33).什麽是阻塞式方法?

阻塞式方法是指程序會一直等待該方法完成期間不做其他事情,ServerSocket的accept()方法就是一直等待客戶端連接。這裏的阻塞是指調用結果返回之前,當前線程會被掛起,直到得到結果之後才會返回。此外,還有異步和非阻塞式方法在任務完成前就返回。

34)Swing是線程安全的嗎? 為什麽?

Swing不是線程安全的,但是你應該解釋這麽回答的原因即便面試官沒有問你為什麽。當我們說swing不是線程安全的常常提到它的組件,這些組件不能在多線程中進行修改,所有對GUI組件的更新都要在AWT線程中完成,而Swing提供了同步和異步兩種回調方法來進行更新。

35).Java中invokeAndWait 和 invokeLater有什麽區別?

invokeAndWait:後面的程序必須等這個線程(參數中的線程)的東西執行完才能執行
invokeLater:後面的程序和這個參數的線程對象可以並行,異步地執行

invokeLater一般用於在線程裏修改swing組件的外觀,因為swing組件是非同步的,所以不能在線程中直接修改,會不同步,得不到期望的效果,所以要把修改外觀的代碼放在一個單獨的線程中,交給invokeLater:後面的程序和這個參數的線程對象可以並行,異步地執行.

36).Swing API中那些方法是線程安全的?

這個問題又提到了swing和線程安全,雖然組件不是線程安全的但是有一些方法是可以被多線程安全調用的,比如repaint(), revalidate()。 JTextComponent的setText()方法和JTextArea的insert() 和 append() 方法也是線程安全的。

37).如何在Java中創建Immutable(不可變)對象?

Immutable對象可以在沒有同步的情況下共享,降低了對該對象進行並發訪問時的同步化開銷。可是Java沒有@Immutable這個註解符,要創建不可變類,要實現下面幾個步驟:通過構造方法初始化所有成員、對變量不要提供setter方法、將所有的成員聲明為私有的,這樣就不允許直接訪問這些成員、在getter方法中,不要直接返回對象本身,而是克隆對象,並返回對象的拷貝。

38).Java中的ReadWriteLock是什麽?

Java中的ReadWriteLock是Java 5 中新增的一個接口,一個ReadWriteLock維護一對關聯的鎖,一個用於只讀操作一個用於寫。在沒有寫線程的情況下一個讀鎖可能會同時被多個讀線程持有。寫鎖是獨占的,你可以使用JDK中的ReentrantReadWriteLock來實現這個規則,它最多支持65535個寫鎖和65535個讀鎖。

39).多線程中的忙循環是什麽?

忙循環就是程序員用循環讓一個線程等待,不像傳統方法wait(), sleep() 或 yield() 它們都放棄了CPU控制,而忙循環不會放棄CPU,它就是在運行一個空循環。這麽做的目的是為了保留CPU緩存,在多核系統中,一個等待線程醒來的時候可能會在另一個內核運行,這樣會重建緩存。為了避免重建緩存和減少等待重建的時間就可以使用它了。

40).如果同步塊內的線程拋出異常會發生什麽?

無論你的同步塊是正常還是異常退出的,裏面的線程都會釋放鎖.

41).單例模式的雙檢鎖是什麽?

public static Singleton getInstance()
{
  if (instance == null)
  {
    synchronized(Singleton.class) {  //1
      if (instance == null)          //2
        instance = new Singleton();  //3
    }
  }
  return instance;
}

雙重檢查鎖定背後的理論是:在 //2 處的第二次檢查使(如清單 3 中那樣)創建兩個不同的 Singleton 對象成為不可能。假設有下列事件序列:

  1. 線程 1 進入 getInstance() 方法。

  2. 由於 instancenull,線程 1 在 //1 處進入 synchronized 塊。

  3. 線程 1 被線程 2 預占。

  4. 線程 2 進入 getInstance() 方法。

  5. 由於 instance 仍舊為 null,線程 2 試圖獲取 //1 處的鎖。然而,由於線程 1 持有該鎖,線程 2 在 //1 處阻塞。

  6. 線程 2 被線程 1 預占。

  7. 線程 1 執行,由於在 //2 處實例仍舊為 null,線程 1 還創建一個 Singleton 對象並將其引用賦值給 instance

  8. 線程 1 退出 synchronized 塊並從 getInstance() 方法返回實例。

  9. 線程 1 被線程 2 預占。

  10. 線程 2 獲取 //1 處的鎖並檢查 instance 是否為 null

  11. 由於 instance 是非 null 的,並沒有創建第二個 Singleton 對象,由線程 1 創建的對象被返回。

雙重檢查鎖定背後的理論是完美的。不幸地是,現實完全不同。雙重檢查鎖定的問題是:並不能保證它會在單處理器或多處理器計算機上順利運行。

雙重檢查鎖定失敗的問題並不歸咎於 JVM 中的實現 bug,而是歸咎於 Java 平臺內存模型。內存模型允許所謂的“無序寫入”,這也是這些習語失敗的一個主要原因。

無序寫入

為解釋該問題,需要重新考察上述清單 4 中的 //3 行。此行代碼創建了一個 Singleton 對象並初始化變量 instance 來引用此對象。這行代碼的問題是:在 Singleton 構造函數體執行之前,變量 instance 可能成為非 null 的。



42).寫出3條你遵循的多線程最佳實踐

這種問題我最喜歡了,我相信你在寫並發代碼來提升性能的時候也會遵循某些最佳實踐。以下三條最佳實踐我覺得大多數Java程序員都應該遵循:

  • 給你的線程起個有意義的名字。
    這樣可以方便找bug或追蹤。OrderProcessor, QuoteProcessor or TradeProcessor 這種名字比 Thread-1. Thread-2 and Thread-3 好多了,給線程起一個和它要完成的任務相關的名字,所有的主要框架甚至JDK都遵循這個最佳實踐。
  • 避免鎖定和縮小同步的範圍
    鎖花費的代價高昂且上下文切換更耗費時間空間,試試最低限度的使用同步和鎖,縮小臨界區。因此相對於同步方法我更喜歡同步塊,它給我擁有對鎖的絕對控制權。
  • 多用同步類少用wait 和 notify
    首先,CountDownLatch, Semaphore, CyclicBarrier 和 Exchanger 這些同步類簡化了編碼操作,而用wait和notify很難實現對復雜控制流的控制。其次,這些類是由最好的企業編寫和維護在後續的JDK中它們還會不斷優化和完善,使用這些更高等級的同步工具你的程序可以不費吹灰之力獲得優化。
  • 多用並發集合少用同步集合
    這是另外一個容易遵循且受益巨大的最佳實踐,並發集合比同步集合的可擴展性更好,所以在並發編程時使用並發集合效果更好。如果下一次你需要用到map,你應該首先想到用ConcurrentHashMap。

43).Java中的fork join框架是什麽?

fork join框架是JDK7中出現的一款高效的工具,Java開發人員可以通過它充分利用現代服務器上的多處理器。它是專門為了那些可以遞歸劃分成許多子模塊設計的,目的是將所有可用的處理能力用來提升程序的性能。fork join框架一個巨大的優勢是它使用了工作竊取算法,可以完成更多任務的工作線程可以從其它線程中竊取任務來執行。

44).Java多線程中調用wait() 和 sleep()方法有什麽不同?

Java程序中wait 和 sleep都會造成某種形式的暫停,它們可以滿足不同的需要。wait()方法用於線程間通信,如果等待條件為真且其它線程被喚醒時它會釋放鎖,而sleep()方法僅僅釋放CPU資源或者讓當前線程停止執行一段時間,但不會釋放鎖。

Java多線程面試題整理