並行程式設計那點兒事

2023-03-09 21:01:25

目錄

執行緒理論

執行緒和程序的區別

程序

程序是作業系統分配資源的最小單位,每個程序都是一個在執行中的程式,在windows中一個執行的xx.exe就是一個程序,他們都擁有自己獨立的一塊記憶體空間,一個程序可以有多個執行緒

執行緒

執行緒是作業系統排程的最小單元,負責當前程序中程式的執行,一個程序可以執行多個執行緒,多個執行緒之間可以共用資料

  1. 本質區別:程序是作業系統資源分配的最小單元,執行緒是處理器任務排程和執行的最小單位
  2. 資源開銷:每個程序都有獨立的程式碼和資料空間,程序之間的切換會有較大的開銷,執行緒可以看做輕量級的程序

程序間通訊

管道

管道是核心管理的一個緩衝區,相當於記憶體中一個的小紙條,管道的一端連線著一個程序的輸入,一端連線著另一個程序的輸出,當管道中沒有資訊的話,從管道中讀的程序會阻塞,直到另一端的程序放入資訊,當管道中資訊放滿時,嘗試放入資訊的程序會阻塞,直到另一個端程序取出資訊,當兩個程序都結束時,管道也就結束了

特點:單向的,一端輸入一端輸出,採用先進先出FIFO模式,大小4K,滿時寫阻塞,空時讀阻塞

分類:普通管道(僅父子程序間通訊)位於記憶體,命名管道位於檔案系統,沒有情緣關係的管道只要知道管道名也可以通訊

訊息佇列

訊息佇列可以看做是一個訊息連結串列,只要執行緒有足夠的許可權,就可以往訊息佇列裡面存訊息和取訊息,他獨立於傳送程序和接收程序,提供了一種從一個程序向另一個程序傳送資料塊的方法,每一個資料塊都有一個訊息型別(頻道)和訊息內容(節目),每個型別相互不受影響,類似於一個獨立的管道

特點:全雙工可讀可寫,生命週期跟隨核心,每個資料塊都有一個型別,接收者可以有不同的型別值,每個訊息的最大長度是有上限的(MSGMAX),位元組數也是有上限的(MSGMNB),系統上的訊息佇列總數也是有上限的(MSGMNI),

號誌

號誌本質上是一個計數器,它不以傳送資料為目的,主要是用來保護共用資源,使得資源在一個時刻只有一個程序獨享

原理:號誌只有等待和傳送兩種操作,即P(sv)和V(sv)兩個操作都屬於原子操作

P(sv):如果sv的值大於0,就給它減1;如果它的值為0,就掛起該程序的執行

S(sv):如果有其他程序因等待sv而被掛起,就讓它恢復執行,如果沒有程序因sv而掛起,就給他加1

共用記憶體

共用記憶體就是允許兩個程序或多個程序共用一定的儲存區,當一個程序改變了這個記憶體區域中的內容時,其他程序都會覺察到這個更改

特點:資料不需要在使用者端和伺服器端之間來回複製,資料直接寫到記憶體,減少了數次資料拷貝,是很快的一種IPC

缺點:共用記憶體沒有任何的同步和互斥機制,需要使用號誌來實現對共用記憶體的存取和同步

通訊端

通訊端是一種允許兩個不同程序進行通訊的程式設計介面,通過通訊端介面,可以使一臺機器之間的程序可以相互通訊,也可以使不同機器上的程序進行網路通訊,通訊端明確把使用者端和伺服器端分開,實現了多個使用者端連線到一個伺服器端

原理:伺服器端應用程式呼叫socket建立一個通訊端,它是系統分配給伺服器程序的類似檔案描述符的資源,不能與其他程序共用,伺服器程序給這個通訊端起一個名字,本地通訊端的名字是Linux檔案系統中的檔名,一般在/tmp或/usr/tmp中,網路通訊端的名字與使用者端連線的特定網路有關的服務識別符號(埠號),系統呼叫bind給通訊端命名後,伺服器程序就開始等待使用者端連線到命名通訊端,系統呼叫一個listen建立一個佇列,用於存放來自使用者端的進入連線,伺服器用accept來接收使用者端的連線,當有使用者端連線時,伺服器程序會建立一個與原有命名不同的新的通訊端,這個通訊端只用於與這個特定的使用者端進行通訊,原有通訊端繼續處理來自其他使用者端的連線。

通訊端的域:

  1. AF_INET域:internet網路,是Novell NetWare網路協定的實現,也就是我們常說的IPV4
  2. AF_INET6域:internet網路,是Novell NetWare網路協定的實現,我們常說的IPV6
  3. AF_UNIX:unix檔案系統域,底層協定的檔案的輸入和輸出,地址就是檔名
  4. AF_ISO域:基於ISO標準協定的網路
  5. AF_XFS域:基於施樂(Xerox)的網路

​ AF_INET域中的型別

  1. 流通訊端:通過TCP/IP連線實現,提供一個有序,可靠,雙向位元組流的連線,保證了傳送的資料不會丟失,複製,或亂序到達
  2. 資料包通訊端:通過UDP/IP實現,提供一個無序的,不可靠的服務,他不需要建立和維護連線,作為一個單獨的網路訊息被傳輸,可能會丟失,複製或亂序的到達,但開銷很小,適用於單次查詢,不保留連線資訊

訊息佇列和管道的區別

  1. 管道是跟隨程序的,訊息佇列是跟隨核心的,也就是說程序結束後,管道就結束了,但訊息佇列還會存在
  2. 管道是檔案,存取速度慢,訊息佇列是資料結構存放在記憶體,存取速度快
  3. 管道是資料流式存取,訊息佇列是資料塊式存取

執行緒間通訊

共用記憶體

執行緒之間共用程式的公共狀態,通過讀寫這個公共狀態來進行隱式通訊,這會經歷兩個過程

  1. 執行緒A把本地記憶體A更新過的共用變數重新整理到主記憶體中
  2. 執行緒B到主記憶體中讀取執行緒A更新後的共用變數

訊息傳遞

執行緒之間通過傳送訊息來顯示的進行通訊,比如wait()和notify()方法

執行緒的五種狀態和生命週期

執行緒被建立並啟動後,他既不是一啟動就進入執行狀態,也不是一直處於執行狀態,線上程的生命週期中,他要經過新建(new),就緒(Runnable),執行(Running),阻塞(Blocked)和死亡(Dead)5種狀態,在啟動過後,他不可能會一直佔用,所以狀態會在執行和阻塞之間切換

  1. 新建狀態(NEW):當程式使用new關鍵字建立了一個執行緒之後,該執行緒就處於新建狀態,此時僅有JVM分配記憶體,並初始化其成員變數的值
  2. 就緒狀態(EUNNABLE):當執行緒物件呼叫了start()方法之後,該執行緒處於就緒狀態,JAVA虛擬機器器會為其建立方法呼叫棧和程式計數器,等待排程執行
  3. 執行狀態(RUNNING):如果處於就緒狀態的執行緒獲得了CPU,開始執行run方法的執行緒執行體,則該執行緒處於執行狀態
  4. 阻塞狀態(BLOCKED):阻塞狀態是指執行緒因為某種原因放棄了cpu使用權,也就是讓出了cpu timeslice,暫時停止執行,直到執行緒進入可可執行(Runtime)狀態,才有機會獲得cpu timeslice轉到執行running狀態
  5. 死亡狀態(sleep/join):執行緒執行結束最後的狀態

執行緒阻塞的三種情況

  1. 等待阻塞:執行的執行緒執行wait方法,JVM會把該執行緒放入等待佇列中
  2. 同步阻塞:執行的執行緒在獲取物件的同步鎖時,若該同步鎖被別的執行緒佔用,則JVM會把該執行緒放入鎖池中
  3. 其他阻塞:執行的執行緒執行Thread.sleep()或者t.join()方法或者發出了I/O請求時,JVM會把該執行緒變成阻塞狀態,當sleep或者join結束,I/O處理完成時,執行緒就會重新轉入可執行狀態

執行緒結束的三種方式

  1. run()或者call()方法執行完成,執行緒正常結束
  2. 執行緒丟擲一個未捕獲的Exception或Error
  3. 執行緒呼叫stop()方法來結束該執行緒-該方法通常容易導致死鎖,不推薦使用

執行緒的上下文

執行緒的上下文是指某一個時間點暫存器和程式計數器的內容,上下文切換可以認為是核心(作業系統的核心)在CPU上對於程序(包括執行緒)進行切換,上下文切換過程中的資訊是儲存在過程控制塊(PCB,process control block)的,PCB也被稱作為切換楨

上下文的切換過程

  1. 掛起一個程序,將這個程序在CPU中的狀態(上下文)儲存在記憶體中
  2. 在記憶體中檢索下一個程序的上下文,並將其在CPU的暫存器中恢復
  3. 跳轉到程式計數器所指向的位置(即跳轉到程序被中斷時的程式碼行),以恢復該程序在程式中

上下文切換的原因

  1. 當前執行任務的時間片用完之後,系統CPU正常排程下一個任務
  2. 當前執行任務碰到IO阻塞,排程器將此任務掛起,繼續下一個任務
  3. 多個任務搶佔鎖資源,當前任務沒有搶到鎖資源,被排程器掛起,繼續下一個任務
  4. 使用者程式碼掛起當前任務,讓出CPU時間
  5. 硬體中斷

暫存器是CPU內部數量較少但是速度很快的記憶體,

程式計數器是一個專用的暫存器,用於表明指令序列中CPU正在執行的位置,存的值為正在執行的指令的位置或者下一個將要被執行的指令的位置,具體依賴於特定的系統

執行緒排程器

執行緒排程器是一個作業系統服務,它負責為Runnable狀態的執行緒分配CPU時間片,當一個執行緒被建立和啟動後,它的執行便依賴於執行緒排程器,分配cpu時間可與基於執行緒的優先順序或者執行緒等待的時間

執行緒排程型別

搶佔式排程

搶佔式排程指的是每條執行緒執行的時間,執行緒的切換都由系統控制,系統控制指的是在系統某種執行機制下,可能每條執行緒都分同樣的執行時間片,也可能是

協同式排程

協同式排程指某一個執行緒執行完成之後主動通知系統切換到一個執行緒上執行,這種模式就像接力賽一樣,一個接一個,執行緒的執行時間由執行緒本身控制,執行緒切換可以預知,不存在多執行緒同步的問題,缺點是如果一個執行緒編寫有問題,一直在執行中,那麼可能導致整個系統崩潰

排程演演算法

先進先出演演算法(FIFO)

按照任務進入佇列的順序,依次呼叫,執行完一個任務再執行下一個任務,只有當任務結束後才切換到下一個任務

優點:最小的工作切換開銷(沒有在任務執行中發生切換),最大的吞吐量(因為沒工作切換開銷),最樸實的公平性(先來先做)

缺點:平均響應時間高

適用場景:佇列中任務耗時差不多的場景

最短耗時任務優先演演算法(SJF)

按照任務的耗時長短進行排程,優先排程耗時最短的任務,這個演演算法的前提是知道每個任務的耗時時間,需要注意的是耗時最短指的是剩餘執行時間,解決了先進先出演演算法中短耗時任務等待長耗時任務的窘境

優點:平均響應時間低

缺點:耗時長的任務遲遲得不到排程,不公平,容易形成飢餓,頻繁的工作切換,排程的額外開銷大

時間片輪轉演演算法(Round Robin)

給佇列中的每個任務分配一個時間片,當時間片到了之後將此任務放到佇列的尾部,切換到下一個任務執行,解決了SJF中長耗時任務飢餓的問題

優點:每個任務都能夠得到公平的排程,耗時短的任務即使落在耗時長的任務後面,也能夠較快的得到排程執行

缺點:工作切換開銷大,需要多次切換任務上下文,時間片不好設定

適用場景:佇列中耗時差不多的任務

JVM的執行緒排程實現

java使用的執行緒排程是搶佔式排程,java中執行緒會按優先順序分配cpu時間片,優先順序越高越先執行,但是優先順序高的執行緒並不能獨自佔用cpu時間片,只能是得到更多的cpu時間片,反之,優先順序低的執行緒分到的執行時間少,但不會分配不到執行時間

執行緒排程器讓執行緒讓出cpu的情況

  1. 當前執行執行緒主動讓出CPU,JVM暫時放棄CPU操作,例如呼叫yiled()方法
  2. 當前執行執行緒因為某種原因進入阻塞狀態,例如阻塞在I/O上
  3. 當前執行執行緒結束,也就是執行完run方法裡面的任務

守護執行緒和使用者執行緒的區別

任何執行緒都可以設定為守護執行緒(Daemon)和使用者執行緒(User),預設情況下新建的執行緒是使用者執行緒,通過setDaemon(true)可以將執行緒設定為守護執行緒,這個函數必須線上程啟動前進行呼叫,否則會報錯,守護執行緒依賴於使用者執行緒,當用戶執行緒都退出了,守護執行緒也就退出了,典型的守護執行緒就是垃圾回收執行緒

執行緒安全

執行緒安全是某個函數,函數庫在多執行緒的環境中被呼叫,能夠正確的處理多個執行緒之間的共用變數,不會對共用資源產生衝突,不會影響程式的執行結果

執行緒不安全的原因

  • 執行緒切換帶來的原子性問題
  • 快取導致的可見性問題
  • 編譯優化帶來的有序性問題

解決方法

  • 原子性問題:JDK Atomic開頭的原子類,synchronized,LOCK
  • 可見性問題: synchronized,volatile,LOCK
  • 有序性問題:Happens-Before規則

執行緒實踐

建立執行緒的四種方式

  1. 繼承Thread類
  2. 實現Runnable介面
  3. 通過Callable和Future建立執行緒
  4. 通過執行緒池建立

幾種方式的區別

  • 實現Runnable,Callable介面的方式建立多執行緒

優勢:執行緒類只是實現了Runnable介面或者Callable介面,還可以繼承其他類,在這種方式下,多個執行緒可以共用同一個target物件,所以非常適合多個相同執行緒來處理同一份資源的情況,從而可以將CPU,程式碼和資料分開,形成清晰的模型,較好的體現了物件導向的思想

劣勢:程式設計稍微複雜,如果要存取當前執行緒,則必須Thread.currentThread()方法

  • 繼承Thread類的方式建立多執行緒

優勢:編寫簡單,如果需要存取當前執行緒,則無需使用Thread.currentThread()方法,直接使用this即可獲得當前執行緒

劣勢:繼承了Thread類,不能再繼承其他類

  • Runnable和Callable的區別
  1. Callable重寫的方法是call()方法,Runnable重寫的方法是run()方法
  2. Callable的任務執行後可以返回值,Runnable的任務不能返回值
  3. Call方法可以丟擲異常,run方法不能丟擲異常
  4. 執行Callable任務可以拿到一個Future物件,表示非同步任務的結果,提供了檢查任務是否完成的方法,獲取任務的結果,通過Future可以瞭解任務的執行情況,可以取消正在執行的任務

執行緒的基本方法

wait

使執行緒進入waiting狀態,只有等待另外執行緒的通知或者被中斷才會返回,呼叫會釋放物件的鎖,因此wait方法一般用在同步方法或者同步程式碼塊中

sleep

使當前執行緒休眠,與wait方法不同的是sleep不會釋放當前鎖,會使執行緒進入timed-wating狀態

yield

使當前執行緒從執行狀態變為就緒狀態,也就是當前執行緒讓出本次cpu時間片,與其他執行緒一起重新競爭CPU時間片

interrupt

中斷一個執行緒,本質是給這個執行緒發行一個終止通知訊號,影響這個執行緒內部的一箇中斷標識位,這個執行緒本身並不會因此而改變狀態,注意線上程處於timed-wating狀態時呼叫interrupt會丟擲InterruptedException,使執行緒提前結束timed-wating狀態

中斷狀態是執行緒固有的一個標識位,通過isInterrupted來獲取標識位的值,根據值然後呼叫thread.interruptd方法來安全的終止執行緒

join

等待其他執行緒終止,在當前執行緒中呼叫一個執行緒的join()方法,則當前執行緒轉為阻塞狀態,回到另一個執行緒結束,當前執行緒再由阻塞狀態變為就緒狀態,等待cpu分配,適用於主執行緒啟動了子執行緒,需要用到子執行緒返回的結果,也就是主執行緒需要在子執行緒結束後再結束,這個時候就可以使用join方法

notify

執行緒喚醒,喚醒在此物件監視器上等待的單個執行緒,如果所有執行緒都在此物件上等待,則會選擇喚醒其中的一個執行緒,選擇是任意的

其他方法

  1. isAlive():判斷一個執行緒是否存在

  2. activeCount():獲取程式中活躍的執行緒數

  3. enumerate():列舉程式中的執行緒

  4. currentThread():得到當前執行緒

  5. isDaemon():判斷當前執行緒是否為一個守護執行緒

  6. setDaemon():設定一個執行緒為守護執行緒

  7. setName():為一個執行緒設定一個名稱

  8. setPriority():設定一個執行緒的優先順序

  9. getPriority():獲得一個執行緒的優先順序

sleep()和wait()的區別

類不同:sleep方法屬於Thread類中,wait方法屬於Object類

釋放鎖:sleep不會釋放鎖,wait會釋放鎖,sleep不釋放鎖到指定時間就會自動喚醒,wait不會自動喚醒

應用場景不同:wait被用於通訊,sleep被用於暫停執行

run()和start()的區別

start方法被用來啟動新建立的執行緒,內部呼叫了run方法,這和直接呼叫run方法效果不一樣,直接呼叫run方法的時候,只會是在原來的執行緒中呼叫,沒有新的執行緒啟動,start()方法會建立新的執行緒

notifyAll和notify的區別

notify只會喚醒一個執行緒,notiyfAll會喚醒所有執行緒

notify可能會導致死鎖,而notifyAll則不會,notify是對notifyAll的一個優化

interrupted和isinterrupted的區別

interrupted會將中斷狀態清除,而isinterrupted不會,java多執行緒中的中斷機制使用這個內部識別符號來實現,當一箇中斷執行緒呼叫Thread.interrupt()來獲取中斷狀態時,中斷狀態會被清除,並設定為true,而非靜態方法呼叫isinterrupted用來查詢其他執行緒的中斷狀態不會改變中斷狀態的標識,任何丟擲InterruptedException異常的方法都會將中斷狀態清零

實現執行緒同步的方法

執行緒同步指執行緒之間的一種制約關係,一個執行緒的執行依賴另一個執行緒的訊息,當它沒有得到另一個執行緒的訊息時應等待,直到訊息到達時才被喚醒,執行緒的同步方法大體可以分為兩類,使用者模式和核心模式,核心模式指的是利用系統核心物件的單一性來進行同步,使用時需要切換核心態和使用者態,例如事件,號誌,互斥量,使用者模式不需要切換到核心態,例如原子操作(單一的一個全域性變數),臨界區

並行理論

三要素

  • 原子性: 一個或多個操作要麼全部執行,要麼全部不執行
  • 可見性: 一個執行緒對共用變數的修改,另一個執行緒可以能夠立刻看見
  • 有序性: 程式執行的順序按照程式碼的先後順序執行

並行和並行

  • 並行:兩個或多個(事件,任務,程式)在同一時刻發生,在一個處理器上交替執行這個任務,只是交替時間非常短,在宏觀上是達到同時的現象
  • 並行:在同一時刻,有多條指令在多個處理器上同時執行,在宏觀和微觀上都是一起執行的

並行關鍵字

synchronized

synchronized可以把任意一個非NULL的物件當做鎖,屬於獨佔式的悲觀鎖,同時也是可重入鎖

作用

synchronized關鍵字是用來控制執行緒同步的,在多執行緒的環境下,控制synchronized程式碼段不被多個執行緒同時執行

作用範圍

  1. 方法:鎖住的是物件的範例(this)
  2. 靜態方法:鎖住的是Class範例,又因為Class的相關資料儲存在雲資料空間,是全域性共用的,所以靜態方法相當類的一個全域性鎖,會鎖住所有呼叫該類的方法
  3. 某一個物件範例:鎖住的是所有以該物件為鎖的程式碼塊,他有多個佇列,當有多個執行緒一起存取某個物件監視器的時候,物件監視器會將這些執行緒儲存在不同的容器中

核心元件

  1. Wait Set:那些呼叫wait方法被阻塞的執行緒被放置這裡面
  2. Contention List:競爭佇列,所有請求鎖的執行緒首先被放在這個競爭佇列中
  3. Entry List:儲存在Contention List中有資格成為候選資源的執行緒
  4. OnDeck:任意時刻,最多隻有一個執行緒正在競爭鎖資源,該執行緒就存在OnDeck中
  5. Owner:當前已經獲取到鎖資源的執行緒
  6. !Owner:當前釋放鎖的執行緒

實現

  1. JVM每次從佇列的尾部取出一個資料用於鎖競爭候選者(OnDeck),但在並行的情況下,ContentionList會被大量的並行執行緒進行CAS存取,為了降低對尾部元素的競爭,JVM會將一部分執行緒移動到EntryList中作為候選競爭執行緒
  2. Owner執行緒會在unlock時,將ContentionList中的部分執行緒遷移到EntryList中,並指定EntryList中的某個執行緒為OnDeck執行緒(一般是最先進去的那個執行緒)
  3. Owner執行緒並不直接把鎖傳遞給OnDeck執行緒,而是把鎖競爭的權力交給OnDeck,OnDeck需要重新競爭鎖,這樣雖然犧牲了一些公平性,但是能極大的提升系統的吞吐量,在JVM中,也把這種行為稱之為「競爭切換」
  4. OnDeck執行緒獲取到鎖資源會變為Owner執行緒,而沒有獲取到鎖資源的仍然停留在EntryList中,如果OWner執行緒被wait方法阻塞,則轉移到WaitSet佇列中,直到某個時刻通過notify或者notifyAll喚醒,重新進入到EntryList中
  5. 處於ContentionList,EntryList,WaitSet中的執行緒都處於阻塞狀態,該阻塞是有作業系統來完成的(Linux 核心下采用pthread_mutex_lock核心函數實現的)
  6. Synchronized是非公平鎖,Synchrionized線上程進入ContentionList時,等待的執行緒會先嚐試自旋獲取鎖,如果獲取不到就進入ContentionList中,這對於已經進入佇列的執行緒是不公平的,還有一個不公平的就是在自旋獲取鎖的執行緒還可能會直接搶佔OnDeck執行緒的資源
  7. 每個物件都有一個monitor物件,加鎖就在競爭monitor物件,程式碼塊加鎖就在前後分別上monitorenter和monitorexit指令來實現,方法加鎖是通過一個標識位來實現
  8. synchronized是一個重量級鎖,需要呼叫作業系統相關的介面,效能是低效的,給執行緒加鎖消耗的時間比有用操作消耗的時間更多
  9. 在java1.6 ,synchronized進行了很多的優化,有適應自旋,鎖消除,鎖粗化,輕量級鎖及偏向鎖等,效率有了本質上的提高,之後在java1.7和java1.8中均對該關鍵字的實現機制做了優化,引入了偏向鎖和輕量級鎖,都是在物件頭中有標記位,不需要經過作業系統加鎖
  10. 鎖可以從偏向鎖升級到輕量級鎖,再升級到重量級鎖,這種升級過程叫做鎖膨脹
  11. JDK1.6中預設是開啟偏向鎖和輕量級鎖,通過-XX:-UseBiasedLocking來禁用偏向鎖

volatile

volatile相比synchronized更加輕量

volatile修飾的變數具有synchronized的可見性,但是不具備原子性,也就是執行緒能夠自動發現volatile變數的最新值

volatile禁止了指令重排,在執行程式時,為了提升效能,處理器和編譯器常常會對指令進行重排,指令重排雖然不會影響單執行緒的執行結果,但是會破壞多執行緒的執行語意,指令重排有兩個條件,1在單執行緒環境下不能改變程式的執行結果,2存在資料依賴關係的情況不允許指令重排

適用場景:一個變數被多個執行緒共用,執行緒直接給這個變數賦值,例如狀態標記量和單例模式的雙檢鎖

ThreadLocal

ThreadLocal是一個本地執行緒副本變數工具類,主要用於將私有執行緒和該執行緒存放的副本物件做一個對映,各個執行緒之間的變數互不干擾,在高並行的場景下,可以實現無狀態的呼叫,特別適用於各個執行緒依賴不同變數值完成操作的場景,是一種空間換時間的做法,在每個Thread裡面維護了一個以開地址實現的ThreadLocal.ThreadLocalMap,把資料進行隔離,資料不共用,自然也就沒有執行緒安全的問題了

基本方法

  1. get()獲取ThreadLocal在當前執行緒中儲存的變數副本
  2. set():設定當前執行緒中變數的副本值
  3. remove():移除當前執行緒中變數的副本
  4. initialValue:延遲載入方法

應用場景:

  1. 資料庫連線,Session管理

final

特點

  1. 被final修飾的類不能被繼承
  2. 被final修飾的方法不可以被重寫
  3. 被final修飾的變數不可以被改變,如果修飾參照,那麼表示參照不可變,參照指向的內容可變
  4. 被final修飾的方法,JVM會嘗試將其內聯,以提高執行效率
  5. 被final修飾的常數,在編譯階段會存入常數池

Synchronized和volatile的區別

volatile應用在多個執行緒對範例變數更改的場合,重新整理主記憶體共用變數的值從而使得各個執行緒可以獲得最新的值,執行緒讀取變數的值需要從主記憶體中讀取;synchronized是鎖定當前變數,只有當前執行緒可以該變數,其他執行緒被阻塞住,synchronize會建立一個記憶體屏障,記憶體屏障保證了所有CPU操作結果都會直接刷到主記憶體中,從而保證了操作的記憶體可見性

volatile僅能使用在變數級別,synchronized則可以使用在變數,方法,和類級別

volatile不會造成執行緒的阻塞,synchronized會造成執行緒的阻塞

volatile只能保證變數的可見性不能保證原子性,synchronized保證了變數的可見性和原子性

volatile標記的變數不會編譯器優化,可以禁止指令重排,synchronized標記的變數可以被編譯器優化

鎖理論

悲觀鎖

悲觀鎖是一種悲觀思想,總是假設最壞的情況,即認為每次都是執行緒不安全的,每次讀寫都會進行加鎖,synchronized就是悲觀鎖的實現

樂觀鎖

樂觀鎖是一種樂觀思想,每次去拿資料的時候都認為別人不會修改,所以不會上鎖,只是在更新的時候判斷一下在此期間別人有沒有去更新這個資料,可以使用版本號等機制實現,適合於多讀的應用場景,在資料庫中write_condition機制就是樂觀鎖的實現,java中java.util.concurrent.atomic包下面的原子變數類也是使用了樂觀鎖的一種CAS實現方法

死鎖

死鎖的四個條件

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

如何避免死鎖

  • 指定獲得鎖的順序,按照同一順序獲得存取資源,類似於序列執行

公平鎖與非公平鎖

公平鎖加鎖前檢查是否有排隊等待的執行緒,優先排隊等待的執行緒,先來先得

非公平鎖加鎖時不考慮等待問題,直接嘗試獲取鎖,獲取不到則自動到隊尾等待

在相同條件下,非公平鎖的效能比公平鎖高5-10倍,因為公平鎖在多核的情況下需要維護一個佇列,增加了開銷

synchronized是非公平鎖,ReentrantLock預設的lock()方法採用的也是非公平鎖

共用鎖和獨享鎖

java並行包提供的加鎖模式分為獨佔鎖和共用鎖

獨佔鎖模式下,每次只能有一個執行緒能持有鎖,ReentrantLock就是以獨佔方式實現的互斥鎖,獨佔鎖採用悲觀鎖的加鎖策略,避免了讀/讀寫衝突,如果某個唯讀執行緒獲取了鎖,則其他執行緒只能等待,這種情況其實不需要加鎖,因為讀操作並不會影響資料的一致性

共用鎖允許多個執行緒同時獲取鎖,並行存取共用資源,加鎖策略是樂觀鎖

可重入鎖(遞迴鎖)

可重入鎖指的是同一執行緒外層函數獲得鎖之後,內層遞迴函數仍然有獲取該鎖的程式碼,但不受影響,ReentrantLock和Synchronized都是可重入鎖

自旋鎖

自旋鎖是指當一個執行緒在獲取鎖的時候,如果鎖已經被其他執行緒獲取,那麼該執行緒將回圈等待,然後不斷的判斷鎖是否能夠被成功獲取,直到獲取到鎖才會退出迴圈,獲取鎖的執行緒一直處於活躍狀態

缺點:

  1. 如果某個執行緒持有鎖的時間過長,就會導致其他等待獲取鎖的執行緒進入迴圈等待,消耗CPU,使用不當會造成CPU使用率極高
  2. 自旋鎖是不公平的,無法滿足等待時間最長的執行緒優先獲取鎖,不公平的鎖就會存線上程餓死的問題

優點:

  1. 不會是執行緒狀態發生切換,一直處於使用者態,執行緒一直處於avtive的,不會使執行緒進入阻塞狀態,減少了不必要的上下文切換

輕量級鎖

鎖的狀態有四種:無鎖狀態,偏向鎖,輕量級鎖,重量級鎖

輕量級是相對於使用作業系統互斥量來實現的傳統鎖而言,輕量鎖不是用來代替重量級鎖的,他的本意是在沒有多執行緒競爭的前提下,減少傳統重量級鎖使用產生的效能消耗,適用於執行緒交替執行同步塊的情況,如果存在同一時間存取同一鎖的情況,就會導致輕量級鎖升級為重量級鎖

鎖升級

隨著鎖的競爭,鎖可以從偏向鎖升級到輕量級鎖,再升級到重量級鎖,鎖的升級是單向的,只能從低到高,不會出現鎖降級

重量級鎖

Synchronized是通過物件內部的一個叫做監視器鎖(monitor)來實現,但是監視器鎖本質是依賴於底層的作業系統的Mutex Lock來實現的,而作業系統實現執行緒之間的切換這就需要從使用者態轉換到核心態,這個成本非常高,狀態之間的轉換需要相對比較長的時間 ,這也就Synchronized效率慢的原因,這依賴於作業系統Mutex Lock所實現的鎖稱之為重量級鎖,JDK1,6之後為了減少獲得鎖和釋放鎖所帶來的效能消耗,提高效能,引入了輕量級鎖和偏向鎖

偏向鎖

偏向鎖的引入是為了在某個執行緒獲得鎖之後,消除這個執行緒的鎖重入(CAS)的開銷,看起來讓這個執行緒得到了偏向,減少不必要的輕量級鎖的執行路徑,因為輕量級鎖的獲取和釋放依賴多次CAS原子指令,而偏向鎖只需要在置換ThreadID的時候依賴一次CAS原子指令,輕量級鎖是為了線上程交替執行同步塊時提高效能,而偏向鎖則是在只有一個執行緒執行同步塊時進一步提高效能

讀寫鎖

讀寫鎖分為讀鎖和寫鎖,讀鎖是無阻塞的,在沒有寫的情況下使用可以提高程式的效率,讀鎖和讀鎖不互斥,讀鎖和寫鎖互斥,寫鎖和寫鎖互斥

讀鎖適用於多個執行緒同時讀,但不能同時寫

寫鎖適用於只能一個執行緒寫資料,且其他執行緒不能讀取

分段鎖

分段鎖不是一種實際的鎖,而是一種思想,ConcurrenHashMap就是使用的分段鎖

Semaphore

Semaphore是一種基於計數的號誌,可以設定一個閾值,根據這個閾值,多個執行緒競爭獲取許可訊號,做完自己的申請後歸還,超過閾值後,申請許可號誌的執行緒將會阻塞,Semaphore可以用來構建一些物件池,資源池之類的,也可以建立一個計數為1的Semaphore二元號誌,類似互斥鎖的機制

CAS

CAS(Compare And Swap/Set)比較並交換,CAS是一種基於鎖的操作,CAS操作包含三個引數(V,A,B),記憶體位置(V),預期原值(A)和新值(B),如果記憶體地址裡面的值和A的值一樣,那麼就將記憶體裡面的值更新成B

CAS操作採用的是樂觀鎖,,通過無限迴圈來獲取鎖,如果在第一輪迴圈中,A執行緒的值被B執行緒修改了,那麼A執行緒需要自旋,到下次迴圈才有機會執行

缺點:

  1. 會導致ABA問題:執行緒1從記憶體位置V取出A,這個時候執行緒2也從記憶體位置V取出A,將A變成B,然後又變成A,這個時候執行緒1再進行CAS,發現還是A,然後執行緒1CAS操作成功,雖然執行緒1操作成功了,但是這中間經過了執行緒2,破壞了有序性,屬於執行緒不安全,從jdk1.5開始引入了AtomicStampedReference
  2. 迴圈時間長開銷大:資源競爭嚴重的情況,CAS自旋的概率會比較大,會浪費更多的CPU資源,效率低於synchronized
  3. 只能保證一個共用變數的原子操作:多個共用變數建議使用鎖

AQS

AQS的全稱是AbstractQueuedSynchronizer,這個類在java.util.concurrent.locks包下面,是一個用來構建鎖和同步器的框架

核心思想:如果被請求的共用資源空閒,則將當前請求資源的執行緒設定成有效的工作執行緒,並且將共用資源設定為鎖定狀態,如果被請求的共用資源被佔用,那麼就需要一套執行緒阻塞等待以及被喚醒時鎖分配的機制,這個機制AQS是用CLH佇列來實現的,即將暫時獲取不到的鎖加入到佇列中

CLH佇列是一個虛擬的雙向佇列,虛擬的雙向佇列即是一個不存在的佇列範例

基於AQS的實現:ReentrantLock,Semaphore,ReentrantReadWriteLock,SynchronousQueue,FutureTask

AQS使用一個int成員變數來表示同步狀態,通過內建的FIFO佇列來完成獲取資源執行緒的排隊工作,AQS使用CAS對該同步狀態進行原子操作實現對其值的修改

private volatile int state;//共用變數,使用volatile修飾保證執行緒的可見性

//獲取同步狀態的當前值
protected final int getState(){
return state;
}
//設定同步狀態的值
protected final void setState(int newState){
  state = newState
}
//原子操作(CAS操作),將同步狀態值設定為給定值
protected final boolean compareAndSetState(int expect,int update){
 return unsafe.compareAndSwapInt(this,stateOffset,expect,update);
}

AQS定義了兩種對資源的共用方式

  1. Exclusive(獨佔):只有一個執行緒能執行,如ReentrantLock,這裡面又可分為公平鎖和非公平鎖
  2. Share(共用):多個執行緒可同時執行

不同的自定義同步器爭用共用資源的方式也不同,自定義同步器在實現時只需要實現共用資源state的獲取與釋放方式即可,至於具體執行緒等待佇列的維護(如獲取資源失敗時入隊和喚醒出隊等),AQS在頂層已經實現了

AQS使用了模板方法模式,自定義同步器時需要重寫下面幾個AQS提供的模板方法

isHeldExclusively()//該執行緒是否正在獨佔資源,只有用到condition才需要去實現它
tryAcquire(int) //獨佔方式,嘗試獲取資源,成功返回true,失敗返回false
tryRelease(int) //獨佔方式,嘗試釋放資源,成功返回true,失敗返回false
tryAcquireShared(int) //共用方式,嘗試獲取資源,負數表示失敗;0表示成功,但沒有剩餘可用資源;正數表示成功,且有剩餘資源;
tryReleaseShared(int) //共用方式,嘗試釋放資源,成功則返回true,失敗返回false

預設情況下每個方法都丟擲UnsupportedIOperationException.這些方法的實現必須都是內部執行緒安全的。並且通常應該簡短而不是阻塞,AQS類中的其他方法都是final,所以無法被其他類使用,只有這幾個方法可以被其他類使用

一般來說,自定義同步器要麼是獨佔方式,要麼是共用方式,只需要實現tryAcquire-tryRelease或者tryAcquireShared-tryReleaseShared,但是AQS也支援同時實現兩種方式,比如ReentrantReadWriteLock

鎖實踐

synchronized

三種使用方式

  1. 修飾實體方法:作用於當前物件範例加鎖,進入同步程式碼前要獲得當前物件範例的鎖
  2. 修飾靜態方法:也就是給當前類加鎖,會作用於類的所有物件範例,因為靜態成員不屬於任何一個範例物件,是類成員
  3. 修飾程式碼塊:指定加鎖物件,給它加鎖,進入同步程式碼庫前要獲得給定物件的鎖

總結,synchronized關鍵字加到static靜態方法和程式碼塊上都是給Class類上鎖。加到實體方法就是給物件範例上加鎖,儘量不要使用到String型別上,因為JVM中字串常數池具有快取功能

底層實現原理

public class SynchronizedDemo {
    public void method(){
        synchronized (this){
            System.out.println("synchronized---code");
        }
    }
}

使用javap反編譯後 javap -c -v SynchronizedDemo

在執行方法之前之後都有一個monitorenter和monitorexit字元,前面的monitorenter就是獲取鎖,執行完程式碼後釋放鎖,執行monitorexit,第二個monitorexit是為了防止在同步程式碼塊中因異常退出而沒有釋放鎖的情況下,第二遍釋放,避免死鎖的情況

可重入的原理

重入的原理是一個執行緒在獲取到該鎖之後,該執行緒可以繼續獲得鎖,底層原理維護了一個計數器,當執行緒獲得該鎖時,計數器加1,再次獲得該鎖時繼續加1,釋放鎖時,計數器減1,當計數器值為0時,表明該鎖未被任何執行緒所持有,其他執行緒可以競爭獲取鎖

鎖升級的原理

在鎖物件的物件頭裡面有一個threadId欄位,在第一次存取的時候threadId為空,jvm讓其持有偏向鎖,並將threadid設定為執行緒id,再次進入的時候會先判斷threadid是否與其執行緒id一致,如果一致則可以繼續使用該物件,如果不一致則升級為了輕量級鎖,通過自旋迴圈一定次數之後,如果還沒有正常獲取到要使用的物件,此時就會把鎖再升級為重量級鎖,從而降低鎖帶來的效能消耗

Lock

Lock可以說是Synchronized的擴充套件版,Lock提供了無條件的,可輪訓的(tryLock方法),定時的(tryLock(long timeout,TimeUnit unit)),可中斷的(lockInterruptitibly),可多條件的(newCondition方法)鎖操作,另外Lock的實現類基本都支援非公平鎖和公平鎖,synchronized只支援非公平鎖

優勢:

  1. 可以使鎖更公平
  2. 可以使執行緒在等待鎖的時候響應中斷
  3. 可以讓執行緒嘗試獲取鎖,並在無法獲取鎖的時候立即返回或者等待一段時間
  4. 可以在不同範圍,以不同順序獲取和釋放鎖

主要方法

  1. void lock();如果鎖處於空閒狀態,則當前執行緒獲取到鎖,如果鎖已經被其他執行緒持有,則將當前執行緒阻塞,直到獲取到鎖
  2. boolean tryLock();如果鎖處於可用狀態,則當前執行緒獲取鎖並且返回true,如果鎖是不可用狀態,則不會將當前執行緒阻塞
  3. void unlock();釋放當前執行緒所持有的鎖,這個方法只能由持有者釋放,如果當前執行緒不說持有者,也就是當前執行緒不持有鎖,那麼將會丟擲異常
  4. Condition newCondition();條件物件,獲取等待通知元件,該元件和當前鎖繫結,當前執行緒只有獲取到了鎖,才能呼叫該元件的await方法,await呼叫後將釋放鎖
  5. getHoldCound():查詢當前執行緒儲存此鎖的次數,也就是執行lock方法的次數
  6. getQueueLength():返回正在等待的獲取此鎖的執行緒估計數,比如如果是20個執行緒,獲取到的可能是18個
  7. getWaitQueueLength(Condition condition):返回等待與此鎖相關的給定條件的執行緒估計數,比如10個執行緒都用condition物件呼叫了await方法,那麼此方法返回10
  8. hasWaiters(Condition condition):查詢是否有執行緒等待與此鎖相關的給定條件
  9. hasQueueThreads(Thread thread):查詢給定執行緒是否在等待獲取鎖
  10. hasQueuedThreads():是否有執行緒在等待此鎖
  11. isFair():該鎖是否是公平鎖
  12. isHeldByCurrentThread():當前執行緒是否儲存鎖鎖定,執行緒執行lock方法的前後分別是false和true
  13. isLock:此鎖是否有任意執行緒佔用
  14. lockInterruptibly():如果當前執行緒未被中斷,獲取鎖
  15. tryLock():嘗試獲取鎖,僅在呼叫時鎖未被執行緒佔用,獲取鎖
  16. tryLock(long timeout,TimeUnit unit):如果鎖在給定等待時間內沒有被另一個執行緒保持,則獲取該鎖

ReentrantLock

ReentrantLock繼承介面Lock並實現了定義的方法,是一種可重入鎖,除了能完成Synchronized所能完成的所有功能外,還提供了諸如可響應的中斷鎖,可輪詢鎖請求,定時鎖等避免死鎖的方法

使用ReentrantLock必須在finally中進行解鎖操作,避免程式出現異常而無法正常解鎖的情況

Synchronized和Lock的區別

synchronized是悲觀鎖,屬於搶佔式,會引起其他執行緒阻塞

Synchronized和ReentrantLock的區別

兩個都是可重入鎖

Synchronized是和if,else,for一樣的關鍵字,ReentrantLock是類

  1. ReentrantLock使用相比Synchronized更加靈活
  2. ReentrantLock必須手動獲取和釋放鎖,Synchronized是自動,由jvm控制
  3. ReentrantLock只適用於程式碼塊,Synchronized可以適用於類,方法,變數等
  4. ReentrantLock底層是使用Unsafe的park方法加鎖,synchronized操作是物件頭的monitorenter加鎖

ReentrantLock相比synchronized的優勢是可中斷,公平鎖,多個鎖

Condition類和Object類鎖的區別

  1. Condition類的await方法和Object類的await方法等效
  2. Condition類的signal方法和Object類的notify方法等效
  3. Condition類的signalAll方法和Object類的notifyAll方法等效
  4. ReentranLock類可以喚醒指定條件的的執行緒,而Object的喚醒是隨機的

tryLock和Lock和lockInterruptibly的區別

  1. tryLock能獲取鎖就返回true,不能就返回false,tryLock(long timeout,TimeUnit unit)增加時間限制,超過該時間還沒獲取到鎖,則返回false
  2. lock能獲取到鎖就返回true,不能的話就會一直等待獲取鎖
  3. lock和lockInterruptibly,如果兩個執行緒分別執行這兩個方法的時候被中斷,lock不會丟擲異常,lockInterruptibly會丟擲異常

鎖優化

減少鎖持有時間

只用在有執行緒安全要求的程式上加鎖

減小鎖粒度

將大物件(這個物件可能會被很多執行緒存取),拆成小物件,增加並行度,ConcurrentHashMap就是其中的一個實現

降低鎖競爭

使用偏向鎖,輕量級鎖,降低鎖競爭

鎖分離

根據功能將鎖分離開,一般分為讀鎖和寫鎖,做到讀讀不互斥,讀寫互斥,寫寫互斥,保證了執行緒安全,又提高了效能

鎖粗化

正常來說是為了保證執行緒間有效並行,會要求鎖的持有時間儘量短,但是如果時間太短,多個執行緒對一個鎖不停的請求,同步,釋放,這其中對鎖的開銷也會浪費系統資源

鎖消除

對不需要共用的資源中取消加鎖

容器

容器類存放於Java.util包中,主要有3種:Set(集),list(列表,包含Queue)和Map(對映)

  1. Collection:Collection是集合List,Set,Queue的最基本的介面
  2. Iterator:迭代器,可以通過迭代器遍歷集合中的資料
  3. Map:對映表的基礎介面

Set

  1. HashSet:無序,唯一,基於HashMap實現,底層採用HashMap來儲存元素
  2. LinkedHashSet:繼承於HashSet,內部是通過LinkedHashMap來實現
  3. TreeSet:有序,唯一,紅黑樹的資料結構
  4. HashMap:JDK1.8之前由陣列+連結串列,連結串列是為了解決hash衝突而存在,JDK1.8之後由陣列+連結串列+紅黑樹組成,當連結串列長度大於閾值8時,將連結串列轉換為紅黑樹,減少搜尋時間
  5. LinkedHashMap:繼承自HashMap,底層是基於陣列和連結串列或紅黑樹組成,增加了一個雙向連結串列,使資料可以保持鍵值對的插入順序
  6. HashTable:陣列+連結串列組成,陣列是HashMap的主體,連結串列主要是為了解決雜湊衝突而存在
  7. TreeMap:紅黑樹

List

List是有序的Collection,實現有ArrayList,Vector,LinkedList

ArrayList

底層通過陣列實現,允許對元素進行快速隨機存取,缺點是每個元素之間不能有間隔,當容量不夠需要增加容量時,需要將原來的資料複製到新的儲存中間中,當進行插入或者刪除時,需要對陣列進行復制,移動,代價比較高

適合隨機查尋和遍歷,不適合插入和刪除,列印時使用Arrays.toString()輸出每個元素

Vector

底層是通過陣列實現,是執行緒安全的,避免了多執行緒同時寫而引起的不一致性,效能比ArrayList慢

LinkedList

採用雙向連結串列結構儲存資料,很適合資料的動態插入和刪除,隨機存取和遍歷速度比較慢,提供了專門操作表頭和表尾的元素

Set

set有獨一無二的性質,用於儲存無序(存入和取出)元素,值不能重複,資料是否重複的本質是比較物件的HashCode值,所以如果想要讓兩個物件相同,就必須覆蓋Object的hashCode和equals方法,實現有HashSet,TreeSet,LinkHashSet

HashSet

內部採用HashMap實現,不允許重複的Key,只允許一儲存一個null物件,判斷key是否重複通過HashCode值來確定

TreeSet

使用二元樹的結構儲存資料,二元樹是有序的,排序時需要實現Comparable介面,重寫comoare函數,排序時該函數返回負整數,零,正整數分別對應小於,等於,大於

LinkhashSet

繼承於HashSet,實現了LinkedHashSet,底層採用LinkedHashMap來儲存元素

Map

HashMap

不是一個執行緒安全的容器,預設容量是16,根據鍵的hashCode儲存資料,大多數情況可以根據hash函數一次性獲取到資料,因此具有很快的查詢速度,鍵只允許一個null,值可以有多個null

JDK1.7採用陣列+連結串列的方式儲存,每次擴容都是2^n,擴容後是原來的兩倍,負載因子是0.75,擴容的閾值是當前陣列容量*負載因子

JDK1.8採用陣列+連結串列+紅黑樹方式儲存,相比JDK1.7多了一個紅黑樹,當連結串列的元素超過了8個以後會將連結串列轉換成紅黑樹

ConcurrentHashMap

ConcurrentHashMap是一個執行緒安全的容器,底層是一個Segment陣列,預設長度是16,所以並行數是16,通過繼承ReentrantLock進行加鎖,每次加鎖的時候只鎖住陣列中的一個Segment,也就是分段鎖的思想,只要保證了操作的Segment執行緒安全,也就實現了全域性的執行緒安全

HashTable

HashTable功能和HashMap相識,不同的是他屬於執行緒安全的,繼承自Dictionary,在效能上不如concurrentHashMap,使用場景較少,因為執行緒安全的時候使用concurrentHashMap,不需要保證執行緒安全的時候使用HashMap

TreeMap

TreeMap實現了SortedMap介面,底層資料結構是紅黑樹,儲存的時候根據鍵值升序排序,適用於在遍歷的時候需要得到的記錄是排序後的,注意在使用的時候,key必須實現Comparable介面,否則就會丟擲執行時異常java.lang.ClassCastException

LinkedHashMap

LinkedHashMap是HashMap的一個子類,儲存了記錄的插入順序,在用iterator遍歷的時候,得到的記錄肯定是先插入的,可以在構造時帶引數,控制存取次序排序

使用場景

  1. 不需要執行緒安全,對效能要求高使用HashMap
  2. 需要執行緒安全,使用ConcurrentHashMap
  3. 需要排序,使用treeMap或者LinkedHashMap

Queue

常用佇列

  1. ArrayBlockingQueue:基於陣列的並行有界阻塞佇列
  2. ArrayDeque:陣列雙端佇列
  3. ConcurrentLinkedQueue:基於連結串列的並行佇列
  4. DelayQueue:使用優先順序排序的延期無界阻塞佇列
  5. LinkedBolckingQueue:基於連結串列的FIFO有界阻塞佇列
  6. LinkedBolckingDeque:基於連結串列的FIFO雙端阻塞佇列
  7. LinkedtransferQueue:由連結串列組成的無界阻塞佇列
  8. PriorityQueue:優先順序佇列
  9. PriorityBlockingQueue:帶優先順序排序的無界阻塞佇列
  10. SynchronousQueue:並行的同步阻塞佇列,不儲存元素

主要方法

  1. add():新增元素到佇列裡,成功返回true,如果容量滿了會丟擲IllegalStateException
  2. addFirst():插入元素到佇列頭部,失敗丟擲異常
  3. addLast():插入元素到佇列尾部,失敗丟擲異常
  4. offer():新增元素到佇列裡,成功返回true,失敗返回false或丟擲異常
  5. offerFirst():新增元素到佇列頭部,成功返回true,失敗返回false或者丟擲異常
  6. offerLast():新增元素到佇列尾部,成功返回true,失敗返回false或者丟擲異常
  7. remove():取出並移除頭部元素,成功返回true,失敗返回false,空佇列丟擲異常
  8. removeFirst():取出並移除頭部元素,空佇列丟擲異常
  9. removeLast():取出並移除尾部元素,空佇列丟擲異常
  10. poll():取出並刪除佇列頭部元素,如果佇列為空返回null
  11. pollFirst():取出並移除頭部元素,如果佇列 為空返回null
  12. pollLast():取出並刪除尾部元素,如果佇列為空返回null
  13. getFirst():取出但不刪除頭部元素,如果佇列為空丟擲異常
  14. getLast():取出但不刪除尾部元素,如果佇列為空丟擲異常
  15. peek():取出但不移除頭部元素,空佇列返回null
  16. peekFirst():取出但不刪除頭部元素,空佇列返回null
  17. peekLast():取出但不刪除尾部元素,空佇列返回null
  18. put():新增元素到佇列裡,成功返回true,如果容量滿了會阻塞直到容量不滿
  19. take():刪除佇列頭部元素,如果佇列為空,一直阻塞到佇列有元素並刪除
  20. removeFirstOccurrence(Object o):刪除佇列中第一次出現的指定元素,如果不存在則佇列不變,刪除成功返回true
  21. removeLastOccurrence(Object o):刪除佇列中最後一次出現的指定元素,如果不存在則佇列不變,刪除成功返回true

集合和陣列的區別

  1. 陣列是固定長度,集合是可變長度
  2. 陣列可以儲存基本資料型別和參照資料型別;集合只能儲存參照資料型別
  3. 資料儲存的元素必須是同一個型別,集合儲存的資料可以是不同資料型別

執行緒池理論

原理

利用池化思想,將執行緒管理起來,使用的時候不需要再建立和銷燬,即用即拿提高了效率,減少了執行緒的開銷,方便了管理

優點

  1. 降低執行緒的建立和銷燬開銷:通過重複利用已建立的執行緒來減低開銷
  2. 提高響應速度:需要使用執行緒的時候,直接從池裡面取,不需要等執行緒建立,相當於提前初始化了
  3. 提高執行緒的可管理性:因為多個執行緒在一個池裡面,所以可以進行統一分配管理,方便調優和監控

核心引數

  1. corePoolSize:執行緒池核心執行緒數量
  2. maximumPoolSize:執行緒池最大執行緒數量
  3. keepAliverTime:當活躍執行緒數大於核心執行緒數時,多餘執行緒的最大存活時間
  4. TimeUnit:存活的時間單位
  5. workQueue:儲存執行緒池的任務佇列,用來構建執行緒池裡面的worker執行緒
  6. threadFactory:建立執行緒的工廠,可以用來設定執行緒名,是否為守護執行緒等
  7. handler:超出執行緒數範圍或佇列容量的拒絕策略

常用WorkQueue的佇列

  1. ArrayBlockingQueue:基於陣列的有界阻塞佇列,按照FIFO先進先出的原則對元素進行排序
  2. LinkedBlockingQueue:基於連結串列的阻塞佇列,按照FIFO先進先出的原則排序元素,吞吐量一般高於ArrayBlockingQueue,使用這個佇列時maximiumPoolSize相當於無效
  3. SynchronousQueue:一個不快取任務的阻塞佇列,也就是說新任務進來的時候會被直接呼叫,如果沒有可用執行緒則會建立新執行緒,直到最大數
  4. PriorityBlockingQueue:一個具有優先順序的無界阻塞佇列,基於二元堆積實現,優先順序通過Comparator引數實現
  5. DelayQueue:無界的阻塞佇列,生產者不會被阻塞,消費者會被阻塞

工作過程

  1. 剛建立時沒有一個執行緒,佇列有任務也不會執行

  2. 當呼叫execute時會根據當前執行緒數做出如下判斷

    1. 當執行緒池中的執行緒數量小於corePoolSize時則建立執行緒,並處理請求

    2. 當執行緒池中的執行緒數量大於等於corePoolSize時,則把請求放入workQueue中,隨著執行緒池中核心執行緒們不斷執行任務,只要執行緒池中有空閒的核心執行緒就從workQueue中獲取任務並執行

    3. 當workQueue已存滿時則新建非核心執行緒入池,並處理請求直到執行緒數量達到MaximumPoolSize(最大執行緒數量)

    4. 當執行緒池中執行緒數大於maximumPoolSize則使用相應的拒絕策略進行拒絕處理

  3. 當一個執行緒完成任務時,從佇列中取出下一個任務繼續執行

  4. 當一個執行緒無事可做,超過一定空閒時間時,執行緒池會做出判斷,如果當前執行緒數大於核心執行緒數,那麼這個執行緒就會被銷燬,所以當所有任務完成後,執行緒池最終會收縮到核心執行緒數的大小

總結:執行任務時檢查執行緒池中的執行緒數量,小於核心數則新建一個執行緒執行任務,大於等於核心數則放入任務佇列,大於核心數且小於最大執行緒數則建立新執行緒,當執行緒數大於核心執行緒數,且空閒時間超過了keepalive時則會銷燬執行緒

拒絕策略

ThreadPoolExecutor.AbortPolicy

執行緒池預設的拒絕策略,當執行緒池中數量達到最大執行緒數時丟擲java.util.concurrent.RejectedExcutionException異常,任務不會被執行

ThreadPoolExecutor.DiscardPolicy

默默丟棄不能執行的新加任務,不會丟擲異常

ThreadPoolExcutor.CallerRunsPolicy

重試新增當前的任務,會自動重複呼叫execute()

ThreadPoolExecutor.DiscardOldestPolicy

拋棄執行緒池中工作佇列頭部的任務,也就是等待得最久的任務,並執行新傳入的任務

執行緒池實踐

建立方式

  1. ThreadPoolExecutor:手動建立執行緒池
  2. Executors:自動建立執行緒池的工具類

自帶的四種執行緒池

  1. newSingleThreadExecutor:只有一個執行緒的執行緒池,線上程死後或異常時會重新啟動一個執行緒來替代原來的執行緒繼續執行下去
  2. newFixedThreadPool:執行緒數固定大小的執行緒池,每次提交任務就建立一個新執行緒,直到執行緒達到執行緒池的最大大小
  3. newCachedThreadPool:緩衝執行緒池,不會對執行緒池的大小做限制,執行緒池大小依賴於JVM能夠建立的執行緒數大小,執行緒空閒時間超過60秒就會被銷燬,適用於執行很多短期非同步任務
  4. newScheduledThreadPool:大小無限的執行緒池,適用於定時和週期性執行任務的場景

自帶執行緒池的注意點

Executors.newFixedThreadPool(10)

底層是通過new ThreadPoolExecutor(10,10,0L,TimeUnit.MILLSECONDS,new LinkedBlockingQueue())建立,初始化一個指定執行緒數的執行緒池,其中核心執行緒數和最大執行緒數相同,使用LinkedBlockingQueue作為阻塞佇列,當執行緒沒有可執行任務時不會釋放執行緒,由於使用LinkedBlockingQueue的特性,這個佇列是無界,若消費不過來,會導致記憶體被任務佇列佔滿,最終OOM

Executors.newCachedThreadPool()

快取執行緒池,底層通過new ThreadPoolExecutor(0,Integer.MAX_VALUE,60L,TimeUnit.SECONDS,new SynchronousQueue())建立緩衝執行緒池,因為執行緒池的最大值是Integer.MAX_VALUE,所以當並行數很大,執行緒池來不及回收時,會導致嚴重的效能問題

Executors.newSingleThreadExecutor()

佇列使用的LinkedBlockingQueue無界佇列,可以無限新增任務,直到記憶體溢位

執行緒池中submit()和execute()的區別

submit可以返回持有計算結果的Future物件,execute返回型別是void

如何合理設定執行緒池的大小

執行緒池大小要看執行什麼型別的任務,一般可分為CPU密集型,IO密集型

CPU密集型應該使用較小的執行緒池,一般為CPU核心數+1

IO密集型兩種方式 1:使用較大的執行緒池,一般為CPU核心數2,2:(執行緒等待時間與執行緒CPU時間之比+1)CPU核心數

並行工具

CountDownLatch

CountDownLatch是基於AQS共用模式的實現,適用於一個或多個執行緒等待某個條件,期間阻塞,直到所有執行緒都符合,然後繼續執行的場景

構造時傳入一個int引數作為計數器,主要方法是countDown和await,每呼叫一次countDown()方法計數器減1,await方法會阻塞,直到計數器為0

CountDownLatch不能夠重用,計數器只能做減法,如果需要重用考慮使用CyclicBarrier或者重新建立CountDownLatch

CyclicBarrier

CyclicBarrier中文叫柵欄,也可以叫同步屏障,讓一組執行緒都到達柵欄之前阻塞,當最後一個執行緒到達柵欄後放行,好比一扇門,預設是關閉狀態,阻塞執行緒的執行,直到所有執行緒都就位時,門才開啟,讓所有執行緒一起通過,最後還可以關閉,繼續下一輪

構造時傳入一個int引數作為需要攔截的執行緒數,每當一個執行緒呼叫await方法就會告訴CyclicBarrier已經有一個執行緒到達柵欄

Semaphore

許可證集合,用於限制可以存取某些資源的執行緒數目,這裡的限制指的是資源的互斥而不是同步,只能保證在同一時刻資源是互斥的,但不是同步的,這點和鎖不一樣

常用方法

  1. acquire(int permits):獲取給定數目的許可,在獲得許可之前將被阻塞
  2. release(int permits):釋放給定數量的許可
  3. availablePermits(),獲取當前剩餘可用的許可數
  4. reducePermits(int reduction):刪除reduction數量的許可
  5. hasQueuedThread:查詢是否有執行緒等待獲取許可
  6. getQueueLength():獲取正在等待的執行緒估計數量,注意是估計值
  7. tryAcquire(int permits,long timeout,TimeUnit unit):嘗試在指定時間內獲取許可