《JAVA編程思想》5分鐘速成:第21章(併發)

第二十章、併發

前言

  • 順序編程:即程序中的所有事物在任意時刻都只能執行一個步驟。
  • 併發編程:程序能夠並行地執行程序中的多個部分。

1、GC 是什麼?為什麼要有GC?

2、Java 中會存在內存洩漏嗎,請簡單描述。

3、描述一下JVM 加載class文件的原理機制?

4、解釋內存中的棧(stack)、堆(heap)和靜態存儲區的用法。


21.1 併發的多面性

併發編程的難點:

  • 併發需要解決的問題有多個;實現併發的方式有多種;
  • 並且,上述兩者之間沒有明確的映射關係。

21.1.1 更快的執行

  • 併發目的:通常是為了提高運行在單處理器上的程序性能;
  • 併發實現1:最直接的方式是操作系統級別使用進程;
  • 併發實現2:Java採用的是更加傳統的方式:在順序型語言的基礎上提供對線程的支持。

併發和並行的區別

  • 併發:輪流處理多個任務:其實是按順序執行的,cpu在任一時間只執行一個線程,通過給不同線程分配時間段的形式來進行調度,只是看起來好像多個任務是同時執行的;
  • 並行:同時處理多個任務:就是多個任務同一時刻在同時進行;

21.1.2 改進代碼設計

  • 搶佔式線程機制:調度機制會週期性的中端線程,將上下文切換到另一個線程,從而為每個線程提供時間片,使得每個線程都會分配到數量合理的時間去驅動它的任務。
  • 非搶佔線程機制:每個線程可以需要CPU多少時間就佔用CPU多少時間。在這種調度方式下,可能一個執行時間很長的線程使得其他所有需要CPU的線程”餓死”。在處理機空閒,即該進程沒有使用CPU時,系統可以允許其他的進程暫時使用CPU。佔用CPU的線程擁有對CPU的控制權,只有它自己主動釋放CPU時,其他的線程才可以使用CPU。
  • 線程機制的選擇:Java採用的是搶佔式線程機制。

 

21.2 基本的線程機制

  • 併發編程:使得我們可以將程序劃分為多個分離的、獨立運行的任務。
  • 多線程機制:通過多線程機制,這些獨立子任務中的每一個都將由執行線程驅動;

21.2.1 定義任務

  • 任務:線程可以驅動任務,因此你需要一種描述任務的方式,這可以由Runnable接口來提供。要想定義任務,只需實現Runnable接口並編寫run()方法,使得該任務可以執行你的命令。
  • 線程執行:當從Runnable導出一個類時,它必須具有run()方法,但是這個方法並無特殊之處。它不會產生任何內在的線程能力。要實現線程行為,你必須顯式地將一個任務附著到線程上。

21.2.2 Thread類(線程)

任務類型:

  • 無結果返回的任務Runnable:將Runnable對象轉變為工作任務的傳統方式是把它提交給一個Thread的構造器(作為參數傳入)
  • 有結果返回的任務Callable:配合使用Future實現獲取任務結果。

Thread線程對象t包含的功能如下

  • new Thread(runnable) 創建線程對象
  • t.start() 啟動線程
  • Thread.sleep(100) 休眠
  • Thread.yield() 讓步(暗示cpu可轉為調度其他線程) --慎用!
  • Thread.currentThread() 獲取當前thread對象
  • t.setProirity(proitity) 設置線程優先級
  • t.setDaemon(true or false) 設置是否為後臺任務
  • t.isDaemon() 是否為後臺任務
  • t.join() 特定線程在另一線程上調用t.join(),特定線程被掛起,直到另一線程執行結束才恢復執行-慎用(建議改為使用CyclicBarrier)!
  • t.interrupt(): 中斷join()方法的調用
  • t.isAlive() 線程是否結束
  • t.isInterrupted() 線程是否已中斷

21.2.3 使用Executor

  • Executor(執行器):Java SE5的 java.util.concurrent包中定義的執行器,用作管理 Thread對象,從而簡化併發編程。並在客戶端和任務執行之間提供了間接層;
  • Executor:允許你管理異步任務的執行,而無須顯示的管理線程的生命週期;非常常見的場景:單個Executor被用來創建&管理系統中的所有任務。

21.2.3.1 newCachedThreadPool

  • 概念:可緩存的線程池。沒有固定大小,如果線程池中的線程數量超過任務執行的數量,會回收60秒不執行的任務的空閒線程。當任務數量增加時,線程池自己會增加線程來執行任務。而能創建多少,就得看jvm能夠創建多少。
  • 應用場景:default:合理的Executor的首選。只有當這種方式會引發問題時,你才需要切換到FixedThreadPool。
  • 實現方式:
<code>ExecutorService exec = Exectors.newCachedThreadPool();for(int i = 0; i<5, i++){    exec.excute(runnble);}exec.shutdown();/<code>

21.2.3.2 newFixedThreadPool

  • 概念:可以一次性預先執行代價高昂的線程分配,因而也就可以限制線程的數量了。這可以節省時間,因為你不用為每個任務都固定地付出創建線程的開銷。
  • 應用場景:這個實例會複用 固定數量的線程 處理一個 共享的無邊界隊列 。任何時間點,最多有 nThreads 個線程會處於活動狀態執行任務。如果當所有線程都是活動時,有多的任務被提交過來,那麼它會一致在隊列中等待直到有線程可用。如果任何線程在執行過程中因為錯誤而中止,新的線程會替代它的位置來執行後續的任務。所有線程都會一致存於線程池中,直到顯式的執行 ExecutorService.shutdown() 關閉。
  • 實現方式:ExecutorService exec = Exectors.newFixedThreadPool(5);

21.2.3.3 SingleThreadExecutor

  • 概念:單例線程池。就像是線程數量為1的FixedThreadPool。(它還提供了一種重要的併發保證,其他線程不會(即沒有兩個線程會)被調用。
  • 每個任務都是按照它們被提交的順序,並且是在下一個任務開始之前完成的。因此,SingleThreadExecutor會序列化所有提交給它的任務,並會維護它自己(隱藏)的懸掛任務隊列。
  • 應用場景:獨佔資源:多個線程都需要使用共用的文件資源時,使用此機制可保證:任何時刻在任何線程中只有一個任務在運行。
  • 實現方式:ExecutorService exec = Exectors.newSingleThreadPool();

21.2.3.4 newScheduledThreadPool

  • 概念:也是固定線程數量大小的線程池,可以延遲或者定時週期執行任務


21.2.4 從任務中產生返回值

  • Runnable
    :是執行工作的獨立任務,但是它不返回任務值。
  • Callable:如果你希望任務在完成時能夠返回一個值,那麼可以實現Callable接口而不是Runnable接口。
  • Callable:在Java SE5中引入的Callable是一種具有類型參數的泛型,它的類型參數表示的是從方法call()(而不是run())中返回的值,並且必須使用ExecutorService.submit()方法調用它。
<code>public class TaskWithResult implements Callable<string> { //泛型類型:<string>    private int id;    public TaskWithResult(int id) {        this.id = id;    }    @Override    public String call() throws Exception {  // Callable.call(),類似於Runnable.run()        return "TaskWithResult ---> " + id;    }}//創建線程池ExecutorService executorService = Executors.newCachedThreadPool();//Future集合ArrayList<future>> list = new ArrayList<>();for (int i = 0; i < 5;i++){Future<string> future = executorService.submit(new TaskWithResult(i)); //必須使用submit()list.add(future); //通過Future作為獲取任務返回值}//遍歷for (Future<string> future : list){try {System.out.println(future.get()); //future.get():阻塞方式獲取返回值} catch (InterruptedException | ExecutionException e) {e.printStackTrace();} finally {//記得關閉線程池executorService.shutdown();}}/<string>/<string>/<future>/<string>/<string>/<code>

21.2.5 休眠

  • 休眠:.sleep() 將使任務中止執行(線程阻塞)指定的時間。
  • 應用場景:阻塞當前線程,用於等待I/O或其他耗時線程完成操作。
<code>Thread.sleep(100) //Java SE5 new style : TimeUnit.MILLISECOND.sleep(100);/<code>

21.2.6 優先級

  • 優先級:.setPriority(priority) :將當前線程的優先級(重要性)傳遞給調度器。
  • 說明:絕大多數場景建議:線程按照默認的優先級執行,試圖操控線程優先級通常是錯誤的。

21.2.7 讓步--慎用!

  • 讓步:.yield() : 暗示線程調度器:本任務已完成當前工作,可以讓其他線程使用CPU了。
  • 說明:yield()讓步機制經常被誤用,此機制不保證一定會被線程調度器採用。--慎用!

21.2.8 後臺線程

  • 後臺線程:(damon線程): 指在程序運行的時候在後臺提供一種通用服務的線程,並且此線程並不屬於程序不可或缺的部分。
  • 區別1:當所有非後臺線程結束時,程序已結束,同時會殺死進程中所有的後臺進程。(有非後臺線程運行,則進程不會結束)。
  • 區別2:後臺線程的try-finally{}塊部分在線程結束時不一定會被執行
  • 設置方法:必須在.start()前setDaemon(),才能設置進程為後臺進程。

21.2.9 編碼(任務+線程)的變體  

  • Thread
  • Thread+Runnable
  • Thread+Callable+Future

21.2.10 常用術語

  • 任務:Java中,任務(Runnable&Callable ≠ 線程Thread),
  • Thread(線程):Java中,Thread本身並不執行任何操作,它只是驅動賦予它的任務。Executor執行器將負責處理線程的創建和管理。
  • Java的線程機制:基於來自C的低級實現方式,開發者需要深入研究&並完全理解其所有實現細節。

21.2.11 加入一個線程(建議改為使用CyclicBarrier)

  • t.join (para): 特定線程可以在另一線程中調用t.join():特定線程被掛起,直到另一線程執行結束才恢復執行-慎用!
  • 方法參數說明:para 超時參數,如果目標線程在這段時間到期仍未結束的話,join()方法總能返回。
  • t.interrupt(): 中斷join()方法的調用


21.2.12 創建有響應的用戶界面

<code>


21.2.13 線程組(應skip此概念,已廢棄)

  • 線程組:持有一個線程集合。線程組的價值可以引用Joshua Bloch的話來總結:“最好把線程組看成是一次不成功的嘗試,你只要忽略它就好了。
  • 不建議繼續研究的原因:如果你花費了大量的時間和精力試圖發現線程組的價值(就像我一樣),那麼你可能會驚異,為什麼沒有來自Sun的關於這個主題的官方聲明,多年以來,相同的問題對於Java發生的其他變化也詢問過無數遍。諾貝爾經濟學將得主Joseph Stiglitz的生活哲學可以用來解釋這個問題,它被稱為承諾升級理論(The Theory of Escalating Commitment):“繼續錯誤的代價由別人來承擔,而承認錯誤的代價由自己承擔。”

21.2.14 捕獲異常

  • 由於線程的本質特性,使得你不能捕獲從線程中逃逸的異常。一旦異常逃出任務的run()方法,它就會向外傳播到控制檯,除非你採取特殊的步驟捕獲這種錯誤的異常。
  • 解決方案:Java SE5新增的Executor解決此問題。(可同時忽略線程組和捕獲異常的相關問題)


21.3 共享受限資源(併發領域問題)

  • 順序編程無共享資源訪問衝突問題:可以把單線程程序當作在問題域求解的單一實體,每次只能做一件事情。
  • 共享受限資源:有了併發後,就可以同時做多件事情。即會存在多個線程 同時訪問 共享數據&資源的問題。

21.3.1 不正確地訪問資源

  • boolean類型的賦值和返回值操作是原子性的
    :即諸如賦值和返回值這樣的簡單操作在發生時沒有中斷的可能,因此你不會看到這個域處於在執行這些簡單操作的過程中的中間狀態。
  • 在Java中,遞增不是原子性的操作。因此,如果不保護任務,即使單一的遞增也不是安全的。

21.3.2 解決共享資源競爭

  • 解決共享資源訪問競爭的方案:任務訪問資源時 加鎖。
  • 序列化訪問共享資源:基本上所有的併發模式在解決線程衝突問題的時候,都採用此方案。即給定時間只允許一個任務訪問共享資源(實現方式:比如方法前添加synchronized)。
  • 互斥量(mutex):因為方法|代碼塊 添加了鎖,產生的互相排斥的效果。此機制稱為互斥量。
  • synchronized(鎖):Java以此關鍵字,為防止資源衝突提供了內置支持。
  • 對象鎖
    :所有的對象都自動含有一個單一的鎖(也稱監視器)。當在對象上調用任意synchronized方法,此對象都被加鎖,此時該對象上其他synchronized方法只有等到前一個方法執行調用完畢並釋放了鎖以後才能被調用。
  • Class類鎖:針對每個類,也有一個鎖(作為類的Class對象的一部分),所以synchronized static 方法也可以在類的範圍防止對static數據的併發訪問。
  • 同步規則:Brian的同步規則:“如果你正在寫一個變量,它可能接下來將被另一個線程讀取;或者正在讀取一個上一次已經被另一個線程寫過的變量,那麼你必須使用同步,並且讀寫線程都必須使用相同的監視器鎖同步。”
  • 臨界共享數據的訪問:每個訪問臨界共享數據的方法,都必須被同步,否則它們都不能正常工作!

21.3.2.1 使用顯式的Lock對象

  • 顯式Lock對象:Java SE5的concurrent類庫包含有定義在java.util.concurrent.locks中的顯式的互斥機制。Lock對象必須被顯式的創建,鎖定和釋放(與內置鎖機制相比,代碼缺乏優雅性)。
  • 顯式鎖選擇原則:優先選擇內置鎖,只有解決特殊問題才改用顯式鎖。
  • ReentrantLock:顯式重入鎖:允許嘗試著獲取但最終未獲取鎖,如其他人已經獲得此鎖,那你就可以離開一段時間執行其他事情,不用一直等直至此鎖被釋放。


21.3.3 原子性&可見性&易變性

  • 原子性:是不能被線程調度機制中斷的操作。一旦操作開始,它一定可以在可能發生的“上下文切換”前(切換到其他線程)執行完畢。
  • 基本數據類型的原子性:原子性可以應用於除了long&double之外的所有基本類型之上的“簡單操作”(簡單的賦值和返回操作,java遞增操作不是原子性的)。
  • long&double變量的原子性:可以結合volatile 保證long&double變量“簡單操作”的原子性(???如何理解)。
  • 可見性:即對變量進行寫操作,那麼所有讀取此變量的線程都能立即讀取到此修改。volatile 關鍵字可以保證變量的可見性。

21.3.4 原子類

  • 原子類:Java SE5新增了AtomicInteger/AtomicLong/AtomicReference等特殊的原子性變量類(封裝了原子操作方法)。

21.3.5 臨界區

  • 臨界區(critical section):也稱同步代碼塊,使用synchronized{}包含的代碼塊。
  • 當只希望防止多個線程同時訪問方法內部的部分代碼塊(而不是整個方法)時,通過這個方式分離出來的代碼塊稱為“臨界區”。

21.3.6 在其他對象上同步

  • 本對象的同步塊:必須給定一個在其上進行同步的對象。最合理的方式:使用方法正在被調用的當前對象:
    synchronized(this){}
  • 其他對象上的同步塊:有時必須在另外一個對象上同步,此場景下,必須保證所有相關的任務都在同一個對象上同步。synchronized(otherObject){}


21.3.7 線程本地存儲(ThreadLocal類)

  • 防止共享資源衝突的方法二:根除對變量的共享
  • 線程本地存儲:是一種自動化機制,可以為相同變量的每個線程都創建不同的存儲;
  • ThreadLocal類:實現創建&管理線程的本地存儲。ThreadLocal<integer> value;/<integer>


21.4 終結任務

21.4.1 裝飾性花園

21.4.2 在阻塞時終結

線程狀態(四種狀態):

  • 新增(new):當線程被創建時,只短暫的處於此狀態;
  • 就緒(Runnable):在此狀態,只要調度器將時間片分配給線程,線程就會運行。
  • 阻塞(Blocked):線程能夠運行,但是某個條件阻止它的運行。當線程處於阻塞狀態時,調度器會忽略此線程,不會分配線程任何 CPU時間。直到線程重新進入就緒狀態,它才有可能執行操作。
  • 死亡(Dead):處於死亡或終止狀態的線程將不再是可調度的,並且再也不會得到CPU時間,它的任務已結束,或不再是可運行的。任務死亡的通常方式是run()方法返回。

《JAVA編程思想》5分鐘速成:第21章(併發)

​​21.4.3 中斷

  • 方法1:Thread.interrupt():可以終止被阻塞的任務;此方法將設置線程的中斷狀態。
  • 方法2:Executor.shutdownNow(),它將發送一個interrupt()調用給它啟動的所有線程。
  • 方法3:Future.cancel():是一種中斷由Executor啟動的單個線程的方式。

Executor 通過調用submit()而不是excutor()來啟動任務,就可以持有該任務的上下文。submit()將返回一個泛型的Future>,持有這種Future的關鍵在於你可以在其上調用cancel(),並因此可以使用它來中斷某個特定任務。


21.5 線程之間的協作

  • 線程協作:當使用線程來同時運行多個任務時,可以通過使用鎖(同步)來同步不同任務的行為,從而使得一個任務不會干涉另一個任務的資源。即不同任務交替訪問某個共享資源(通常是內存),可以使用互斥使得任何時刻只有一個任務可以訪問這項資源。
  • 任務間握手:任務協作的關鍵問題,可以通過Object的wait()¬ify()¬ifyAll(),或者Condition對象的await()&signal()方法來實現。

21.5.1 wait()與notifyAll()

  • wait():使你可以等待某個條件發生變化,而改變這個條件超出了當前方法的控制能力。通常,這種條件將由另一個任務來改變。--可避免採用忙等待的方式。
  • 忙等待(空循環):不斷地進行空循環,這被稱為忙等待, 通常是一種不良的週期使用方式。
  • notify()或notifyAll() :發生時,即表示發生了某些感興趣的事物,這個任務才會被喚醒並去檢查所產生的變化。因此,wait()提供了一種在任務之間對活動同步的方式。

其中,sleep()&wait()的區別:

  • sleep():調用時,鎖沒有被釋放,調用yield()也屬於此情況;
  • wait():將釋放鎖(並且線程被掛起),意味著另外一個任務可以獲得這個鎖;並且再通過notify()或notifyAll()或者時間到期,則會從wait()中恢復執行。
  • wait(), notify()以及notifyAll()有一個比較特殊的方面:那就是這些方法是基類Object的一個部分,而不是屬於Thread的一部分。

錯失的信號:

  • 當兩個線程使用notify()+wait() 或者notifyAll()+wait()進行協作時,可能會錯過某個信號。

21.5.2 notify() 與 notifyAll()

  在有關Java的線程機制的討論中,有一個令人困惑的描述: notifyAll()將喚醒“所有下在等等的任務”。這是否意味著在程序中任何地方,任何處於wait()狀態中的任務都將被任何對notifyAll()的調用喚醒呢?有示例說明情況並非如此——事實上,當notifyAll()因某個特定鎖而被調用時,只有等待這個鎖的任務才會被喚醒。


21.5.3 生成者與消費者

使用顯式的Lock和Condition對象

  • 顯式Lock:Lock lock = new ReentrantLock();
  • Condition對象:使用互斥並允許任務掛起的基本類。
  • Condition.await():掛起任務(類似於Object.wait())
  • Condition.signal()&signalAll():外部條件變化時,通知這個&所有任務,從而喚醒對應任務(類似於Object.notify() ¬ifyAll())

21.5.4 生成者與消費者的實現

  • 方法1:使用wait()和notifyAll()
  • 方法2:使用高級容器(同步隊列):LinkedBlockingQueue 或者ArrayBlockingQueue

21.5.5 任務間使用管道進行輸入&輸出(已廢棄!)

  • 管道:基本是一個阻塞隊列,存在於引入BlockingQueue之前的Java版本。
  • PipedWriter:(允許任務向管道寫)
  • PipedReader:(允許不同任務從同一個管道讀取):


21.6 死鎖

  • 死鎖:鎖場景下,任務之間互相等待的連續循環,沒有哪個線程能繼續,稱為“死鎖”。

由Edsger Dijkstrar提出的哲學家就餐問題是一個經典的死鎖例證。要修正死鎖問題,你必須明白,當以下四個條件同時滿足時,就會發生死鎖:

  • 1. 互斥條件。任務使用的資源中至少有一個是不能共享的。這裡,一根Chopstick一次就只能被一個Philosopher使用。
  • 2. 至少有一個任務它必須持有一個資源且正在等待獲取一個當前被別的任務持有的資源。也就是說,要發生死鎖,Philosopher必須拿著一根Chopstick並且等待另一根。
  • 3. 資源不能被任務搶佔,任務必須把資源釋放當作普通事件。Philosopher很有禮貌,他們不會從其他Philosopher那裡搶佔Chopstick。
  • 4. 必須有循環等待,這時,一個任務等待其他任務所持有的資源,後者又在等待另一個任務所持有的漿,這樣一直下去,直到有一個任務在等待第一個任務所持有的資源,使得大家都被鎖住。在DeadlockingDiningPhilosophers.java中,因為每個Philosopher都試圖先得到右邊的Chopstick,然後得到左邊的Chopstick,所以發徨了循環等待。

備註:所以要防止死鎖的話,只需破壞其中一個即可。防止死鎖最容易的方法是破壞第4個條件。


21.7 新類庫中的構件

21.7.1 CountDownLatch(減法計數門閂

  • 適用場景:它被用來同步一個或多個任務,強制它們等待由其他任務執行的一組操作完成。即一個或多個任務需要等待,等待到其它任務,比如一個問題的初始部分,完成為止。
  • 你可以向CountDownLatch對象設置一個初始值,任何在這個對象上調用wait()的方法都將阻塞,直到這個計數值到達0.其他因結束其工作時,可以在訪對象上調用countDown()來減小這個計數值。CountDownLatch被設計為只解發一次,計數值不能被重置。如果你需要能夠重置計數值的版本,則可以使用CyclicBarrier。
  • 調用countDown()的任務在產生這個調用時並沒有被阻塞,只有對await()的調用會被阻塞,直至計數值到達0。
  • CountDownLatch的典型用法是將一個程序分為n個互相獨立的可解決任務,並創建值為n的CountDownLatch。當每個任務完成時,都會在這個鎖存器上調用countDown()。等待問題被解決的任務在這個鎖存器上調用await(),將它們自己掛起,直至鎖存器計數結束。

21.7.2 CyclicBarrier(柵欄

  • 適用場景:你希望創建一組任務,它們並行地執行工作,然後在進行下一下步驟之前等待,直至所有任務都完成(看起來有些像Join())。它使得所有的並行任務都將在柵欄處列隊,因此可以一致地向前移動。
  • 例如程序賽馬程序:HorseRace.java

備註:CountDownLatch&CyclicBarrier的區別:

  • CountDownLatch的下一步的動作實施者是主線程,具有不可重複性;
  • CyclicBarrier的下一步動作實施者還是“其他線程”本身,具有往復多次實施動作的特點。

21.7.3 DelayQueue

  • DelayQueue:是一個無界的BlockingQueue(同步隊列),用於放置實現了Delayed接口的對象,其中的對象只能在其到期時才能從隊列中取走。
  • 這種隊列是有序的,即隊頭對象是最先到期的對象。如果沒有到期的對象,那麼隊列就沒有頭元素,所以poll()將返回null(也正因為此,我們不能將null放置到這種隊列中)。
  • 如上所述,DelayQueue就成為了優先級隊列的一種變體。

21.7.4 PriorityBlockingQueue

  • PriorityBlockingQueue:這是一個很基礎的優先級隊列,它具有可阻塞的讀取操作。
  • 這種隊列的阻塞特性提供了所有必需的同步,所以你應該注意到了,這裡不需要任何顯式的同步。--不必考慮當你從這種隊列中讀取時,其中是否有元素,因為這個隊列在沒有元素時,將直接阻塞讀取者。

21.7.5 使用ScheduledExecutor的室溫控制器

“溫室控制系統”可以被看作是一種併發問題,每個期望的溫室事件都是一個預定時間運行的任務。

ScheduledThreadPoolExecutor可以解決這種問題:

  • schedule() 用來運行一次任務
  • scheduleAtFixedRate() 每隔規定的時間重複執行任務。
  • 兩個方法接收delayTime參數。可以將Runnable對象設置為在將來的某個時刻執行。

21.7.6 Semaphre(計數信號量)

  • 正常的鎖:(來自concurrent.locks顯式鎖,或者內建的synchronized鎖):在任何時刻只允許一個任務訪問。
  • Semaphre(計數信號量):允許n個任務同時訪問這個資源。可以看作向外分發使用資源的許可證,儘管實際上並沒有任何許可證對象。

21.7.7 Exchanger

  • Exchanger:在兩個任務之間交換對象的柵欄。
  • 典型場景:一個任務在創建對象,這些對象的生產代價很高昂,而另一個任務在消費這些對象。通過這個方式,可以有更多的對象在被創建的同時被消費。


21.8 仿真

21.8.1 銀行出納員仿真(ArrayBlockingQueue)

21.8.2 飯店仿真(SynchronousQueue)

21.8.3 分發工作(LinkedBlockingQueue)

不同BlockingQueue的區別:

  • ArrayBlockingQueue:是一個有界緩存等待隊列,可以指定緩存隊列的大小,當正在執行的線程數等於corePoolSize時,多餘的元素緩存在ArrayBlockingQueue隊列中等待有空閒的線程時繼續執行,當ArrayBlockingQueue已滿時,加入ArrayBlockingQueue失敗,會開啟新的線程去執行,當線程數已經達到最大的maximumPoolSizes時,再有新的元素嘗試加入ArrayBlockingQueue時會報錯。
  • LinkedBlockingQueue:是一個無界緩存等待隊列。當前執行的線程數量達到corePoolSize的數量時,剩餘的元素會在阻塞隊列裡等待。(所以在使用此阻塞隊列時maximumPoolSizes就相當於無效了),每個線程完全獨立於其他線程。生產者和消費者使用獨立的鎖來控制數據的同步,即在高併發的情況下可以並行操作隊列中的數據。
  • SynchronousQueue:沒有容量,是無緩衝等待隊列,是一個不存儲元素的阻塞隊列,會直接將任務交給消費者,必須等隊列中的添加元素被消費後才能繼續添加新的元素。


21.9 性能調優(Performance Tuning)

21.9.1 比較各類互斥技術(Comparing mutex technologies)

  • “微基準測試(microbenchmarking)”危險:這個術語通常指在隔離的、脫離上下文環境的情況下對某個特性進行性能測試。當然,你仍舊必須編寫測試來驗證諸如“Lock比synchronized更快”這樣的斷言,但是你需要在編寫這些測試的進修意識到,在編譯過程中和在運行時實際會發生什麼。
  • 不同的編譯器和運行時系統在這方面會有所差異,因此很難確切瞭解將會發生什麼,但是我們需要防止編譯器去預測結果的可能性。

使用Lock通常會比使用synchronized要高效許多,而且synchronized的開銷看起來變化範圍太大,而Lock相對比較一致。這是否意味著你永遠都不應該使用synchronized關鍵字呢?這裡有兩個因素需要考慮:

  • 1. 互斥方法的方法體的大小。
  • 2. 代碼可讀性:synchronized關鍵字所產生的代碼與Lock所需的“加鎖-try/finally-解鎖”慣用法所產生的代碼相比,可讀性提高了很多。
  • 總結1:關於代碼可讀性:代碼被閱讀的次數遠多於被編寫的次數。在編程時,與其他人交流相對於與計算機交流而言,要重要得多,因此代碼的可讀性至關重要。
  • 總結2:優先選擇synchronized關鍵字,只有在性能調優時才替換為Lock對象這種做法,是具有實際意義的。

21.9.2 免鎖容器(Lock-free containers)及樂觀鎖  


21.10 總結

  線程的一個額外好處是它們提供了輕量級的執行上下文切換(大約100條指令),而不是重量級的進程上下文切換(要上千條指令)。因為一個給定進程內的所有線程共享相同的內存空間,輕量級的上下文切換隻是改變了程序的執行序列和局部變量。進程切換(重量級的上下文切換)必須改變所有內存空間。


《JAVA編程思想》5分鐘速成:第21章(併發)


分享到:


相關文章: