JAVA SE 面試題 —— JAVA 多線程面試題

什麼是線程?

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

線程和進程有什麼區別?

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

如何在Java中實現線程?

java.lang.Thread 類的實例就是一個線程但是它需要調用java.lang.Runnable接口來執行,由於線程類本身就是調用的Runnable接口所以你可以繼承java.lang.Thread 類或者直接調用Runnable接口來重寫run()方法實現線程。
實現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();   

至此,一個線程就創建完成了。

使用ExecutorService、Callable、Future實現有返回結果的線程

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

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

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

注意:get方法是阻塞的,即:線程無返回結果,get方法會一直等待,再結合線程池接口ExecutorService就可以實現傳說中有返回結果的多線程了。

Thread 類中的start() 和 run() 方法有什麼區別

(1)start()方法
start()方法來啓動線程,真正實現了多線程運行。這時無需等待run方法體代碼執行完畢,可以直接繼續執行下面的代碼;通過調用Thread類的start()方法來啓動一個線程, 這時此線程是處於就緒狀態, 並沒有運行。 然後通過此Thread類調用方法run()來完成其運行操作的, 這裏方法run()稱爲線程體,它包含了要執行的這個線程的內容, Run方法運行結束, 此線程終止。然後CPU再調度其它線程。

(2)run()方法
run()方法當作普通方法的方式調用。程序還是要順序執行,要等待run方法體執行完畢後,纔可繼續執行下面的代碼; 程序中只有主線程——這一個線程, 其程序執行路徑還是隻有一條, 這樣就沒有達到寫線程的目的。

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

Java中CyclicBarrier 和 CountDownLatch有什麼不同

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

Java內存模型是什麼?

Java內存模型規定和指引Java程序在不同的內存架構、CPU和操作系統間有確定性地行爲。它在多線程的情況下尤其重要。

Java內存模型對一個線程所做的變動能被其它線程可見提供了保證,它們之間是先行發生關係。這個關係定義了一些規則讓程序員在併發編程時思路更清晰。比如,先行發生關係確保了:

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

Java中的volatile 變量是什麼

可見性,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採用了允許將多條指令不按程序規定的順序分開發送給各相應電路單元處理)。

什麼是線程安全?Vector是一個線程安全類嗎?

如果你的代碼所在的進程中有多個線程在同時運行,而這些線程可能會同時運行這段代碼。如果每次運行結果和單線程運行的結果是一樣的,而且其他的變量的值也和預期的是一樣的,就是線程安全的。

一個線程安全的計數器類的同一個實例對象在被多個線程使用的情況下也不會出現計算失誤。很顯然你可以將集合類分成兩組,線程安全和非線程安全的。

Vector 是用同步方法來實現線程安全的, 而和它相似的ArrayList不是線程安全的。

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()方法就是一個臨界區,它會產生競態條件。

一個線程運行時發生異常會怎樣?

所以這裏存在兩種情形:

  • 如果該異常被捕獲或拋出,則程序繼續運行。
  • 如果異常沒有被捕獲該線程將會停止執行。

Thread.UncaughtExceptionHandler是用於處理未捕獲異常造成線程突然中斷情況的一個內嵌接口。當一個未捕獲異常將造成線程中斷的時候JVM會使用Thread.getUncaughtExceptionHandler()來查詢線程的UncaughtExceptionHandler,並將線程和異常作爲參數傳遞給handler的uncaughtException()方法進行處理。

線程間如何通信,進程間如何通信?

多線程間的通信:
(1)共享變量;

(2)wait, notify;

(3)Lock/Condition機制;

(4)管道機制,創建管道輸出流PipedOutputStream pos和管道輸入流PipedInputStream pis,將pos和pis匹配,pos.connect(pis),將pos賦給信息輸入線程,pis賦給信息獲取線程,就可以實現線程間的通訊了。

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

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

(5)進程間通信:

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

Java中notify 和 notifyAll有什麼區別?

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

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

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

爲什麼wait, notify 和 notifyAll這些方法不在thread類裏面?

一個很明顯的原因是JAVA提供的鎖是對象級的而不是線程級的,每個對象都有鎖,通過線程獲得。如果線程需要等待某些鎖那麼調用對象中的wait()方法就有意義了。如果wait()方法定義在Thread類中,線程正在等待的是哪個鎖就不明顯了。

簡單的說,由於wait,notify和notifyAll都是鎖級別的操作,所以把他們定義在Object類中因爲鎖屬於對象。

什麼是ThreadLocal變量?

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

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

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

Java中ThreadLocal變量、volatile變量、synchronized的區別?

volatile主要是用來在多線程中同步變量。
在一般情況下,爲了提升性能,每個線程在運行時都會將主內存中的變量保存一份在自己的內存中作爲變量副本,但是這樣就很容易出現多個線程中保存的副本變量不一致,或與主內存的中的變量值不一致的情況。

而當一個變量被volatile修飾後,該變量就不能被緩存到線程的內存中,它會告訴編譯器不要進行任何移出讀取和寫入操作的優化,換句話說就是不允許有不同於“主”內存區域的變量拷貝,所以當該變量有變化時,所有調用該變量的線程都會獲得相同的值,這就確保了該變量在應用中的可視性(當一個任務做出了修改在應用中必須是可視的),同時性能也相應的降低了(還是比synchronized高)。

但需要注意volatile只能確保操作的是同一塊內存,並不能保證操作的原子性。所以volatile一般用於聲明簡單類型變量,使得這些變量具有原子性,即一些簡單的賦值與返回操作將被確保不中斷。但是當該變量的值由自身的上一個決定時,volatile的作用就將失效,這是由volatile關鍵字的性質所決定的。
所以在volatile時一定要謹慎,千萬不要以爲用volatile修飾後該變量的所有操作都是原子操作,不再需要synchronized關鍵字了。

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

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

什麼是Future,FutureTask?

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

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

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提供了三種功能:

  • 判斷任務是否完成;
  • 能夠中斷任務;
  • 能夠獲取任務執行結果。
public class FutureTask<V> implements RunnableFuture<V>
FutureTask類實現了RunnableFuture接口,我們看一下RunnableFuture接口的實現:
public interface RunnableFuture<V> extends Runnable, Future<V> {
    void run();
}

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

Java中堆和棧有什麼不同?

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

什麼是線程池? 爲什麼要使用它?

創建線程要花費昂貴的資源和時間,如果任務來了才創建線程那麼響應時間會變長,而且一個進程能創建的線程數有限。爲了避免這些問題,在程序啓動的時候就創建若干線程來響應處理,它們被稱爲線程池,裏面的線程叫工作線程。

從JDK1.5開始,Java API提供了Executor框架讓你可以創建不同的線程池。比如單線程池,每次處理一個任務;數目固定的線程池或者是緩存線程池(一個適合很多生存期短的任務的程序的可擴展線程池)。

Java中interrupted 和 isInterruptedd方法的區別?

interrupted()isInterrupted()的主要區別是前者會將中斷狀態清除而後者不會。

Java多線程的中斷機制是用內部標識來實現的,調用Thread.interrupt()來中斷一個線程就會設置中斷標識爲true。當中斷線程調用靜態方法Thread.interrupted()來檢查中斷狀態時,中斷狀態會被清零。而非靜態方法isInterrupted()用來查詢其它線程的中斷狀態且不會改變中斷狀態標識。

簡單的說就是任何拋出InterruptedException異常的方法都會將中斷狀態清零。無論如何,一個線程的中斷狀態有有可能被其它線程調用中斷來改變。

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

interrupted方法的實現:

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

和isInterrupted的實現
public boolean isInterrupted() {  
    return isInterrupted(false);  
}  

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

Java中volatile和原子類?

如果一個變量加了volatile關鍵字,就會告訴編譯器和JVM的內存模型:這個變量是對所有線程共享的、可見的,每次jvm都會讀取最新寫入的值並使其最新值在所有CPU可見。

volatile似乎是有時候可以代替簡單的鎖,似乎加了volatile關鍵字就省掉了鎖。但又說volatile不能保證原子性(java程序員很熟悉這句話:volatile僅僅用來保證該變量對所有線程的可見性,但不保證原子性)。如果你的字段是volatile,Java內存模型將在寫操作後插入一個寫屏障指令,在讀操作前插入一個讀屏障指令。

這意味着如果你對一個volatile字段進行寫操作,你必須知道:

  • 一旦你完成寫入,任何訪問這個字段的線程將會得到最新的值。
  • 在你寫入前,會保證所有之前發生的事已經發生,並且任何更新過的數據值也是可見的,因爲內存屏障會把之前的寫入值都刷新到緩存。

volatile爲什麼沒有原子性?

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

原子類保證瞭解決了上述的volatile的原子性沒有保證的問題, 用到了CAS操作,因爲CAS是基於樂觀鎖的,也就是說當寫入的時候,如果寄存器舊值已經不等於現值,說明有其他CPU在修改,那就繼續嘗試。所以這就保證了操作的原子性。

爲什麼wait和notify方法要在同步塊中調用?

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

Java中的同步集合與併發集合有什麼區別

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

同樣的,CopyOnWriteArrayList 允許多個線程以非同步的方式讀,當有線程寫的時候它會將整個List複製一個副本給它。
如果在讀多寫少這種對併發集合有利的條件下使用併發集合,這會比使用同步集合更具有可伸縮性。

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

synchronized 關鍵字?

synchronized關鍵字解決的是多個線程之間訪問資源的同步性,synchronized關鍵字可以保證被它修飾的方法或者代碼塊在任意時刻只能有一個線程執行。

另外,在 Java 早期版本中,synchronized屬於重量級鎖,效率低下,因爲監視器鎖(monitor)是依賴於底層的操作系統的 Mutex Lock 來實現的,Java 的線程是映射到操作系統的原生線程之上的。如果要掛起或者喚醒一個線程,都需要操作系統幫忙完成,而操作系統實現線程之間的切換時需要從用戶態轉換到內核態,這個狀態之間的轉換需要相對比較長的時間,時間成本相對較高,這也是爲什麼早期的 synchronized 效率低的原因。

慶幸的是在 Java 6 之後 Java 官方對從 JVM 層面對synchronized 較大優化,所以現在的 synchronized 鎖效率也優化得很不錯了。JDK1.6對鎖的實現引入了大量的優化,如自旋鎖、適應性自旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級鎖等技術來減少鎖操作的開銷。

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