文章很長,建議收藏起來,慢慢讀! 瘋狂創客圈爲小夥伴奉上如下珍貴的學習資源:html
高併發 必讀 的精彩博文 | |
---|---|
nacos 實戰(史上最全) | sentinel (史上最全+入門教程) |
Zookeeper 分佈式鎖 (圖解+秒懂+史上最全) | Webflux(史上最全) |
SpringCloud gateway (史上最全) | TCP/IP(圖解+秒懂+史上最全) |
10分鐘看懂, Java NIO 底層原理 | Feign原理 (圖解) |
更多精彩博文 ..... | 請參見【 瘋狂創客圈 高併發 總目錄 】 |
說明,此文的知識,很是基礎, 後續 架構師尼恩 將使用 視頻的方式,進行講解,視頻的內容具體請關注 瘋狂創客圈的視頻:《從菜鳥到大神 Java高併發核心編程》. 具體的文字或者內容升級,請掃架構師尼恩微信瞭解詳情java
另外,此文的格式,由markdown 經過程序轉成而來,因爲不少表格,沒有來的及調整, 更完善的版本,請參見瘋狂創客《Java面試紅寶書》最新版本。具體狀況,能夠掃架構師尼恩微信瞭解詳情程序員
線程是操做系統可以進行運算調度的最小單位,它被包含在進程之中,是進程中的實際運做單位。面試
程序員能夠經過它進行多處理器編程,你能夠使用多線程對運算密集型任務提速。算法
好比,若是一個線程完成一個任務要100毫秒,那麼用十個線程完成該任務只需10毫秒。sql
一個進程是一個獨立(self contained)的運行環境,它能夠被看做一個程序或者一個應用。而線程是在進程中執行的一個任務。數據庫
線程是進程的子集,一個進程能夠有不少線程,每條線程並行執行不一樣的任務。apache
不一樣的進程使用不一樣的內存空間,而全部的線程共享一片相同的內存空間。編程
每一個線程都擁有單獨的棧內存用來存儲本地數據。設計模式
繼承Thread類,而後重寫run方法.(因爲Java單繼承的特性,這種方式用的比較少)
public class MyThread extends Thread { public MyThread() { } public void run() { for(int i=0;i<10;i++) { System.out.println(Thread.currentThread()+":"+i); } } public static void main(String[] args) { MyThread mThread1=new MyThread(); MyThread mThread2=new MyThread(); MyThread myThread3=new MyThread(); mThread1.start(); mThread2.start(); myThread3.start(); } }
推薦此方式。兩個特色:
a.覆寫Runnable接口實現多線程能夠避免單繼承侷限
b.實現Runnable()能夠更好的體現共享的概念
c.當執行目標類實現Runnable接口,此時執行目標(target)類和Thread是代理模式(子類負責真是業務的操做,thread負責資源調度與線程建立輔助真實業務。
public class MyTarget implements Runnable{ public static int count=20; public void run() { while(count>0) { try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"-當前剩餘票數:"+count--); } } public static void main(String[] args) { MyThread target=new MyTarget(); Thread mThread1=new Thread(target,"線程1"); Thread mThread2=new Thread(target,"線程2"); Thread mThread3=new Thread(target,"線程3"); mThread1.start(); mThread2.start(); myThread3.start(); } }
a.執行目標核心方法叫call()方法
b.有返回值
import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; public class MyTarget implements Callable<String> { private int count = 20; @Override public String call() throws Exception { for (int i = count; i > 0; i--) { // Thread.yield(); System.out.println(Thread.currentThread().getName()+"當前票數:" + i); } return "sale out"; } public static void main(String[] args) throws InterruptedException, ExecutionException { Callable<String> callable =new MyTarget(); FutureTask <String>futureTask=new FutureTask<>(callable); Thread mThread=new Thread(futureTask); Thread mThread2=new Thread(futureTask); Thread mThread3=new Thread(futureTask); // mThread.setName("hhh"); mThread.start(); mThread2.start(); mThread3.start(); System.out.println(futureTask.get()); } }
請參見後面線程池的面試題。
這個問題是上題的後續,你們都知道咱們能夠經過繼承Thread類或者調用Runnable接口來實現線程,問題是,那個方法更好呢?什麼狀況下使用它?
提示:這個問題很容易回答。Java不支持類的多重繼承,但容許你調用多個接口。因此若是你要繼承其餘類,固然是調用Runnable接口好了。
參考答案,有兩點:
a.覆寫Runnable接口實現多線程能夠避免單繼承侷限
b.實現Runnable()能夠更好的體現共享的概念
start()方法被用來啓動新建立的線程,使該被建立的線程狀態變爲可運行狀態。
當你直接調用run()方法的時候,只會是在原來的線程中調用,沒有新的線程啓動。只有調用start()方法纔會啓動新線程。
若是咱們調用了Thread的run()方法,它的行爲就會和普通的方法同樣,直接運行run()方法。爲了在新的線程中執行咱們的代碼,必須使用Thread.start()方法。
Runnable和Callable都表明那些要在不一樣的線程中執行的任務目標target。
Runnable從JDK1.0開始就有了,Callable是在JDK1.5增長的。它們的主要區別是Callable的 call() 方法能夠返回值和拋出異常,而Runnable的run()方法沒有這些功能。
Java內存模型規定和指引Java程序在不一樣的內存架構、CPU和操做系統間有肯定性地行爲。它在多線程的狀況下尤爲重要。Java內存模型對一個線程所作的變更能被其它線程可見提供了保證,它們之間是先行發生關係。這個關係定義了一些規則讓程序員在併發編程時思路更清晰。好比,先行發生關係確保了:
強烈建議你們閱讀《Java高併發核心編程(卷2):多線程、鎖、JMM、JUC、高併發設計模式》,來加深對Java內存模型的理解。
volatile是一個特殊的修飾符,只有成員變量才能使用它。在Java併發程序缺乏同步類的狀況下,多線程對成員變量的操做對其它線程是透明的。volatile變量能夠保證下一個讀取操做會在前一個寫操做以後發生。線程都會直接從內存中讀取該變量而且不緩存它。這就確保了線程讀取到的變量是同內存中是一致的。
若是你的代碼所在的進程中有多個線程在同時運行,而這些線程可能會同時運行這段代碼。若是每次運行結果和單線程運行的結果是同樣的,並且其餘的變量的值也和預期的是同樣的,就是線程安全的。一個線程安全的計數器類的同一個實例對象在被多個線程使用的狀況下也不會出現計算失誤。很顯然你能夠將集合類分紅兩組,線程安全和非線程安全的。Vector 是用同步方法來實現線程安全的, 而和它類似的ArrayList不是線程安全的。
在大多數實際的多線程應用中,兩個或兩個以上的線程須要共享對同一數據的存取。若是i線程存取相同的對象,而且每個線程都調用了一個修改該對象狀態的方法,將會發生什麼呢?能夠想象,線程彼此踩了對方的腳。根據線程訪問數據的次序,可能會產生訛誤的對象。這樣的狀況一般稱爲競爭條件。
Java提供了很豐富的API但沒有爲中止線程提供API。JDK 1.0原本有一些像stop(), suspend() 和 resume()的控制方法,可是因爲潛在的死鎖威脅。所以在後續的JDK版本中他們被棄用了,以後Java API的設計者就沒有提供一個兼容且線程安全的方法來中止一個線程。當run() 或者 call() 方法執行完的時候線程會自動結束,若是要手動結束一個線程,能夠用volatile 布爾變量來退出run()方法的循環或者是取消任務來中斷線程。
若是異常沒有被捕獲該線程將會中止執行。Thread.UncaughtExceptionHandler是用於處理未捕獲異常形成線程忽然中斷狀況的一個內嵌接口。
當一個未捕獲異常將形成線程中斷的時候,JVM會使用Thread.getUncaughtExceptionHandler()來查詢線程的UncaughtExceptionHandler並將線程和異常做爲參數傳遞給handler的uncaughtException()方法進行處理。
在Java併發程序中FutureTask表示一個能夠取消的異步運算。它有啓動和取消運算、查詢運算是否完成和取回運算結果等方法。只有當運算完成的時候結果才能取回,若是運算還沒有完成get方法將會阻塞。一個FutureTask對象能夠對調用了Callable和Runnable的對象進行包裝,因爲FutureTask也是調用了Runnable接口因此它能夠提交給Executor來執行。
interrupted() 和 isInterrupted()的主要區別是前者會將中斷狀態清除然後者不會。Java多線程的中斷機制是用內部標識來實現的,調用Thread.interrupt()來中斷一個線程就會設置中斷標識爲true。當中斷線程調用靜態方法Thread.interrupted()來檢查中斷狀態時,中斷狀態會被清零。而非靜態方法isInterrupted()用來查詢其它線程的中斷狀態且不會改變中斷狀態標識。簡單的說就是任何拋出InterruptedException異常的方法都會將中斷狀態清零。不管如何,一個線程的中斷狀態有有可能被其它線程調用中斷來改變。
同步集合與併發集合都爲多線程和併發提供了合適的線程安全的集合,不過併發集合的可擴展性更高。在Java1.5以前程序員們只有同步集合來用且在多線程併發的時候會致使爭用,阻礙了系統的擴展性。Java5介紹了併發集合像ConcurrentHashMap,不只提供線程安全還用鎖分離和內部分區等現代技術提升了可擴展性。更多內容詳見答案。
爲何把這個問題歸類在多線程和併發面試題裏?由於棧是一塊和線程緊密相關的內存區域。每一個線程都有本身的棧內存,用於存儲本地變量,方法參數和棧調用,一個線程中存儲的變量對其它線程是不可見的。而堆是全部線程共享的一片公用內存區域。對象都在堆裏建立,爲了提高效率線程會從堆中弄一個緩存到本身的棧,若是多個線程使用該變量就可能引起問題,這時volatile 變量就能夠發揮做用了,它要求線程從主存中讀取變量的值。
在現實中你解決的許多線程問題都屬於生產者消費者模型,就是一個線程生產任務供其它線程進行消費,你必須知道怎麼進行線程間通訊來解決這個問題。比較低級的辦法是用wait和notify來解決這個問題,比較讚的辦法是用Semaphore 或者 BlockingQueue來實現生產者消費者模型。
Java多線程中的死鎖
死鎖是指兩個或兩個以上的進程在執行過程當中,因爭奪資源而形成的一種互相等待的現象,若無外力做用,它們都將沒法推動下去。這是一個嚴重的問題,由於死鎖會讓你的程序掛起沒法完成任務,死鎖的發生必須知足如下四個條件:
避免死鎖最簡單的方法就是阻止循環等待條件,將系統中全部的資源設置標誌位、排序,規定全部的進程申請資源必須以必定的順序(升序或降序)作操做來避免死鎖。
這是上題的擴展,活鎖和死鎖相似,不一樣之處在於處於活鎖的線程或進程的狀態是不斷改變的,活鎖能夠認爲是一種特殊的飢餓。一個現實的活鎖例子是兩我的在狹小的走廊碰到,兩我的都試着避讓對方好讓彼此經過,可是由於避讓的方向都同樣致使最後誰都不能經過走廊。簡單的說就是,活鎖和死鎖的主要區別是前者進程的狀態能夠改變可是卻不能繼續執行。
在java.lang.Thread中有一個方法叫holdsLock(),它返回true若是當且僅當當前線程擁有某個具體對象的鎖。
對於不一樣的操做系統,有多種方法來得到Java進程的線程堆棧。當你獲取線程堆棧時,JVM會把全部線程的狀態存到日誌文件或者輸出到控制檯。在Windows你能夠使用Ctrl + Break組合鍵來獲取線程堆棧,Linux下用kill -3命令。你也能夠用jstack這個工具來獲取,它對線程id進行操做,你能夠用jps這個工具找到id。
這個問題很簡單, -Xss參數用來控制線程的堆棧大小。你能夠查看JVM配置列表來了解這個參數的更多信息。
Java在過去很長一段時間只能經過synchronized關鍵字來實現互斥,它有一些缺點。好比你不能擴展鎖以外的方法或者塊邊界,嘗試獲取鎖時不能中途取消等。Java 5 經過Lock接口提供了更復雜的控制來解決這些問題。 ReentrantLock 類實現了 Lock,它擁有與 synchronized 相同的併發性和內存語義且它還具備可擴展性。
在多線程中有多種方法讓線程按特定順序執行,你能夠用線程類的join()方法在一個線程中啓動另外一個線程,另一個線程完成該線程繼續執行。爲了確保三個線程的順序你應該先啓動最後一個(T3調用T2,T2調用T1),這樣T1就會先完成而T3最後完成。
yield方法能夠暫停當前正在執行的線程對象,讓其它有相同優先級的線程執行。它是一個靜態方法並且只保證當前線程放棄CPU佔用而不能保證使其它線程必定能佔用CPU,執行yield()的線程有可能在進入到暫停狀態後立刻又被執行。點擊這裏查看更多yield方法的相關內容。
ConcurrentHashMap把實際map劃分紅若干部分來實現它的可擴展性和線程安全。這種劃分是使用併發度得到的,它是ConcurrentHashMap類構造函數的一個可選參數,默認值爲16,這樣在多線程狀況下就能避免爭用。
Java中的Semaphore是一種新的同步類,它是一個計數信號。從概念上講,從概念上講,信號量維護了一個許可集合。若有必要,在許可可用前會阻塞每個 acquire(),而後再獲取該許可。每一個 release()添加一個許可,從而可能釋放一個正在阻塞的獲取者。可是,不使用實際的許可對象,Semaphore只對可用許可的號碼進行計數,並採起相應的行動。信號量經常用於多線程的代碼中,好比數據庫鏈接池。更多詳細信息請點擊這裏。
這個問題問得很狡猾,許多程序員會認爲該任務會阻塞直到線程池隊列有空位。事實上若是一個任務不能被調度執行那麼ThreadPoolExecutor’s submit()方法將會拋出一個RejectedExecutionException異常。
兩個方法均可以向線程池提交任務,execute()方法的返回類型是void,它定義在Executor接口中, 而submit()方法能夠返回持有計算結果的Future對象,它定義在ExecutorService接口中,它擴展了Executor接口,其它線程池類像ThreadPoolExecutor和ScheduledThreadPoolExecutor都有這些方法。更多詳細信息請點擊這裏。
阻塞式方法是指程序會一直等待該方法完成期間不作其餘事情,ServerSocket的accept()方法就是一直等待客戶端鏈接。這裏的阻塞是指調用結果返回以前,當前線程會被掛起,直到獲得結果以後纔會返回。此外,還有異步和非阻塞式方法在任務完成前就返回。更多詳細信息請點擊這裏。
每個線程都是有優先級的,通常來講,高優先級的線程在運行時會具備優先權,但這依賴於線程調度的實現,這個實現是和操做系統相關的(OS dependent)。咱們能夠定義線程的優先級,可是這並不能保證高優先級的線程會在低優先級的線程前執行。線程優先級是一個int變量(從1-10),1表明最低優先級,10表明最高優先級。
線程調度器是一個操做系統服務,它負責爲Runnable狀態的線程分配CPU時間。一旦咱們建立一個線程並啓動它,它的執行便依賴於線程調度器的實現。時間分片是指將可用的CPU時間分配給可用的Runnable線程的過程。分配CPU時間能夠基於線程優先級或者線程等待的時間。線程調度並不受到Java虛擬機控制,因此由應用程序來控制它是更好的選擇(也就是說不要讓你的程序依賴於線程的優先級)。
上下文切換是存儲和恢復CPU狀態的過程,它使得線程執行可以從中斷點恢復執行。上下文切換是多任務操做系統和多線程環境的基本特徵。
Immutable對象能夠在沒有同步的狀況下共享,下降了對該對象進行併發訪問時的同步化開銷。要建立不可變類,要實現下面幾個步驟:經過構造方法初始化全部成員、對變量不要提供setter方法、將全部的成員聲明爲私有的,這樣就不容許直接訪問這些成員、在getter方法中,不要直接返回對象自己,而是克隆對象,並返回對象的拷貝。
通常而言,讀寫鎖是用來提高併發程序性能的鎖分離技術的成果。Java中的ReadWriteLock是Java 5 中新增的一個接口,一個ReadWriteLock維護一對關聯的鎖,一個用於只讀操做一個用於寫。在沒有寫線程的狀況下一個讀鎖可能會同時被多個讀線程持有。寫鎖是獨佔的,你能夠使用JDK中的ReentrantReadWriteLock來實現這個規則,它最多支持65535個寫鎖和65535個讀鎖。
忙循環就是程序員用循環讓一個線程等待,不像傳統方法wait(), sleep() 或 yield() 它們都放棄了CPU控制,而忙循環不會放棄CPU,它就是在運行一個空循環。這麼作的目的是爲了保留CPU緩存,在多核系統中,一個等待線程醒來的時候可能會在另外一個內核運行,這樣會重建緩存。爲了不重建緩存和減小等待重建的時間就能夠使用它了。
這是個有趣的問題。首先,volatile 變量和 atomic 變量看起來很像,但功能卻不同。Volatile變量能夠確保先行關係,即寫操做會發生在後續的讀操做以前, 但它並不能保證原子性。例如用volatile修飾count變量那麼 count++ 操做就不是原子性的。而AtomicInteger類提供的atomic方法可讓這種操做具備原子性如getAndIncrement()方法會原子性的進行增量操做把當前值加一,其它數據類型和引用變量也能夠進行類似操做。
這個問題坑了不少Java程序員,若你能想到鎖是否釋放這條線索來回答還有點但願答對。不管你的同步塊是正常仍是異常退出的,裏面的線程都會釋放鎖,因此對比鎖接口咱們更喜歡同步塊,由於它不用花費精力去釋放鎖,該功能能夠在finally block裏釋放鎖實現。
這個問題在Java面試中常常被問到,可是面試官對回答此問題的滿意度僅爲50%。一半的人寫不出雙檢鎖還有一半的人說不出它的隱患和Java1.5是如何對它修正的。它實際上是一個用來建立線程安全的單例的老方法,當單例實例第一次被建立時它試圖用單個鎖進行性能優化,可是因爲太過於複雜在JDK1.4中它是失敗的。
這是上面那個問題的後續,若是你不喜歡雙檢鎖而面試官問了建立Singleton類的替代方法,你能夠利用JVM的類加載和靜態變量初始化特徵來建立Singleton實例,或者是利用枚舉類型來建立Singleton。
如下三條最佳實踐大多數Java程序員都應該遵循:
這樣能夠方便找bug或追蹤。OrderProcessor, QuoteProcessor or TradeProcessor 這種名字比 Thread-1. Thread-2 and Thread-3 好多了,給線程起一個和它要完成的任務相關的名字,全部的主要框架甚至JDK都遵循這個最佳實踐。
鎖花費的代價高昂且上下文切換更耗費時間空間,試試最低限度的使用同步和鎖,縮小臨界區。所以相對於同步方法我更喜歡同步塊,它給我擁有對鎖的絕對控制權。
首先,CountDownLatch, Semaphore, CyclicBarrier 和 Exchanger 這些同步類簡化了編碼操做,而用wait和notify很難實現對複雜控制流的控制。其次,這些類是由最好的企業編寫和維護在後續的JDK中它們還會不斷優化和完善,使用這些更高等級的同步工具你的程序能夠不費吹灰之力得到優化。
這是另一個容易遵循且受益巨大的最佳實踐,併發集合比同步集合的可擴展性更好,因此在併發編程時使用併發集合效果更好。若是下一次你須要用到map,你應該首先想到用ConcurrentHashMap。
這個問題就像是如何強制進行Java垃圾回收,目前尚未以爲方法,雖然你能夠使用System.gc()來進行垃圾回收,可是不保證能成功。在Java裏面沒有辦法強制啓動一個線程,它是被線程調度器控制着且Java沒有公佈相關的API。
fork join框架是JDK7中出現的一款高效的工具,Java開發人員能夠經過它充分利用現代服務器上的多處理器。它是專門爲了那些能夠遞歸劃分紅許多子模塊設計的,目的是將全部可用的處理能力用來提高程序的性能。fork join框架一個巨大的優點是它使用了工做竊取算法,能夠完成更多任務的工做線程能夠從其它線程中竊取任務來執行。
Java程序中wait 和 sleep都會形成某種形式的暫停,它們能夠知足不一樣的須要。wait()方法用於線程間通訊,若是等待條件爲真且其它線程被喚醒時它會釋放鎖,而sleep()方法僅僅釋放CPU資源或者讓當前線程中止執行一段時間,但不會釋放鎖。須要注意的是,sleep()並不會讓線程終止,一旦從休眠中喚醒線程,線程的狀態將會被改變爲Runnable,而且根據線程調度,它將獲得執行。
ThreadGroup是一個類,它的目的是提供關於線程組的信息。
ThreadGroup API比較薄弱,它並無比Thread提供了更多的功能。它有兩個主要的功能:一是獲取線程組中處於活躍狀態線程的列表;二是設置爲線程設置未捕獲異常處理器(ncaught exception handler)。但在Java 1.5中Thread類也添加了setUncaughtExceptionHandler(UncaughtExceptionHandler eh) 方法,因此ThreadGroup是已通過時的,不建議繼續使用。
線程轉儲是一個JVM活動線程的列表,它對於分析系統瓶頸和死鎖很是有用。有不少方法能夠獲取線程轉儲——使用Profiler,Kill -3命令,jstack工具等等。咱們更喜歡jstack工具,由於它容易使用而且是JDK自帶的。因爲它是一個基於終端的工具,因此咱們能夠編寫一些腳本去定時的產生線程轉儲以待分析。
java.util.Timer是一個工具類,能夠用於安排一個線程在將來的某個特定時間執行。Timer類能夠用安排一次性任務或者週期任務。
java.util.TimerTask是一個實現了Runnable接口的抽象類,咱們須要去繼承這個類來建立咱們本身的定時任務並使用Timer去安排它的執行。
原子操做是指一個不受其餘操做影響的操做任務單元。原子操做是在多線程環境下避免數據不一致必須的手段。
int++並非一個原子操做,因此當一個線程讀取它的值並加1時,另一個線程有可能會讀到以前的值,這就會引起錯誤。
在 java.util.concurrent.atomic 包中添加原子變量類以後,這種狀況才發生了改變。全部原子變量類都公開比較並設置原語(與比較並交換相似),這些原語都是使用平臺上可用的最快本機結構(比較並交換、加載連接/條件存儲,最壞的狀況下是旋轉鎖)來實現的。 java.util.concurrent.atomic 包中提供了原子變量的 9 種風格( AtomicInteger; AtomicLong; AtomicReference; AtomicBoolean;原子整型;長型;引用;及原子標記引用和戳記引用類的數組形式,其原子地更新一對值)。
Lock接口比同步方法和同步塊提供了更具擴展性的鎖操做。他們容許更靈活的結構,能夠具備徹底不一樣的性質,而且能夠支持多個相關類的條件對象。
它的優點有:
Java 5在concurrency包中引入了java.util.concurrent.Callable 接口,它和Runnable接口很類似,但它能夠返回一個對象或者拋出一個異常。
Callable接口使用泛型去定義它的返回類型。Executors類提供了一些有用的方法去在線程池中執行Callable內的任務。因爲Callable任務是並行的,咱們必須等待它返回的結果。java.util.concurrent.Future對象爲咱們解決了這個問題。在線程池提交Callable任務後返回了一個Future對象,使用它咱們能夠知道Callable任務的狀態和獲得Callable返回的執行結果。Future提供了get()方法讓咱們能夠等待Callable結束並獲取它的執行結果。
FutureTask包裝器是一種很是便利的機制,可將Callable轉換成Future和Runnable,它同時實現二者的接口。
FutureTask類是Future 的一個實現,並實現了Runnable,因此可經過Excutor(線程池) 來執行。也可傳遞給Thread對象執行。若是在主線程中須要執行比較耗時的操做時,但又不想阻塞主線程時,能夠把這些做業交給Future對象在後臺完成,當主線程未來須要時,就能夠經過Future對象得到後臺做業的計算結果或者執行狀態。
Java集合類都是快速失敗的,這就意味着當集合被改變且一個線程在使用迭代器遍歷集合的時候,迭代器的next()方法將拋出ConcurrentModificationException異常。
併發容器:併發容器是針對多個線程併發訪問設計的,在jdk5.0引入了concurrent包,其中提供了不少併發容器,如ConcurrentHashMap,CopyOnWriteArrayList等。併發容器使用了與同步容器徹底不一樣的加鎖策略來提供更高的併發性和伸縮性,例如在ConcurrentHashMap中採用了一種粒度更細的加鎖機制,能夠稱爲分段鎖,在這種鎖機制下,容許任意數量的讀線程併發地訪問map,而且執行讀操做的線程和寫操做的線程也能夠併發的訪問map,同時容許必定數量的寫操做線程併發地修改map,因此它能夠在併發環境下實現更高的吞吐量。
使用Thread類的setDaemon(true)方法能夠將線程設置爲守護線程,須要注意的是,須要在調用start()方法前調用這個方法,不然會拋出IllegalThreadStateException異常。
當咱們在Java程序中建立一個線程,它就被稱爲用戶線程。一個守護線程是在後臺執行而且不會阻止JVM終止的線程。當沒有用戶線程在運行的時候,JVM關閉程序而且退出。一個守護線程建立的子線程依然是守護線程。
當咱們在Java程序中新建一個線程時,它的狀態是New。當咱們調用線程的start()方法時,狀態被改變爲Runnable。線程調度器會爲Runnable線程池中的線程分配CPU時間而且講它們的狀態改變爲Running。其餘的線程狀態還有Waiting,Blocked 和Dead。
當線程間是能夠共享資源時,線程間通訊是協調它們的重要的手段。Object類中wait()\notify()\notifyAll()方法能夠用於線程間通訊關於資源的鎖的狀態。
Thread類的sleep()和yield()方法將在當前正在執行的線程上運行。因此在其餘處於等待狀態的線程上調用這些方法是沒有意義的。這就是爲何這些方法是靜態的。它們能夠在當前正在執行的線程中工做,並避免程序員錯誤的認爲能夠在其餘非運行線程調用這些方法。
在Java中能夠有不少方法來保證線程安全——
同步,
使用原子類(atomic concurrent classes),
使用顯示鎖,
使用volatile關鍵字,
使用不變類
(1) 搶佔式調度策略
Java運行時系統的線程調度算法是搶佔式的 (preemptive)。Java運行時系統支持一種簡單的固定優先級的調度算法。若是一個優先級比其餘任何處於可運行狀態的線程都高的線程進入就緒狀態,那麼運行時系統就會選擇該線程運行。新的優先級較高的線程搶佔(preempt)了其餘線程。可是Java運行時系統並不搶佔同優先級的線程。換句話說,Java運行時系統不是分時的(time-slice)。然而,基於Java Thread類的實現系統多是支持分時的,所以編寫代碼時不要依賴分時。當系統中的處於就緒狀態的線程都具備相同優先級時,線程調度程序採用一種簡單的、非搶佔式的輪轉的調度順序。
(2) 時間片輪轉調度策略
有些系統的線程調度採用時間片輪轉(round-robin)調度策略。這種調度策略是從全部處於就緒狀態的線程中選擇優先級最高的線程分配必定的CPU時間運行。該時間事後再選擇其餘線程運行。只有當線程運行結束、放棄(yield)CPU或因爲某種緣由進入阻塞狀態,低優先級的線程纔有機會執行。若是有兩個優先級相同的線程都在等待CPU,則調度程序以輪轉的方式選擇運行的線程。
Thread.UncaughtExceptionHandler是java SE5中的新接口,它容許咱們在每個Thread對象上添加一個異常處理器。
Java中線程的狀態分爲6種。
初始(NEW):新建立了一個線程對象,但尚未調用start()方法。
運行(RUNNABLE):Java線程中將就緒(ready)和運行中(running)兩種狀態籠統的稱爲「運行」。
線程對象建立後,其餘線程(好比main線程)調用了該對象的start()方法。該狀態的線程位於可運行線程池中,等待被線程調度選中,獲取CPU的使用權,此時處於就緒狀態(ready)。就緒狀態的線程在得到CPU時間片後變爲運行中狀態(running)。阻塞(BLOCKED):表示線程阻塞於鎖。
等待(WAITING):進入該狀態的線程須要等待其餘線程作出一些特定動做(通知或中斷)。
超時等待(TIMED_WAITING):該狀態不一樣於WAITING,它能夠在指定的時間後自行返回。
終止(TERMINATED):表示該線程已經執行完畢。
這6種狀態定義在Thread類的State枚舉中,可查看源碼進行一一對應。
實現Runnable接口和繼承Thread能夠獲得一個線程類,new一個實例出來,線程就進入了初始狀態。
2.執行Thread#start以後,線程進行RUNNABLE可運行狀態
線程調度程序從可運行池中選擇一個線程做爲當前線程時線程所處的狀態。這也是線程進入運行狀態的惟一的一種方式。
阻塞狀態是線程阻塞在進入synchronized關鍵字修飾的方法或代碼塊(獲取鎖)時的狀態。
處於這種狀態的線程不會被分配CPU執行時間,它們要等待被顯式地喚醒,不然會處於無限期等待的狀態。
處於這種狀態的線程不會被分配CPU執行時間,不過無須無限期等待被其餘線程顯示地喚醒,在達到必定時間後它們會自動喚醒。
synchronized要理解爲加鎖,而不是鎖,這個思惟有助於你更好的理解線程同步。
這裏簡要介紹一下,爲之後的內容作一下鋪墊:
public synchronized void say(){ System.out.println("Hello,everyone..."); }
public void say(boolean isYou){ synchronized (obj){ System.out.println("Hello"); } }
public static synchronized void work(){ System.out.println("Work hard..."); }
但願你們再遇到對象鎖,類鎖而不知所措…有時候遇到面試官把問題描述的不夠清楚時,要敢於及時和麪試官溝通。雖然找工做時總會遇到奇葩面試官,可是若是你遇到的概率過高時,請自覺地審視一下本身…
看到上面synchronized的用法,你會有這樣的疑問嗎?synchronized能修飾類級別(靜態)代碼塊嗎?(PS: 這個話題是我臨時想起的,改天在面試中問一下看看效果…)
結論:synchronized不能用在類級別的(靜態)代碼塊
若是在面試中不給你編譯器,大多數人估計都是要mountain泰吧。這裏直接給出個人理解:
這個要從加載順序上考慮。
類級別的代碼塊在加載順序上是要優先於任何方法的,其執行順序只跟代碼位置前後有關。沒人跟你搶,天然不須要同步。
這裏經過一個常見的面試題-單例模式來展開,這篇文章主要內容是考察synchronized關鍵字的,就直奔主題進入DCL(Double Check Lock)雙重校驗鎖的單例。
若是DCL不懂,就尷尬無止境啦…。感興趣的能夠去看一下《Java高併發核心編程(卷2)》
public class SingleInstance { private volatile static SingleInstance instance = null; private SingleInstance(){ } public static SingleInstance getInstance(){ if (instance == null){ synchronized (SingleInstance.class){ if (instance == null){ instance = new SingleInstance(); } } } return instance; } }
synchronized 關鍵字主要用來解決的是多線程同步問題,其能夠保證在被其修飾的代碼任意時刻只有一個線程執行。視狀況而定,(主動)說出它的用法及底層實現原理(使用的是moniterenter 和 moniterexit指令…),PS:synchronized的底層實現原理會單獨展開…
volatile只能保證變量的可見性,並不能保證對volatile修飾的變量的操做的原子性。
volatile的主要做用:
- 保持內存可見性;使全部線程都能看到共享內存的最新狀態。
- 防止指令重排的問題;
經過設置內存屏障實現的。感興趣的能夠去看一下《Java高併發核心編程(卷2)》
我的拙見,能答出來上面的內容便可,更深刻的絕大部分都是在SHOW或者就是壓薪資…
爲了節約各位看官的時間,先把結論給出來:
- 如果對象鎖,則每一個對象都持有一把本身的獨一無二的鎖,且對象之間的鎖互不影響 。如果類鎖,全部該類的對象共用這把鎖。
- 一個線程獲取一把鎖,沒有獲得鎖的線程只能排隊等待;
- synchronized 是可重入鎖,避免不少狀況下的死鎖發生。
- synchronized 方法若發生異常,則JVM會自動釋放鎖。
- 鎖對象不能爲空,不然拋出NPE(NullPointerException)
- 同步自己是不具有繼承性的:即父類的synchronized 方法,子類重寫該方法,分狀況討論:沒有synchonized修飾,則該子類方法不是線程同步的。(PS :涉及同步繼承性的問題要分狀況)
- synchronized自己修飾的範圍越小越好。畢竟是同步阻塞。跑不快還佔着超車道…
結論:不能,二者的鎖對象不同。前者是類鎖(XXX.class),後者是this
結論:不能,由於synchronized只會對被修飾的方法起做用。
結論:不能,每一個對象都擁有一把鎖。兩個對象至關於有兩把鎖,致使鎖對象不一致。(PS:若是是類鎖,則全部對象共用一把鎖)
JVM會自動釋放鎖,不會致使死鎖問題
鎖對象不能爲空,不然拋出NPE(NullPointerException)
鎖對象不能爲空,不然拋出NPE(NullPointerException)
重寫父類的synchronized的方法,主要分爲兩種狀況:
synchronized的不具有繼承性。因此子類方法是線程不安全的。
兩個鎖對象實際上是一把鎖,並且是子類對象做爲鎖。這也證實了: synchronized的鎖是可重入鎖。不然將出現死鎖問題。
關於synchronized 的內容部分,我在面試過程當中常常問且只問這一道題。本人認爲這個能很好的考察面試者的綜合素質。(PS:畢竟是要擰螺絲的…)
synchronized同步的範圍是越小越好。由於若該方法耗時好久,那其它線程必須等到該持鎖線程執行完才能運行。(黃花菜都涼了都…)
而synchronized代碼塊部分只有這一部分是同步的,其它的照樣能夠異步執行,提升運行效率。
你能夠經過共享對象來實現這個目的,或者是使用像阻塞隊列這樣併發的數據結構。這篇教程《Java線程間通訊》(涉及到在兩個線程間共享對象)用wait和notify方法實現了生產者消費者模型。
這又是一個刁鑽的問題,由於多線程能夠等待單監控鎖,Java API 的設計人員提供了一些方法當等待條件改變的時候通知它們,可是這些方法沒有徹底實現。notify()方法不能喚醒某個具體的線程,因此只有一個線程在等待的時候它纔有用武之地。而notifyAll()喚醒全部線程並容許他們爭奪鎖確保了至少有一個線程能繼續運行。
一個很明顯的緣由是JAVA提供的鎖是對象級的而不是線程級的,每一個對象都有鎖,經過線程得到。若是線程須要等待某些鎖那麼調用對象中的wait()方法就有意義了。若是wait()方法定義在Thread類中,線程正在等待的是哪一個鎖就不明顯了。簡單的說,因爲wait,notify和notifyAll都是鎖級別的操做,因此把他們定義在Object類中由於鎖屬於對象。
當一個線程須要調用對象的wait()方法的時候,這個線程必須擁有該對象的鎖,接着它就會釋放這個對象鎖並進入等待狀態直到其餘線程調用這個對象上的notify()方法。一樣的,當一個線程須要調用對象的notify()方法時,它會釋放這個對象的鎖,以便其餘在等待的線程就能夠獲得這個對象鎖。因爲全部的這些方法都須要線程持有對象的鎖,這樣就只能經過同步來實現,因此他們只能在同步方法或者同步塊中被調用。若是你不這麼作,代碼會拋出IllegalMonitorStateException異常。
處於等待狀態的線程可能會收到錯誤警報和僞喚醒,若是不在循環中檢查等待條件,程序就會在沒有知足結束條件的狀況下退出。所以,當一個等待線程醒來時,不能認爲它原來的等待狀態仍然是有效的,在notify()方法調用以後和等待線程醒來以前這段時間它可能會改變。這就是在循環中使用wait()方法效果更好的緣由,你能夠在Eclipse中建立模板調用wait和notify試一試。若是你想了解更多關於這個問題的內容,推薦你閱讀《Effective Java》這本書中的線程和同步章節。
線程1獲取對象A的鎖,正在使用對象A。
線程1調用對象A的wait()方法。
線程1釋放對象A的鎖,並立刻進入等待隊列。
鎖池裏面的對象爭搶對象A的鎖。
線程5得到對象A的鎖,進入synchronized塊,使用對象A。
線程5調用對象A的notifyAll()方法,喚醒全部線程,全部線程進入同步隊列。若線程5調用對象A的notify()方法,則喚醒一個線程,不知道會喚醒誰,被喚醒的那個線程進入同步隊列。
notifyAll()方法所在synchronized結束,線程5釋放對象A的鎖。
同步隊列的線程爭搶對象鎖,但線程1何時能搶到就不知道了。
記得開始學習Java的時候,一遇到多線程狀況就使用synchronized,相對於當時的咱們來講synchronized是這麼的神奇而又強大,那個時候咱們賦予它一個名字「同步」,也成爲了咱們解決多線程狀況的百試不爽的良藥。可是,隨着學習的進行咱們知道在JDK1.5以前synchronized是一個重量級鎖,相對於j.u.c.Lock,它會顯得那麼笨重,以致於咱們認爲它不是那麼的高效而慢慢摒棄它。
不過,隨着Javs SE 1.6對synchronized進行的各類優化後,synchronized並不會顯得那麼重了。下面來一塊兒探索synchronized的基本使用、實現機制、Java是如何對它進行了優化、鎖優化機制、鎖的存儲結構等升級過程。
Synchronized是Java中解決併發問題的一種最經常使用的方法,也是最簡單的一種方法。Synchronized的做用主要有三個:
- 原子性:確保線程互斥的訪問同步代碼;
- 可見性:保證共享變量的修改可以及時可見,實際上是經過Java內存模型中的 「對一個變量unlock操做以前,必需要同步到主內存中;若是對一個變量進行lock操做,則將會清空工做內存中此變量的值,在執行引擎使用此變量前,須要從新從主內存中load操做或assign操做初始化變量值」 來保證的;
- 有序性:有效解決重排序問題,即 「一個unlock操做先行發生(happen-before)於後面對同一個鎖的lock操做」;
從語法上講,Synchronized能夠把任何一個非null對象做爲"鎖",在HotSpot JVM實現中,鎖有個專門的名字:對象監視器(Object Monitor)。
Synchronized總共有三種用法:
- 當synchronized做用在實例方法時,監視器鎖(monitor)即是對象實例(this);
- 當synchronized做用在靜態方法時,監視器鎖(monitor)即是對象的Class實例,由於Class數據存在於永久代,所以靜態方法鎖至關於該類的一個全局鎖;
- 當synchronized做用在某一個對象實例時,監視器鎖(monitor)即是括號括起來的對象實例;
注意,synchronized 內置鎖 是一種 對象鎖(鎖的是對象而非引用變量),做用粒度是對象 ,能夠用來實現對 臨界資源的同步互斥訪問 ,是 可重入 的。其可重入最大的做用是避免死鎖,如:
子類同步方法調用了父類同步方法,如沒有可重入的特性,則會發生死鎖;
數據同步須要依賴鎖,那鎖的同步又依賴誰?synchronized給出的答案是在軟件層面依賴JVM,而j.u.c.Lock給出的答案是在硬件層面依賴特殊的CPU指令。
當一個線程訪問同步代碼塊時,首先是須要獲得鎖才能執行同步代碼,當退出或者拋出異常時必需要釋放鎖,那麼它是如何來實現這個機制的呢?咱們先看一段簡單的代碼:
package com.paddx.test.concurrent; public class SynchronizedDemo { public void method() { synchronized (this) { System.out.println("Method 1 start"); } } }
查看反編譯後結果:
反編譯結果
monitorenter:每一個對象都是一個監視器鎖(monitor)。當monitor被佔用時就會處於鎖定狀態,線程執行monitorenter指令時嘗試獲取monitor的全部權,過程以下:
- 若是monitor的進入數爲0,則該線程進入monitor,而後將進入數設置爲1,該線程即爲monitor的全部者;
- 若是線程已經佔有該monitor,只是從新進入,則進入monitor的進入數加1;
- 若是其餘線程已經佔用了monitor,則該線程進入阻塞狀態,直到monitor的進入數爲0,再從新嘗試獲取monitor的全部權;
monitorexit:執行monitorexit的線程必須是objectref所對應的monitor的全部者。指令執行時,monitor的進入數減1,若是減1後進入數爲0,那線程退出monitor,再也不是這個monitor的全部者。其餘被這個monitor阻塞的線程能夠嘗試去獲取這個 monitor 的全部權。
monitorexit指令出現了兩次,第1次爲同步正常退出釋放鎖;第2次爲發生異步退出釋放鎖;
經過上面兩段描述,咱們應該能很清楚的看出Synchronized的實現原理,Synchronized的語義底層是經過一個monitor的對象來完成,其實wait/notify等方法也依賴於monitor對象,這就是爲何只有在同步的塊或者方法中才能調用wait/notify等方法,不然會拋出java.lang.IllegalMonitorStateException的異常的緣由。
再來看一下同步方法:
package com.paddx.test.concurrent; public class SynchronizedMethod { public synchronized void method() { System.out.println("Hello World!"); } }
查看反編譯後結果:
反編譯結果
從編譯的結果來看,方法的同步並無經過指令 monitorenter
和 monitorexit
來完成(理論上其實也能夠經過這兩條指令來實現),不過相對於普通方法,其常量池中多了 ACC_SYNCHRONIZED
標示符。JVM就是根據該標示符來實現方法的同步的:
當方法調用時,調用指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標誌是否被設置,若是設置了,執行線程將先獲取monitor,獲取成功以後才能執行方法體,方法執行完後再釋放monitor。在方法執行期間,其餘任何線程都沒法再得到同一個monitor對象。
兩種同步方式本質上沒有區別,只是方法的同步是一種隱式的方式來實現,無需經過字節碼來完成。兩個指令的執行是JVM經過調用操做系統的互斥原語mutex來實現,被阻塞的線程會被掛起、等待從新調度,會致使「用戶態和內核態」兩個態之間來回切換,對性能有較大影響。
在JVM中,對象在內存中的佈局分爲三塊區域:對象頭、實例數據和對齊填充。以下圖所示:
Synchronized用的鎖就是存在Java對象頭裏的,那麼什麼是Java對象頭呢?Hotspot虛擬機的對象頭主要包括兩部分數據:Mark Word(標記字段)、Class Pointer(類型指針)。其中 Class Pointer是對象指向它的類元數據的指針,虛擬機經過這個指針來肯定這個對象是哪一個類的實例,Mark Word用於存儲對象自身的運行時數據,它是實現輕量級鎖和偏向鎖的關鍵。 Java對象頭具體結構描述以下:
Java對象頭結構組成
Mark Word用於存儲對象自身的運行時數據,如:哈希碼(HashCode)、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程 ID、偏向時間戳等。好比鎖膨脹就是藉助Mark Word的偏向的線程ID 參考:JAVA鎖的膨脹過程和優化(阿里) 阿里也常常問的問題
下圖是Java對象頭 無鎖狀態下Mark Word部分的存儲結構(32位虛擬機):
Mark Word存儲結構
對象頭信息是與對象自身定義的數據無關的額外存儲成本,可是考慮到虛擬機的空間效率,Mark Word被設計成一個非固定的數據結構以便在極小的空間內存存儲儘可能多的數據,它會根據對象的狀態複用本身的存儲空間,也就是說,Mark Word會隨着程序的運行發生變化,可能變化爲存儲如下4種數據:
Mark Word可能存儲4種數據
在64位虛擬機下,Mark Word是64bit大小的,其存儲結構以下:
64位Mark Word存儲結構
對象頭的最後兩位存儲了鎖的標誌位,01是初始狀態,未加鎖,其對象頭裏存儲的是對象自己的哈希碼,隨着鎖級別的不一樣,對象頭裏會存儲不一樣的內容。偏向鎖存儲的是當前佔用此對象的線程ID;而輕量級則存儲指向線程棧中鎖記錄的指針。從這裏咱們能夠看到,「鎖」這個東西,多是個鎖記錄+對象頭裏的引用指針(判斷線程是否擁有鎖時將線程的鎖記錄地址和對象頭裏的指針地址比較),也多是對象頭裏的線程ID(判斷線程是否擁有鎖時將線程的ID和對象頭裏存儲的線程ID比較)。
HotSpot虛擬機對象頭Mark Word
在線程進入同步代碼塊的時候,若是此同步對象沒有被鎖定,即它的鎖標誌位是01,則虛擬機首先在當前線程的棧中建立咱們稱之爲「鎖記錄(Lock Record)」的空間,用於存儲鎖對象的Mark Word的拷貝,官方把這個拷貝稱爲Displaced Mark Word。整個Mark Word及其拷貝相當重要。
Lock Record是線程私有的數據結構,每個線程都有一個可用Lock Record列表,同時還有一個全局的可用列表。每個被鎖住的對象Mark Word都會和一個Lock Record關聯(對象頭的MarkWord中的Lock Word指向Lock Record的起始地址),同時Lock Record中有一個Owner字段存放擁有該鎖的線程的惟一標識(或者object mark word
),表示該鎖被這個線程佔用。以下圖所示爲Lock Record的內部結構:
Lock Record | 描述 |
---|---|
Owner | 初始時爲NULL表示當前沒有任何線程擁有該monitor record,當線程成功擁有該鎖後保存線程惟一標識,當鎖被釋放時又設置爲NULL; |
EntryQ | 關聯一個系統互斥鎖(semaphore),阻塞全部試圖鎖住monitor record失敗的線程; |
RcThis | 表示blocked或waiting在該monitor record上的全部線程的個數; |
Nest | 用來實現 重入鎖的計數; |
HashCode | 保存從對象頭拷貝過來的HashCode值(可能還包含GC age)。 |
Candidate | 用來避免沒必要要的阻塞或等待線程喚醒,由於每一次只有一個線程可以成功擁有鎖,若是每次前一個釋放鎖的線程喚醒全部正在阻塞或等待的線程,會引發沒必要要的上下文切換(從阻塞到就緒而後由於競爭鎖失敗又被阻塞)從而致使性能嚴重降低。Candidate只有兩種可能的值0表示沒有須要喚醒的線程1表示要喚醒一個繼任線程來競爭鎖。 |
任何一個對象都有一個Monitor與之關聯,當且一個Monitor被持有後,它將處於鎖定狀態。Synchronized在JVM裏的實現都是 基於進入和退出Monitor對象來實現方法同步和代碼塊同步,雖然具體實現細節不同,可是均可以經過成對的MonitorEnter和MonitorExit指令來實現。
那什麼是Monitor?能夠把它理解爲 一個同步工具,也能夠描述爲 一種同步機制,它一般被 描述爲一個對象。
與一切皆對象同樣,全部的Java對象是天生的Monitor,每個Java對象都有成爲Monitor的潛質,由於在Java的設計中 ,每個Java對象自打孃胎裏出來就帶了一把看不見的鎖,它叫作內部鎖或者Monitor鎖。
也就是一般說Synchronized的對象鎖,MarkWord鎖標識位爲10,其中指針指向的是Monitor對象的起始地址。在Java虛擬機(HotSpot)中,Monitor是由ObjectMonitor實現的,其主要數據結構以下(位於HotSpot虛擬機源碼ObjectMonitor.hpp文件,C++實現的):
ObjectMonitor() { _header = NULL; _count = 0; // 記錄個數 _waiters = 0, _recursions = 0; _object = NULL; _owner = NULL; _WaitSet = NULL; // 處於wait狀態的線程,會被加入到_WaitSet _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ; // 處於等待鎖block狀態的線程,會被加入到該列表 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; }
ObjectMonitor中有兩個隊列,_WaitSet 和 _EntryList,用來保存ObjectWaiter對象列表( 每一個等待鎖的線程都會被封裝成ObjectWaiter對象 ),_owner指向持有ObjectMonitor對象的線程,當多個線程同時訪問一段同步代碼時:
- 首先會進入 _EntryList 集合,當線程獲取到對象的monitor後,進入 _Owner區域並把monitor中的owner變量設置爲當前線程,同時monitor中的計數器count加1;
- 若線程調用 wait() 方法,將釋放當前持有的monitor,owner變量恢復爲null,count自減1,同時該線程進入 WaitSet集合中等待被喚醒;
- 若當前線程執行完畢,也將釋放monitor(鎖)並復位count的值,以便其餘線程進入獲取monitor(鎖);
同時,Monitor對象存在於每一個Java對象的對象頭Mark Word中(存儲的指針的指向),Synchronized鎖即是經過這種方式獲取鎖的,也是爲何Java中任意對象能夠做爲鎖的緣由,同時notify/notifyAll/wait等方法會使用到Monitor鎖對象,因此必須在同步代碼塊中使用。
監視器Monitor有兩種同步方式:互斥與協做。多線程環境下線程之間若是須要共享數據,須要解決互斥訪問數據的問題,監視器能夠確保監視器上的數據在同一時刻只會有一個線程在訪問。
何時須要協做? 好比:
一個線程向緩衝區寫數據,另外一個線程從緩衝區讀數據,若是讀線程發現緩衝區爲空就會等待,當寫線程向緩衝區寫入數據,就會喚醒讀線程,這裏讀線程和寫線程就是一個合做關係。JVM經過Object類的wait方法來使本身等待,在調用wait方法後,該線程會釋放它持有的監視器,直到其餘線程通知它纔有執行的機會。一個線程調用notify方法通知在等待的線程,這個等待的線程並不會立刻執行,而是要通知線程釋放監視器後,它從新獲取監視器纔有執行的機會。若是恰好喚醒的這個線程須要的監視器被其餘線程搶佔,那麼這個線程會繼續等待。Object類中的notifyAll方法能夠解決這個問題,它能夠喚醒全部等待的線程,總有一個線程執行。
如上圖所示,一個線程經過1號門進入Entry Set(入口區),若是在入口區沒有線程等待,那麼這個線程就會獲取監視器成爲監視器的Owner,而後執行監視區域的代碼。若是在入口區中有其它線程在等待,那麼新來的線程也會和這些線程一塊兒等待。線程在持有監視器的過程當中,有兩個選擇,一個是正常執行監視器區域的代碼,釋放監視器,經過5號門退出監視器;還有可能等待某個條件的出現,因而它會經過3號門到Wait Set(等待區)休息,直到相應的條件知足後再經過4號門進入從新獲取監視器再執行。
注意:
當一個線程釋放監視器時,在入口區和等待區的等待線程都會去競爭監視器,若是入口區的線程贏了,會從2號門進入;若是等待區的線程贏了會從4號門進入。只有經過3號門才能進入等待區,在等待區中的線程只有經過4號門才能退出等待區,也就是說一個線程只有在持有監視器時才能執行wait操做,處於等待的線程只有再次得到監視器才能退出等待狀態。
從JDK5引入了現代操做系統新增長的CAS原子操做( JDK5中並無對synchronized關鍵字作優化,而是體如今J.U.C中,因此在該版本concurrent包有更好的性能 ),從JDK6開始,就對synchronized的實現機制進行了較大調整,包括使用JDK5引進的CAS自旋以外,還增長了自適應的CAS自旋、鎖消除、鎖膨脹、偏向鎖、輕量級鎖這些優化策略。因爲此關鍵字的優化使得性能極大提升,同時語義清晰、操做簡單、無需手動關閉,因此推薦在容許的狀況下儘可能使用此關鍵字,同時在性能上此關鍵字還有優化的空間。
鎖主要存在四種狀態,依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態、重量級鎖狀態,鎖能夠從偏向鎖升級到輕量級鎖,再升級的重量級鎖。可是鎖的升級是單向的,也就是說只能從低到高升級,不會出現鎖的降級。
在 JDK 1.6 中默認是開啓偏向鎖和輕量級鎖的,能夠經過-XX:-UseBiasedLocking來禁用偏向鎖。
線程的阻塞和喚醒須要CPU從用戶態轉爲核心態,頻繁的阻塞和喚醒對CPU來講是一件負擔很重的工做,勢必會給系統的併發性能帶來很大的壓力。同時咱們發如今許多應用上面,對象鎖的鎖狀態只會持續很短一段時間,爲了這一段很短的時間頻繁地阻塞和喚醒線程是很是不值得的。
因此引入自旋鎖,何謂自旋鎖?
所謂自旋鎖,就是指當一個線程嘗試獲取某個鎖時,若是該鎖已被其餘線程佔用,就一直循環檢測鎖是否被釋放,而不是進入線程掛起或睡眠狀態。
自旋鎖適用於鎖保護的臨界區很小的狀況,臨界區很小的話,鎖佔用的時間就很短。自旋等待不能替代阻塞,雖然它能夠避免線程切換帶來的開銷,可是它佔用了CPU處理器的時間。若是持有鎖的線程很快就釋放了鎖,那麼自旋的效率就很是好,反之,自旋的線程就會白白消耗掉處理的資源,它不會作任何有意義的工做,典型的佔着茅坑不拉屎,這樣反而會帶來性能上的浪費。因此說,自旋等待的時間(自旋的次數)必需要有一個限度,若是自旋超過了定義的時間仍然沒有獲取到鎖,則應該被掛起。
自旋鎖在JDK 1.4.2中引入,默認關閉,可是能夠使用-XX:+UseSpinning開開啓,在JDK1.6中默認開啓。同時自旋的默認次數爲10次,能夠經過參數-XX:PreBlockSpin來調整。
若是經過參數-XX:PreBlockSpin來調整自旋鎖的自旋次數,會帶來諸多不便。假如將參數調整爲10,可是系統不少線程都是等你剛剛退出的時候就釋放了鎖(假如多自旋一兩次就能夠獲取鎖),是否是很尷尬。因而JDK1.6引入自適應的自旋鎖,讓虛擬機會變得愈來愈聰明。
JDK 1.6引入了更加聰明的自旋鎖,即自適應自旋鎖。所謂自適應就意味着自旋的次數再也不是固定的,它是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。那它如何進行適應性自旋呢?
線程若是自旋成功了,那麼下次自旋的次數會更加多,由於虛擬機認爲既然上次成功了,那麼這次自旋也頗有可能會再次成功,那麼它就會容許自旋等待持續的次數更多。反之,若是對於某個鎖,不多有自旋可以成功,那麼在之後要或者這個鎖的時候自旋的次數會減小甚至省略掉自旋過程,以避免浪費處理器資源。
有了自適應自旋鎖,隨着程序運行和性能監控信息的不斷完善,虛擬機對程序鎖的情況預測會愈來愈準確,虛擬機會變得愈來愈聰明。
爲了保證數據的完整性,在進行操做時須要對這部分操做進行同步控制,可是在有些狀況下,JVM檢測到不可能存在共享數據競爭,這是JVM會對這些同步鎖進行鎖消除。
鎖消除的依據是逃逸分析的數據支持
若是不存在競爭,爲何還須要加鎖呢?因此鎖消除能夠節省毫無心義的請求鎖的時間。變量是否逃逸,對於虛擬機來講須要使用數據流分析來肯定,可是對於程序員來講這還不清楚麼?在明明知道不存在數據競爭的代碼塊前加上同步嗎?可是有時候程序並非咱們所想的那樣?雖然沒有顯示使用鎖,可是在使用一些JDK的內置API時,如StringBuffer、Vector、HashTable等,這個時候會存在隱形的加鎖操做。好比StringBuffer的append()方法,Vector的add()方法:
public void vectorTest(){ Vector<String> vector = new Vector<String>(); for(int i = 0 ; i < 10 ; i++){ vector.add(i + ""); } System.out.println(vector); }
在運行這段代碼時,JVM能夠明顯檢測到變量vector沒有逃逸出方法vectorTest()以外,因此JVM能夠大膽地將vector內部的加鎖操做消除。
在使用同步鎖的時候,須要讓同步塊的做用範圍儘量小—僅在共享數據的實際做用域中才進行同步,這樣作的目的是 爲了使須要同步的操做數量儘量縮小,若是存在鎖競爭,那麼等待鎖的線程也能儘快拿到鎖。
在大多數的狀況下,上述觀點是正確的。可是若是一系列的連續加鎖解鎖操做,可能會致使沒必要要的性能損耗,因此引入鎖粗話的概念。
鎖粗話概念比較好理解,就是將多個連續的加鎖、解鎖操做鏈接在一塊兒,擴展成一個範圍更大的鎖
如上面實例:
vector每次add的時候都須要加鎖操做,JVM檢測到對同一個對象(vector)連續加鎖、解鎖操做,會合並一個更大範圍的加鎖、解鎖操做,即加鎖解鎖操做會移到for循環以外。
偏向鎖是JDK6中的重要引進,由於HotSpot做者通過研究實踐發現,在大多數狀況下,鎖不只不存在多線程競爭,並且老是由同一線程屢次得到,爲了讓線程得到鎖的代價更低,引進了偏向鎖。
偏向鎖是在單線程執行代碼塊時使用的機制,若是在多線程併發的環境下(即線程A還沒有執行完同步代碼塊,線程B發起了申請鎖的申請),則必定會轉化爲輕量級鎖或者重量級鎖。
在JDK5中偏向鎖默認是關閉的,而到了JDK6中偏向鎖已經默認開啓。若是併發數較大同時同步代碼塊執行時間較長,則被多個線程同時訪問的機率就很大,就能夠使用參數-XX:-UseBiasedLocking來禁止偏向鎖(但這是個JVM參數,不能針對某個對象鎖來單獨設置)。
引入偏向鎖主要目的是:爲了在沒有多線程競爭的狀況下儘可能減小沒必要要的輕量級鎖執行路徑。由於輕量級鎖的加鎖解鎖操做是須要依賴屢次CAS原子指令的,而偏向鎖只須要在置換ThreadID的時候依賴一次CAS原子指令(因爲一旦出現多線程競爭的狀況就必須撤銷偏向鎖,因此偏向鎖的撤銷操做的性能損耗也必須小於節省下來的CAS原子指令的性能消耗)。
輕量級鎖是爲了在線程交替執行同步塊時提升性能,而偏向鎖則是在只有一個線程執行同步塊時進一步提升性能。
那麼偏向鎖是如何來減小沒必要要的CAS操做呢?首先咱們看下無競爭下鎖存在什麼問題:
如今幾乎全部的鎖都是可重入的,即已經得到鎖的線程能夠屢次鎖住/解鎖監視對象,按照以前的HotSpot設計,每次加鎖/解鎖都會涉及到一些CAS操做(好比對等待隊列的CAS操做),CAS操做會延遲本地調用,所以偏向鎖的想法是 一旦線程第一次得到了監視對象,以後讓監視對象「偏向」這個線程,以後的屢次調用則能夠避免CAS操做,說白了就是置個變量,若是發現爲true則無需再走各類加鎖/解鎖流程。
CAS爲何會引入本地延遲?這要從SMP(對稱多處理器)架構提及,下圖大概代表了SMP的結構:
SMP(對稱多處理器)架構
其意思是 全部的CPU會共享一條系統總線(BUS),靠此總線鏈接主存。每一個核都有本身的一級緩存,各核相對於BUS對稱分佈,所以這種結構稱爲「對稱多處理器」。
而CAS的全稱爲Compare-And-Swap,是一條CPU的原子指令,其做用是讓CPU比較後原子地更新某個位置的值,通過調查發現,其實現方式是基於硬件平臺的彙編指令,就是說CAS是靠硬件實現的,JVM只是封裝了彙編調用,那些AtomicInteger類即是使用了這些封裝後的接口。
例如:Core1和Core2可能會同時把主存中某個位置的值Load到本身的L1 Cache中,當Core1在本身的L1 Cache中修改這個位置的值時,會經過總線,使Core2中L1 Cache對應的值「失效」,而Core2一旦發現本身L1 Cache中的值失效(稱爲Cache命中缺失)則會經過總線從內存中加載該地址最新的值,你們經過總線的來回通訊稱爲「Cache一致性流量」,由於總線被設計爲固定的「通訊能力」,若是Cache一致性流量過大,總線將成爲瓶頸。而當Core1和Core2中的值再次一致時,稱爲「Cache一致性」,從這個層面來講,鎖設計的終極目標即是減小Cache一致性流量。
而CAS剛好會致使Cache一致性流量,若是有不少線程都共享同一個對象,當某個Core CAS成功時必然會引發總線風暴,這就是所謂的本地延遲,本質上偏向鎖就是爲了消除CAS,下降Cache一致性流量。
Cache一致性:
上面提到Cache一致性,實際上是有協議支持的,如今通用的協議是MESI(最先由Intel開始支持),具體參考:http://en.wikipedia.org/wiki/MESI_protocol。
Cache一致性流量的例外狀況:
其實也不是全部的CAS都會致使總線風暴,這跟Cache一致性協議有關,具體參考:http://blogs.oracle.com/dave/entry/biased_locking_in_hotspot
NUMA(Non Uniform Memory Access Achitecture)架構:
與SMP對應還有非對稱多處理器架構,如今主要應用在一些高端處理器上,主要特色是沒有總線,沒有公用主存,每一個Core有本身的內存,針對這種結構此處不作討論。
因此,當一個線程訪問同步塊並獲取鎖時,會在對象頭和棧幀中的鎖記錄裏存儲鎖偏向的線程ID,之後該線程進入和退出同步塊時不須要花費CAS操做來爭奪鎖資源,只須要檢查是否爲偏向鎖、鎖標識爲以及ThreadID便可,處理流程以下:
- 檢測Mark Word是否爲可偏向狀態,便是否爲偏向鎖1,鎖標識位爲01;
- 若爲可偏向狀態,則測試線程ID是否爲當前線程ID,若是是,則執行步驟(5),不然執行步驟(3);
- 若是測試線程ID不爲當前線程ID,則經過CAS操做競爭鎖,競爭成功,則將Mark Word的線程ID替換爲當前線程ID,不然執行線程(4);
- 經過CAS競爭鎖失敗,證實當前存在多線程競爭狀況,當到達全局安全點,得到偏向鎖的線程被掛起,偏向鎖升級爲輕量級鎖,而後被阻塞在安全點的線程繼續往下執行同步代碼塊;
- 執行同步代碼塊;
偏向鎖的釋放採用了 一種只有競爭纔會釋放鎖的機制,線程是不會主動去釋放偏向鎖,須要等待其餘線程來競爭。偏向鎖的撤銷須要 等待全局安全點(這個時間點是上沒有正在執行的代碼)。其步驟以下:
- 暫停擁有偏向鎖的線程;
- 判斷鎖對象是否還處於被鎖定狀態,否,則恢復到無鎖狀態(01),以容許其他線程競爭。是,則掛起持有鎖的當前線程,並將指向當前線程的鎖記錄地址的指針放入對象頭Mark Word,升級爲輕量級鎖狀態(00),而後恢復持有鎖的當前線程,進入輕量級鎖的競爭模式;
注意:此處將 當前線程掛起再恢復的過程當中並無發生鎖的轉移,仍然在當前線程手中,只是穿插了個 「將對象頭中的線程ID變動爲指向鎖記錄地址的指針」 這麼個事。
偏向鎖的獲取和釋放過程
引入輕量級鎖的主要目的是 在沒有多線程競爭的前提下,減小傳統的重量級鎖使用操做系統互斥量產生的性能消耗。當關閉偏向鎖功能或者多個線程競爭偏向鎖致使偏向鎖升級爲輕量級鎖,則會嘗試獲取輕量級鎖,其步驟以下:
在線程進入同步塊時,若是同步對象鎖狀態爲無鎖狀態(鎖標誌位爲「01」狀態,是否爲偏向鎖爲「0」),虛擬機首先將在當前線程的棧幀中創建一個名爲鎖記錄(Lock Record)的空間,用於存儲鎖對象目前的Mark Word的拷貝,官方稱之爲 Displaced Mark Word。此時線程堆棧與對象頭的狀態以下圖所示:
輕量級鎖CAS操做以前線程堆棧與對象的狀態
拷貝對象頭中的Mark Word複製到鎖記錄(Lock Record)中;
拷貝成功後,虛擬機將使用CAS操做嘗試將對象Mark Word中的Lock Word更新爲指向當前線程Lock Record的指針,並將Lock record裏的owner指針指向object mark word。若是更新成功,則執行步驟(4),不然執行步驟(5);
若是這個更新動做成功了,那麼當前線程就擁有了該對象的鎖,而且對象Mark Word的鎖標誌位設置爲「00」,即表示此對象處於輕量級鎖定狀態,此時線程堆棧與對象頭的狀態以下圖所示:
輕量級鎖CAS操做以後線程堆棧與對象的狀態
若是這個更新操做失敗了,虛擬機首先會檢查對象Mark Word中的Lock Word是否指向當前線程的棧幀,若是是,就說明當前線程已經擁有了這個對象的鎖,那就能夠直接進入同步塊繼續執行。不然說明多個線程競爭鎖,進入自旋執行(3),若自旋結束時仍未得到鎖,輕量級鎖就要膨脹爲重量級鎖,鎖標誌的狀態值變爲「10」,Mark Word中存儲的就是指向重量級鎖(互斥量)的指針,當前線程以及後面等待鎖的線程也要進入阻塞狀態。
輕量級鎖的釋放也是經過CAS操做來進行的,主要步驟以下:
- 經過CAS操做嘗試把線程中複製的Displaced Mark Word對象替換當前的Mark Word;
- 若是替換成功,整個同步過程就完成了,恢復到無鎖狀態(01);
- 若是替換失敗,說明有其餘線程嘗試過獲取該鎖(此時鎖已膨脹),那就要在釋放鎖的同時,喚醒被掛起的線程;
對於輕量級鎖,其性能提高的依據是 「對於絕大部分的鎖,在整個生命週期內都是不會存在競爭的」,若是打破這個依據則除了互斥的開銷外,還有額外的CAS操做,所以在有多線程競爭的狀況下,輕量級鎖比重量級鎖更慢。
輕量級鎖的獲取和釋放過程
爲何升級爲輕量鎖時要把對象頭裏的Mark Word複製到線程棧的鎖記錄中呢?
由於在申請對象鎖時 須要以該值做爲CAS的比較條件,同時在升級到重量級鎖的時候,能經過這個比較斷定是否在持有鎖的過程當中此鎖被其餘線程申請過,若是被其餘線程申請了,則在釋放鎖的時候要喚醒被掛起的線程。
爲何會嘗試CAS不成功以及什麼狀況下會不成功?
CAS自己是不帶鎖機制的,其是經過比較而來。假設以下場景:線程A和線程B都在對象頭裏的鎖標識爲無鎖狀態進入,那麼如線程A先更新對象頭爲其鎖記錄指針成功以後,線程B再用CAS去更新,就會發現此時的對象頭已經不是其操做前的對象HashCode了,因此CAS會失敗。也就是說,只有兩個線程併發申請鎖的時候會發生CAS失敗。
而後線程B進行CAS自旋,等待對象頭的鎖標識從新變回無鎖狀態或對象頭內容等於對象HashCode(由於這是線程B作CAS操做前的值),這也就意味着線程A執行結束(參見後面輕量級鎖的撤銷,只有線程A執行完畢撤銷鎖了纔會重置對象頭),此時線程B的CAS操做終於成功了,因而線程B得到了鎖以及執行同步代碼的權限。若是線程A的執行時間較長,線程B通過若干次CAS時鐘沒有成功,則鎖膨脹爲重量級鎖,即線程B被掛起阻塞、等待從新調度。
此處,如何理解「輕量級」?「輕量級」是相對於使用操做系統互斥量來實現的傳統鎖而言的。可是,首先須要強調一點的是,輕量級鎖並非用來代替重量級鎖的,它的本意是在沒有多線程競爭的前提下,減小傳統的重量級鎖使用產生的性能消耗。
輕量級鎖所適應的場景是線程交替執行同步塊的狀況,若是存在同一時間訪問同一鎖的狀況,必然就會致使輕量級鎖膨脹爲重量級鎖。
Synchronized是經過對象內部的一個叫作 監視器鎖(Monitor)來實現的。可是監視器鎖本質又是依賴於底層的操做系統的Mutex Lock來實現的。而操做系統實現線程之間的切換這就須要從用戶態轉換到核心態,這個成本很是高,狀態之間的轉換須要相對比較長的時間,這就是爲何Synchronized效率低的緣由。所以,這種依賴於操做系統Mutex Lock所實現的鎖咱們稱之爲 「重量級鎖」。
Synchronized偏向鎖、輕量級鎖及重量級鎖轉換流程
各類鎖並非相互代替的,而是在不一樣場景下的不一樣選擇,絕對不是說重量級鎖就是不合適的。每種鎖是隻能升級,不能降級,即由偏向鎖->輕量級鎖->重量級鎖,而這個過程就是開銷逐漸加大的過程。
- 若是是單線程使用,那偏向鎖毫無疑問代價最小,而且它就能解決問題,連CAS都不用作,僅僅在內存中比較下對象頭就能夠了;
- 若是出現了其餘線程競爭,則偏向鎖就會升級爲輕量級鎖;
- 若是其餘線程經過必定次數的CAS嘗試沒有成功,則進入重量級鎖;
在第3種狀況下進入同步代碼塊就 要作偏向鎖創建、偏向鎖撤銷、輕量級鎖創建、升級到重量級鎖,最終仍是得靠重量級鎖來解決問題,那這樣的代價就比直接用重量級鎖要大很多了。因此使用哪一種技術,必定要看其所處的環境及場景,在絕大多數的狀況下,偏向鎖是有效的,這是基於HotSpot做者發現的「大多數鎖只會由同一線程併發申請」的經驗規律。
Synchronized原理很是複雜,若是上面的問題不清楚,請前往閱讀:
《Java高併發核心編程(卷2):多線程、鎖、JMM、JUC、高併發設計模式》。
建立線程要花費昂貴的資源和時間,若是任務來了才建立線程那麼響應時間會變長,並且一個進程能建立的線程數有限。
多線程技術主要解決處理器單元內多個線程執行的問題,它能夠顯著減小處理器單元的閒置時間,增長處理器單元的吞吐能力。
假設一個服務器完成一項任務所需時間爲:T1 建立線程時間,T2 在線程中執行任務的時間,T3 銷燬線程時間。
若是:T1 + T3 遠大於 T2,則能夠採用線程池,以提升服務器性能。
一個線程池包括如下四個基本組成部分:
一、線程池管理器(ThreadPool):用於建立並管理線程池,包括 建立線程池,銷燬線程池,添加新任務;
二、工做線程(PoolWorker):線程池中線程,在沒有任務時處於等待狀態,能夠循環的執行任務;
三、任務接口(Task):每一個任務必須實現的接口,以供工做線程調度任務的執行,它主要規定了任務的入口,任務執行完後的收尾工做,任務的執行狀態等;
四、任務隊列(taskQueue):用於存放沒有處理的任務。提供一種緩衝機制。
線程池技術正是關注如何縮短或調整T1,T3時間的技術,從而提升服務器程序性能的。它把T1,T3分別安排在服務器程序的啓動和結束的時間段或者一些空閒的時間段,這樣在服務器程序處理客戶請求時,不會有T1,T3的開銷了。
線程池不只調整T1,T3產生的時間段,並且它還顯著減小了建立線程的數目,看一個例子:
假設一個服務器一天要處理50000個請求,而且每一個請求須要一個單獨的線程完成。在線程池中,線程數通常是固定的,因此產生線程總數不會超過線程池中線程的數目,而若是服務器不利用線程池來處理這些請求則線程總數爲50000。通常線程池大小是遠小於50000。因此利用線程池的服務器程序不會爲了建立50000而在處理請求時浪費時間,從而提升效率。
爲了不這些問題,在程序啓動的時候就建立若干線程來響應處理,它們被稱爲線程池,裏面的線程叫工做線程。從JDK1.5開始,Java API提供了Executor框架讓你能夠建立不一樣的線程池。好比單線程池,每次處理一個任務;數目固定的線程池或者是緩存線程池(一個適合不少生存期短的任務的程序的可擴展線程池)。
Executor框架同java.util.concurrent.Executor 接口在Java 5中被引入。Executor框架是一個根據一組執行策略調用,調度,執行和控制的異步任務的框架。
無限制的建立線程會引發應用程序內存溢出。因此建立一個線程池是個更好的的解決方案,由於能夠限制線程的數量而且能夠回收再利用這些線程。利用Executor框架能夠很是方便的建立一個線程池。
60) Executors類是什麼?
Executors爲Executor,ExecutorService,ScheduledExecutorService,ThreadFactory和Callable類提供了一些工具方法。
Executors能夠用於方便的建立線程池。
java.util.concurrent.BlockingQueue的特性是:當隊列是空的時,從隊列中獲取或刪除元素的操做將會被阻塞,或者當隊列是滿時,往隊列裏添加元素的操做會被阻塞。
阻塞隊列不接受空值,當你嘗試向隊列中添加空值的時候,它會拋出NullPointerException。
阻塞隊列的實現都是線程安全的,全部的查詢方法都是原子的而且使用了內部鎖或者其餘形式的併發控制。
BlockingQueue 接口是java collections框架的一部分,它主要用於實現生產者-消費者問題。
①newSingleThreadExecutor
單個線程的線程池,即線程池中每次只有一個線程工做,單線程串行執行任務
②newFixedThreadExecutor(n)
固定數量的線程池,沒提交一個任務就是一個線程,直到達到線程池的最大數量,而後後面進入等待隊列直到前面的任務完成才繼續執行
③newCacheThreadExecutor(推薦使用)
可緩存線程池,當線程池大小超過了處理任務所需的線程,那麼就會回收部分空閒(通常是60秒無執行)的線程,當有任務來時,又智能的添加新線程來執行。
④newScheduleThreadExecutor
大小無限制的線程池,支持定時和週期性的執行線程
java提供的線程池更增強大,相信理解線程池的工做原理,看類庫中的線程池就不會感到陌生了。
要配置一個線程池是比較複雜的,尤爲是對於線程池的原理不是很清楚的狀況下,頗有可能配置的線程池不是較優的,所以在Executors類裏面提供了一些靜態工廠,生成一些經常使用的線程池。
4.1 newSingleThreadExecutor
建立一個單線程的線程池。這個線程池只有一個線程在工做,也就是至關於單線程串行執行全部任務。若是這個惟一的線程由於異常結束,那麼會有一個新的線程來替代它。此線程池保證全部任務的執行順序按照任務的提交順序執行。
4.2 newFixedThreadPool
建立固定大小的線程池。每次提交一個任務就建立一個線程,直到線程達到線程池的最大大小。線程池的大小一旦達到最大值就會保持不變,若是某個線程由於執行異常而結束,那麼線程池會補充一個新線程。
4.3 newCachedThreadPool
建立一個可緩存的線程池。若是線程池的大小超過了處理任務所須要的線程,
那麼就會回收部分空閒(60秒不執行任務)的線程,當任務數增長時,此線程池又能夠智能的添加新線程來處理任務。此線程池不會對線程池大小作限制,線程池大小徹底依賴於操做系統(或者說JVM)可以建立的最大線程大小。
4.4 newScheduledThreadPool
建立一個大小無限的線程池。此線程池支持定時以及週期性執行任務的需求。
阿里巴巴Java開發手冊,明確指出不容許使用Executors靜態工廠構建線程池
緣由以下:
線程池不容許使用Executors去建立,而是經過ThreadPoolExecutor的方式,這樣的處理方式讓寫的同窗更加明確線程池的運行規則,規避資源耗盡的風險
說明:Executors返回的線程池對象的弊端以下:
1:FixedThreadPool 和 SingleThreadPool:
容許的請求隊列(底層實現是LinkedBlockingQueue)長度爲Integer.MAX_VALUE,可能會堆積大量的請求,從而致使OOM
2:CachedThreadPool 和 ScheduledThreadPool
容許的建立線程數量爲Integer.MAX_VALUE,可能會建立大量的線程,從而致使OOM。
建立線程池的正確姿式
避免使用Executors建立線程池,主要是避免使用其中的默認實現,那麼咱們能夠本身直接調用ThreadPoolExecutor的構造函數來本身建立線程池。在建立的同時,給BlockQueue指定容量就能夠了。
private static ExecutorService executor = new ThreadPoolExecutor(10, 10, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue(10));
或者是使用開源類庫:開源類庫,如apache和guava等。
/** * Creates a new {@code ThreadPoolExecutor} with the given initial * parameters. * * @param corePoolSize the number of threads to keep in the pool, even * if they are idle, unless {@code allowCoreThreadTimeOut} is set * @param maximumPoolSize the maximum number of threads to allow in the * pool * @param keepAliveTime when the number of threads is greater than * the core, this is the maximum time that excess idle threads * will wait for new tasks before terminating. * @param unit the time unit for the {@code keepAliveTime} argument * @param workQueue the queue to use for holding tasks before they are * executed. This queue will hold only the {@code Runnable} * tasks submitted by the {@code execute} method. * @param threadFactory the factory to use when the executor * creates a new thread * @param handler the handler to use when execution is blocked * because the thread bounds and queue capacities are reached * @throws IllegalArgumentException if one of the following holds:<br> * {@code corePoolSize < 0}<br> * {@code keepAliveTime < 0}<br> * {@code maximumPoolSize <= 0}<br> * {@code maximumPoolSize < corePoolSize} * @throws NullPointerException if {@code workQueue} * or {@code threadFactory} or {@code handler} is null */ public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { }
corePoolSize:核心線程數量,會一直存在,除非allowCoreThreadTimeOut設置爲true
maximumPoolSize:線程池容許的最大線程池數量
keepAliveTime:線程數量超過corePoolSize,空閒線程的最大超時時間
unit:超時時間的單位
workQueue:工做隊列,保存未執行的Runnable 任務
threadFactory:建立線程的工廠類
handler:當線程已滿,工做隊列也滿了的時候,會被調用。被用來實現各類拒絕策略。
關閉線程池能夠調用shutdownNow和shutdown兩個方法來實現
shutdownNow:對正在執行的任務所有發出interrupt(),中止執行,對還未開始執行的任務所有取消,而且返回還沒開始的任務列表。
shutdown:當咱們調用shutdown後,線程池將再也不接受新的任務,但也不會去強制終止已經提交或者正在執行中的任務。
若是任務是IO密集型,通常線程數須要設置2倍CPU數以上,以此來儘可能利用CPU資源。
若是任務是CPU密集型,通常線程數量只須要設置CPU數加1便可,更多的線程數也只能增長上下文切換,不能增長CPU利用率。
若是任務是混合型,有一個公式,具體請參考:《Java高併發核心編程(卷2)》
一、ArrayBlockingQueue
是一個基於數組結構的有界阻塞隊列,此隊列按 FIFO(先進先出)原則對元素進行排序。
二、LinkedBlockingQueue
一個基於鏈表結構的阻塞隊列,此隊列按FIFO (先進先出) 排序元素,吞吐量一般要高於ArrayBlockingQueue。靜態工廠方法Executors.newFixedThreadPool()使用了這個隊列
三、SynchronousQueue
一個不存儲元素的阻塞隊列。每一個插入操做必須等到另外一個線程調用移除操做,不然插入操做一直處於阻塞狀態,吞吐量一般要高於LinkedBlockingQueue,靜態工廠方法Executors.newCachedThreadPool使用了這個隊列。
四、PriorityBlockingQueue
一個具備優先級的無限阻塞隊列。
ThreadLocal是Java裏一種特殊的變量。每一個線程都有一個ThreadLocal就是每一個線程都擁有了本身獨立的一個變量,競爭條件被完全消除了。若是爲每一個線程提供一個本身獨有的變量拷貝,將大大提升效率。首先,經過複用減小了代價高昂的對象的建立個數。其次,你在沒有使用高代價的同步或者不變性的狀況下得到了線程安全。
討論ThreadLocal用在什麼地方前,咱們先明確下,若是僅僅就一個線程,那麼都不用談ThreadLocal的,ThreadLocal是用在多線程的場景的!!!
ThreadLocal概括下來就2類用途:
因爲ThreadLocal的特性,同一線程在某地方進行設置,在隨後的任意地方均可以獲取到。從而能夠用來保存線程上下文信息。
經常使用的好比每一個請求怎麼把一串後續關聯起來,就能夠用ThreadLocal進行set,在後續的任意須要記錄日誌的方法裏面進行get獲取到請求id,從而把整個請求串起來。
還有好比Spring的事務管理,用ThreadLocal存儲Connection,從而各個DAO能夠獲取同一Connection,能夠進行事務回滾,提交等操做。
備註: ThreadLocal的這種用處,不少時候是用在一些優秀的框架裏面的,通常咱們不多接觸,反而下面的場景咱們接觸的更多一些!
ThreadLocal爲解決多線程程序的併發問題提供了一種新的思路。可是ThreadLocal也有侷限性,咱們來看看阿里規範:
每一個線程往ThreadLocal中讀寫數據是線程隔離,互相之間不會影響的,因此ThreadLocal沒法解決共享對象的更新問題!
因爲不須要共享信息,天然就不存在競爭問題了,從而保證了某些狀況下線程的安全,以及避免了某些狀況須要考慮線程安全必須同步帶來的性能損失!!!
這類場景阿里規範裏面也提到了:
Thread類有屬性變量threadLocals (類型是ThreadLocal.ThreadLocalMap),也就是說每一個線程有一個本身的ThreadLocalMap ,因此每一個線程往這個ThreadLocal中讀寫隔離的,而且是互相不會影響的。
一個ThreadLocal只能存儲一個Object對象,若是須要存儲多個Object對象那麼就須要多個ThreadLocal!!!
如圖:
看到上面的幾個圖,大概思路應該都清晰了,咱們Entry的key指向ThreadLocal用虛線表示弱引用 ,下面咱們來看看ThreadLocalMap:
java對象的引用包括 : 強引用,軟引用,弱引用,虛引用 。
由於這裏涉及到弱引用,簡單說明下:
弱引用也是用來描述非必需對象的,當JVM進行垃圾回收時,不管內存是否充足,該對象僅僅被弱引用關聯,那麼就會被回收。
當僅僅只有ThreadLocalMap中的Entry的key指向ThreadLocal的時候,ThreadLocal會進行回收的!!!
ThreadLocal被垃圾回收後,在ThreadLocalMap裏對應的Entry的鍵值會變成null,可是Entry是強引用,那麼Entry裏面存儲的Object,並無辦法進行回收,因此ThreadLocalMap 作了一些額外的回收工做。
雖然作了可是也會存在內存泄漏風險(我沒有遇到過,網上不少相似場景,因此會提到後面的ThreadLocal最佳實踐!!!)
ThreadLocal被垃圾回收後,在ThreadLocalMap裏對應的Entry的鍵值會變成null,可是Entry是強引用,那麼Entry裏面存儲的Object,並無辦法進行回收,因此ThreadLocalMap 作了一些額外的回收工做。
備註: 不少時候,咱們都是用在線程池的場景,程序不中止,線程基本不會銷燬!!!
因爲線程的生命週期很長,若是咱們往ThreadLocal裏面set了很大很大的Object對象,雖然set、get等等方法在特定的條件會調用進行額外的清理,可是ThreadLocal被垃圾回收後,在ThreadLocalMap裏對應的Entry的鍵值會變成null,可是後續在也沒有操做set、get等方法了。
因此最佳實踐,應該在咱們不使用的時候,主動調用remove方法進行清理。
這裏把ThreadLocal定義爲static還有一個好處就是,因爲ThreadLocal有強引用在,那麼在ThreadLocalMap裏對應的Entry的鍵會永遠存在,那麼執行remove的時候就能夠正確進行定位到而且刪除!!!
最佳實踐作法應該爲:
try { // 其它業務邏輯 } finally { threadLocal對象.remove(); }
答案:100個5050