java併發包和類總結-JUC總結

 

java併發包和類總結-JUC總結

 

多線程課程
JUC課程
實戰Java高併發
Java併發編程的藝術

多線程

  1. 程序:是一個靜態的概念,一般對應於操作系統中的一個可執行文件。一組指令的集合。
  2. 進程:是一個動態的概念,執行中的程序叫做進程。1.進程是程序的一次動態執行過程, 佔用特定的地址空間。2.每個進程由3部分組成:cpu、data、code。每個進程都是獨立的,保有自己的cpu時間,代碼和數據,即便用同一份程序產生好幾個進程,它們之間還是擁有自己的這3樣東西,這樣的缺點是:浪費內存,cpu的負擔較重。3.多任務(Multitasking)操作系統將CPU時間動態地劃分給每個進程,操作系統同時執行多個進程,每個進程獨立運行。以進程的觀點來看,它會以爲自己獨佔CPU的使用權。其實這是並行執行。
  3. 線程:一個進程可以產生多個線程。同多個進程可以共享操作系統的某些資源一樣,同一進程的多個線程也可以共享此進程的某些資源(比如:代碼、數據),所以線程又被稱爲輕量級進程(lightweight process)。1.一個進程內部的一個執行單元,它是程序中的一個單一的順序控制流程。2.一個進程可擁有多個並行的(concurrent)線程。3.一個進程中的多個線程共享相同的內存單元/內存地址空間,可以訪問相同的變量和對象,而且它們從同一堆中分配對象並進行通信、數據交換和同步操作。線程都擁有各自的計數器、堆棧和局部變量。4.由於線程間的通信是在同一地址空間上進行的,所以不需要額外的通信機制,這就使得通信更簡便而且信息傳遞的速度也更快。5.線程的啓動、中斷、消亡,消耗的資源非常少。
  4. 進程和線程的區別:1.每個進程都有獨立的代碼和數據空間(進程上下文),進程間的切換會有較大的開銷。
    2.線程可以看成是輕量級的進程,屬於同一進程的線程共享代碼和數據空間,每個線程有獨立的運行棧和程序計數器(PC),線程切換的開銷小。
    3.線程和進程最根本的區別在於:進程是資源分配的單位,線程是調度和執行的單位。
    4.多進程: 在操作系統中能同時運行多個任務(程序)。
    5.多線程: 在同一應用程序中有多個順序流同時執行。
    6.線程是進程的一部分,所以線程有的時候被稱爲輕量級進程。
    7.一個沒有線程的進程是可以被看作單線程的,如果一個進程內擁有多個線程,進程的執行過程不是一條線(線程)的,而是多條線(線程)共同完成的。
    8.系統在運行的時候會爲每個進程分配不同的內存區域,但是不會爲線程分配內存(線程所使用的資源是它所屬的進程的資源),線程組只能共享資源。那就是說,除了CPU之外(線程在運行的時候要佔用CPU資源),計算機內部的軟硬件資源的分配與線程無關,線程只能共享它所屬進程的資源。
  5. 進程與程序的區別:一個進程肯定與一個程序相對應,並且只有一個,但是一個程序可以有多個進程,或者一個進程都沒有。除此之外,進程還有併發性和交往性。簡單地說,進程是程序的一部分,程序運行的時候會產生進程。
  6. 創建線程的三種方式:繼承Thread類、實現Runnable接口、實現Callable接口。通過線程池創建。
  7. 創建:繼承Thread+重寫run,啓動: 創建子類對象 + start
  8. 執行線程必須調用start(),加入到調度器中,不一定立即執行,系統安排調度分配執行;開啓線程後,當方法執行時,調用run()代碼將被執行。run()方法是線程體的入口點,線程執行的代碼;直接調用run()方法不是開啓多線程,是普通的方法調用,此時run()方法自己會去調用start()方法。
  9. 創建:實現Runnable+重寫run啓動: 創建實現類對象 +Thread代理類對象+ start(啓動線程還是要借用Thread類,只有 thread才具有和cpu直接 打交道的能力,即啓動start())。我們使用實現runnable接口重寫run方法啓動線程時,必須借用Thread對象,這個對象就是代理對象:就是真實角色和代理角色都實現同一個接口,代理角色中含有真實角色的引用,可以調用真實角色的操作。
  10. Thread類的構造方法
    Thread() Thread(Runnable target) Thread(Runnable target,String name) Thread(String name)
    Thread(ThreadGroup group,Runnable target) Thread(ThreadGroup group,Runnable target,String name)
    方法:start():導致此線程開始執行,Java虛擬機調用此線程的run()方法
    run():如果這個線程是使用單獨的Runnable運行對象構造的,那麼這個Runnable對象的run()方法被調用;否則此方法不執行任何操作並返回
  11. Runnale對同一個資源可以有多個代理,共享資源,併發(要保證線程安全,後期會講)。
  12. 創建:實現Callable接口+重寫call()方法,run()方法不能拋出異常和沒有返回值,而call()方法可以拋出異常和有返回值。需要藉助服務、線程池。創建目標對象:創建執行服務: 提交執行: 獲取結果:關閉服務: 。
  13. Thread的構造方法要的是runnable,有的是callable,如何把它們之間加上關係:runnable實現了FutureTask,而FutureTask的構造方法中需要callable。FutureTask futuretask=new FutureTask<>(new MyThread());Thread t=new Thread(futuretask,”AA”);t.start();適配模式。多個線程都要使用callable,每次都要返回一個成功或者失敗的返回值。futuretask.get();建議放在最後因爲我們不會等這個線程,給它充足的時間去計算。如果把get放到前面,mian線程就被堵住了。以這裏可以加一個循環的判斷!等算完了,才往下做。兩個線程都開始做同一個任務,只會執行一次!即複用。如果說非要進去!那麼就要啓動多個futuretask。
    在主線程中需要執行比較耗時的操作時,但又不想阻塞主線程時,可以把這些作業交給Future對象在後臺完成,
    當主線程將來需要時,就可以通過Future對象獲得後臺作業的計算結果或者執行狀態。

一般FutureTask多用於耗時的計算,主線程可以在完成自己的任務後,再去獲取結果。

僅在計算完成時才能檢索結果;如果計算尚未完成,則阻塞 get 方法。一旦計算完成,就不能再重新開始或取消計算。get方法而獲取結果只有在計算完成時獲取,否則會一直阻塞直到任務轉入完成狀態,然後會返回結果或者拋出異常。

  1. 函數式編程:與面向對象的比較:
  2. Lamda表達式:函數式 編程。避免匿名內部類定義過多。
    拷貝中括號+寫死右箭頭+落地大括號。一個接口裏面有且僅有一個方法的接口(纔是函數式接口),纔可以使用Lambda Express。
    (params)->expression (params)->statements (params)->{statements}
    如:new Thread({}->System.out.println(“多線程學習”)).start();
    用的線程比較少,只關注於線程體。
    Lambda表達式 簡化線程(用一次)的使用。
    靜態內部類,隨着外部類的加載而加載;局部內部類,把類丟到方法內部來;匿名內部類 必須藉助接口或者父類;jdk8 簡化 lambda表達式 只需要關注線程體,刪掉了接口名刪掉了方法名,只需要關注你傳什麼參數,實現什麼方法
  3. lambda推導,沒有參數,沒有返回值,lambda推導必須存在類型。類型(變量名)=()->{表達式};
  4. lambda推導 +參數,只要把方法 拷貝過來,不需要方法名;類型也可以拿掉;括號也省略;只有一行代碼,花括號也省略。參數->表達式
  5. lambda推導 +參數+返回值,去掉類型,多個參數括號不能省略;假設只有一行代碼,那麼可以省掉return。(參數1,參數2)->表達式
  6. 線程狀態:新生狀態NEW、start()就緒狀態、獲得/失去執行權運行狀態RUNNABLEyield()、run()方法結束死亡狀態TERMINATED、阻塞狀態(從運行狀態wait()等待阻塞WAITING、TIME WAITINGsynchronized同步阻塞BLOCKED、sleep()/join()/IO流阻塞其他阻塞;等待阻塞notify()同步阻塞、同步阻塞鎖可用【拿到對象的鎖標記】就緒狀態、其他阻塞sleep()休眠時間到/join()聯合線程執行完畢/IO流阻塞結束就緒狀態)

當線程執行wait()方法之後,線程進入等待狀態。進入等待狀態的線程需要依靠其他線程的通知才能夠返回到運行狀態,而超時等待狀態相當於在等待狀態的基礎上增加了超時限制,也就是超時時間到達時將會返回到運行狀態。當線程調用同步方法時,在沒有獲取到鎖的情況下,線程將會進入到阻塞狀態。
20. 新生狀態(New):用new關鍵字建立一個線程對象後,該線程對象就處於新生狀態。處於新生狀態的線程有自己的內存空間,new 線程有了自己的工作空間,一個工作空間針對一個線程。
線程對象在構造的時候需要提供線程所需要的屬性,如線程所屬的線程組、線程優先級、是否是Daemon線程等信息。一個新構造的線程對象是由其parent線程來進行空間分配的,而child線程繼承了parent是否爲Daemon、優先級和加載資源的contextClassLoader以及可繼承的ThreadLocal,同時還會分配一個唯一的ID來標識這個child線程。至此,一個能夠運行的線程對象就初始化好了,在堆內存中等待着運行。

  1. 就緒狀態(Runnable):具備了運行條件,還沒有分配到CPU,處於“線程就緒隊列”,等待系統爲其分配CPU。(有四種方法可以進入就緒狀態1start()2阻塞解除3yield()4 JVM將CPU資源從本線程切換到其他線程。)
    線程start()方法的含義是:當前線程(即parent線程)同步告知Java虛擬機,只要線程規劃器空閒,應立即啓動調用start()方法的線程。
  2. 運行狀態(Running):在運行狀態的線程執行自己run方法中的代碼。直到調用其他方法而終止或等待某資源而阻塞或完成任務而死亡。如果在給定的時間片內沒有執行結束,就會被系統給換下來回到就緒狀態。也可能由於某些“導致阻塞的事件”而進入阻塞狀態。
  3. 阻塞狀態(Blocked):阻塞指的是暫停一個線程的執行以等待某個條件發生(如某資源就緒)。(有四種方法可以進入阻塞狀態1sleep(int millsecond)2wait()-notify()3join()-另一個線程執行完4read()write())
  4. 死亡狀態(Terminated)1正常終止run()方法運行完畢2線程被強制終止stop() 或destory()當一個線程進入死亡狀態以後,就不能再回到其它狀態了。
  5. 線程方法:
    sleep()使線程停止運行一段時間,將處於阻塞狀態。如果調用了sleep()方法後,沒有其他等待執行的線程,這個時候當前線程不會馬上恢復執行。
    join()阻塞指定線程等到另一個線程完成以後再繼續執行。
    yield()讓當前正在執行線程暫停,不是阻塞線程,而是將線程轉入就緒狀態;調用以後,如果沒有其他等待執行的線程,此時當前線程馬上就會恢復執行
    setDaemon()可以將指定的線程設置成後臺線程,守護線程;創建用戶線程的線程結束時,後臺線程也將隨之消亡;只能在線程啓動之前把它設爲後臺線程
    setPriority(int newPriority) getPriority()線程的優先級代表的是概率 範圍從1到10,默認爲5
    stop()不推薦使用
  6. 終止線程的典型方式:終止線程我們一般不使用JDK提供的stop()/destroy()方法(它們本身也被JDK廢棄了)。通常的做法是使用中斷狀態interrupt()或者提供一個boolean型的終止變量,當這個變量置爲false,則終止線程的運行。1.線程類中定義線程體使用的標識2.線程體使用該標識3.對外提供方法改變標識

暫停、恢復和停止操作對應在線程Thread的API就是suspend()、resume()和stop()。這都是過期方法不建議使用,以suspend()方法爲例,在調用後,線程不會釋放已經佔有的資源(比如鎖),而是佔有着資源進入睡眠狀態,這樣容易引發死鎖問題。同樣,stop()方法在終結一個線程時不會保證線程的資源正常釋放,通常是沒有給予線程完成資源釋放工作的機會,因此會導致程序可能工作在不確定狀態下。
27. 暫停線程執行:sleep(時間)指定當前線程阻塞的毫秒數;存在異常InterruptedException;時間達到後線程進入就緒狀態;可以模擬網絡延時、倒計時等;每個對象都有一個鎖,sleep()不會釋放鎖:特點是抱着鎖睡覺,站在馬路中間誰都過不去。
sleep()方法與對象沒關係,誰執行這個線程體,誰就去執行sleep()操作
28. 暫停線程:yield(),可以引起線程切換,但運行時沒有明顯延遲。禮讓線程,讓當前正在執行的線程暫停;不是阻塞線程,而是將線程從運行狀態轉入就緒狀態,讓CPU調度器重新調度,可能禮讓成功,也可能又重回來調用自己了。即有時候禮讓成功,有時候禮讓不成功。
29. 暫停線程執行常用的方法有sleep()和yield()方法,這兩個方法的區別是:
1.sleep()方法:可以讓正在運行的線程進入阻塞狀態,直到休眠時間滿了,進入就緒狀態。
2.yield()方法:可以讓正在運行的線程直接進入就緒狀態,讓出CPU的使用權。
30. 線程的聯合:join(),合併線程,待此線程執行完成後,再執行其他線程,其他線程阻塞。又叫做插隊線程,一個車插到別的車前面,別的車就得等插隊車走後才能走,並且別的車進入阻塞狀態。join用於讓當前執行線程等待join線程執行結束。其實現原理是不停檢查join線程是否存活,如果join線程存活則讓當前線程永遠等待。其中,wait(0)表示永遠等待下去。直到join線程中止後,線程的this.notifyAll()方法會被調用,調用notifyAll()方法是在JVM裏實現的。CountDownLatch也可以實現join的功能,並且比join的功能更多。join()寫在誰A的run方法體中就阻塞誰,誰去調用join()誰B就去插隊。
線程A在運行期間,可以線程B調用的join()方法,讓線程B和線程A聯合。這樣,線程A就必須等待線程B執行完畢後,才能繼續執行。“爸爸線程A”要抽菸,於是聯合了“兒子線程B”去買菸,必須等待“兒子線程B”買菸完畢,“爸爸線程A”才能繼續抽菸。
如果一個線程A執行了thread.join()語句,其含義是:當前線程A等待thread線程終止之後才從thread.join()返回。線程Thread除了提供join()方法之外,還提供了join(long millis)和join(long millis,int nanos)兩個具備超時特性的方法。這兩個超時方法表示,如果線程thread在給定的超時時間裏沒有終止,那麼將會從該超時方法中返回。

  1. 線程優先級priority:Java提供一個線程調度器來監控程序中啓動後進入就緒狀態的所有線程。線程調度器按照線程的優先級決定應該調度哪個線程來執行。線程優先級用數字來表示,範圍從1到10。Thread.MIN_PRIORITY=1;Thread.MAX_PRIORITY =10;Thread.NORM_PRIORITY=5;默認爲5。使用以下方法獲得或設置線程對象的優先級:
    int getPriority()void setpriority(int newPriority)優先級的設定建議在start()調用前。優先級低只是意味着獲得調度的概率低,並不是絕對先調用優先級高後調用優先級低的線程。線程優先級不能作爲程序正確性的依賴,因爲操作系統可以完全不用理會Java線程對於優先級的設定。

優先級高的線程分配時間片的數量要多於優先級低的線程。設置線程優先級時,針對頻繁阻塞(休眠或者I/O操作)的線程需要設置較高優先級,而偏重計算(需要較多CPU時間或者偏運算)的線程則設置較低的優先級,確保處理器不會被獨佔。
32. 獲取線程基本信息的方法:isAlive()setName()getName()currentThread()
33. 線程組:ThreadGroup th=new ThreadGroup(“pg”);Thread t1=new Thread(th,new ThreadGroupName(),”T1”);使用構造函數指定線程所屬的線程組th.activeCount()獲得活動線程的總數 th.list()獲得整個線程組中所有線程的信息
34. 守護線程:線程分爲用戶線程和守護線程;Daemon線程是一種支持型線程,因爲它主要被用作程序中後臺調度以及支持性工作。這意味着,當一個Java虛擬機中不存在非Daemon線程的時候,Java虛擬機將會退出。虛擬機必須確保用戶線程執行完畢;虛擬機不用等待守護線程執行完畢;如後臺記錄操作日誌,監控內存使用等等。Thread.setDaemon(true)將線程設置爲守護線程。Daemon屬性需要在啓動線程之前設置,不能在啓動線程之後設置。

  1. Daemon線程被用作完成支持性工作,但是在Java虛擬機退出時Daemon線程中的finally塊並不一定會執行,(main線程(非Daemon線程)在啓動了線程DaemonRunner之後隨着main方法執行完畢而終止,而此時Java虛擬機中已經沒有非Daemon線程,虛擬機需要退出。Java虛擬機中的所有Daemon線程都需要立即終止,因此DaemonRunner立即終止,但是DaemonRunner中的finally塊並沒有執行。)在構建Daemon線程時,不能依靠finally塊中的內容來確保執行關閉或清理資源的邏輯。

  2. 線程中斷:中斷可以理解爲線程的一個標識位屬性,它表示一個運行中的線程是否被其他線程進行了中斷操作。中斷好比其他線程對該線程打了個招呼,其他線程通過調用該線程的interrupt()方法對其進行中斷操作。線程通過檢查自身是否被中斷來進行響應,線程通過方法isInterrupted()來進行判斷是否被中斷,也可以調用靜態方法Thread.interrupted()對當前線程的中斷標識位進行復位。如果該線程已經處於終結狀態,即使該線程被中斷過,在調用該線程對象的isInterrupted()時依舊會返回false。
    從Java的API中可以看到,許多聲明拋出InterruptedException的方法(例如Thread.sleep(long millis)方法)這些方法在拋出InterruptedException之前,Java虛擬機會先將該線程的中斷標識位清除,然後拋出InterruptedException,此時調用isInterrupted()方法將會返回false。

  3. 併發:同一個對象多個線程同時操作。(同時操作同一個賬戶;同時購買同一車次的票;操作容器)《比如說:多個代理同時去訪問:有負數的情況、有相同的情況。分析:負數:臨界值沒有控制 相同的值:拷貝10的時候已經都拿到自己的工作臺。》

  4. 線程同步:處理多線程問題時,多個線程訪問同一個對象,並且某些線程還想修改這個對象。這時候,我們就需要用到線程同步。線程同步其實就是一種等待機制,多個需要同時訪問此對象的線程進入這個對象的等待池形成隊列,等待前面的線程使用完畢後,下一個線程再使用。

  5. 鎖機制(synchronized):由於同一進程的多個線程共享同一塊存儲空間,在帶來方便的同時,也帶來了訪問衝突的問題。爲了保證數據在方法中被訪問時的正確性,在訪問時加入了鎖機制(synchronized),當一個線程獲得對象的排他鎖,獨佔資源,其他線程必須等待,使用後釋放鎖即可,存在以下問題:
    1.一個線程持有鎖會導致其他所有需要此鎖的線程掛起。
    2.在多線程競爭下,加鎖、釋放鎖會導致比較多的上下文切換和調度延時,引起性能問題。
    3.如果一個優先級高的線程等待一個優先級低的線程釋放鎖會導致優先級倒置,引起性能問題。

  6. synchronized關鍵字:由於我們可以通過private關鍵字來保證數據對象只能被方法訪問,所以我們只需針對方法提出一套機制,這套機制就是
    synchronized關鍵字,它包括兩種用法:synchronized方法和synchronized塊。

  7. 同步方法:public synchonized void method(int args){}
    synchronized方法控制對"成員變量|類變量"對象的訪問:每個對象對應一把鎖,每個synchronized方法都必須獲得調用該方法的對象的鎖方能執行,否則所屬線程阻塞。方法一旦執行,就獨佔該鎖,直到從該方法返回時纔將鎖釋放,此後被阻塞的線程方法方能獲得該鎖,重新進入可執行狀態。缺陷:若將一個大的方法聲明爲synchonized將會大大影響效率。
    synchronized操作了什麼,鎖了對象的資源、而不是鎖方法,鎖是當前實例對象。而靜態同步方法,鎖是當前類的Class對象。

1 標準訪問,請問先打印短信還是email?(兩個線程同時訪問同一個資源,誰先誰後是不一定的;在兩個線程之間加上Thread.sleep(100);就能確定先後了)
也就是說如果一個實例對象的非靜態同步方法獲取鎖後,該實例對象的其他非靜態同步方法必須等待獲取鎖的方法釋放鎖後才能獲取鎖,鎖的是當前對象this,被鎖定後,其它的線程都不能進入到當前對象的其它的synchronized方法
所有的非靜態同步方法用的都是同一把鎖——實例對象本身,
2 sendSMS()睡覺4秒鐘,請問先打印短信還是email?(剛開始等了4秒,然後先短信後email)(同一個同學同一時刻只能使用手機的一個功能!)
也就是說如果一個實例對象的非靜態同步方法獲取鎖後,該實例對象的其他非靜態同步方法必須等待獲取鎖的方法釋放鎖後才能獲取鎖,鎖的是當前對象this,被鎖定後,其它的線程都不能進入到當前對象的其它的synchronized方法
所有的非靜態同步方法用的都是同一把鎖——實例對象本身,
3 新增普通方法openPC,請問先打印短信還是openPhone?(先開機【因爲這個沒有同步方法啊】,等了4秒,然後打印短信)
加個普通方法後發現和同步鎖無關
4 有兩部手機,請問先打印短信還是email?(先打印郵件,等了4秒,然後打印短信)
可是別的實例對象的非靜態同步方法因爲跟該實例對象的非靜態同步方法用的是不同的鎖,所以毋須等待該實例對象已獲取鎖的非靜態同步方法釋放鎖就可以獲取他們自己的鎖。
換成兩個對象後,不是同一把鎖了,情況立刻變化。
所有的 非靜態同步方法用的都是同一把鎖——實例對象本身,
5 兩個靜態同步方法,同一部手機,請問先打印短信還是email?(等了4秒,先打印短信,然後打印郵件)
6 兩個靜態同步方法,2部手機,請問先打印短信還是email?(等了4秒,先打印短信,然後打印郵件)
但是一旦一個靜態同步方法獲取鎖後,其他的靜態同步方法都必須等待該方法釋放鎖後才能獲取鎖,而不管是同一個實例對象的靜態同步方法之間,還是不同的實例對象的靜態同步方法之間,只要它們同一個類的實例對象!
所有的靜態同步方法用的也是同一把鎖——類對象本身,
7 1個靜態同步方法,1個普通同步方法,同一部手機,請問先打印短信還是email?(先打印郵件,等了4秒,打印短信)
8 1個靜態同步方法,1個普通同步方法,2部手機,請問先打印短信還是email?(先打印郵件,等了4秒,打印短信)
這兩把鎖是兩個不同的對象,所以靜態同步方法與非靜態同步方法之間是不會有競態條件的。

一個對象裏面如果有多個synchronized方法,某一個時刻內,只要一個線程去調用其中的一個synchronized方法了,
其它的線程都只能等待,換句話說,某一個時刻內,只能有唯一一個線程去訪問這些synchronized方法
鎖的是當前對象this,被鎖定後,其它的線程都不能進入到當前對象的其它的synchronized方法

加個普通方法後發現和同步鎖無關
換成兩個對象後,不是同一把鎖了,情況立刻變化。

都換成靜態同步方法後,情況又變化
所有的非靜態同步方法用的都是同一把鎖——實例對象本身,

也就是說如果一個實例對象的非靜態同步方法獲取鎖後,該實例對象的其他非靜態同步方法必須等待獲取鎖的方法釋放鎖後才能獲取鎖,
可是別的實例對象的非靜態同步方法因爲跟該實例對象的非靜態同步方法用的是不同的鎖,
所以毋須等待該實例對象已獲取鎖的非靜態同步方法釋放鎖就可以獲取他們自己的鎖。

所有的靜態同步方法用的也是同一把鎖——類對象本身,
這兩把鎖是兩個不同的對象,所以靜態同步方法與非靜態同步方法之間是不會有競態條件的。
但是一旦一個靜態同步方法獲取鎖後,其他的靜態同步方法都必須等待該方法釋放鎖後才能獲取鎖,而不管是同一個實例對象的靜態同步方法之間,
還是不同的實例對象的靜態同步方法之間,只要它們同一個類的實例對象!
42. 同步塊:synchronized(obj){},obj稱之爲同步監視器。
obj可以是任何對象,但是推薦使用共享資源作爲同步監視器。
同步方法中無需執行同步監視器,因爲同步方法的同步監視器是this即該對象本身,或class即類的模子。
同步監視器的執行過程:
第一個線程訪問,鎖定同步監視器,執行其中代碼
第二個線程訪問,發現同步監視器被鎖定,無法訪問
第一個線程訪問完畢,解鎖同步監視器
第二個線程訪問,發現同步監視器未鎖,鎖定並訪問。

方法裏面的塊:局部塊
構造塊:對象的信息
靜態塊:類的信息
同步塊:
43. Synchronized的實現原理:JVM基於進入和退出Monitor對象來實現方法同步和代碼塊同步。monitorenter指令是在編譯後插入到同步代碼塊的開始位置,而monitorexit是插入到方法結束處和異常處,JVM要保證每個monitorenter必須有對應的monitorexit與之配對。任何對象都有一個monitor與之關聯,當且一個monitor被持有後,它將處於鎖定狀態。線程執行到monitorenter指令時,將會嘗試獲取對象所對應的monitor的所有權,即嘗試獲得對象的鎖。對於同步塊的實現使用了monitorenter和monitorexit指令,而同步方法則是依靠方法修飾符上的ACC_SYNCHRONIZED來完成的。無論採用哪種方式,其本質是對一個對象的監視器(monitor)進行獲取,而這個獲取過程是排他的,也就是同一時刻只能有一個線程獲取到由synchronized所保護對象的監視器。任意一個對象都擁有自己的監視器,當這個對象由同步塊或者這個對象的同步方法調用時,執行方法的線程必須先獲取到該對象的監視器才能進入同步塊或者同步方法,而沒有獲取到監視器(執行該方法)的線程將會被阻塞在同步塊和同步方法的入口處,進入BLOCKED狀態。

當線程釋放鎖時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存中。當線程獲取鎖時,JMM會把該線程對應的本地內存置爲無效。從而使得被監視器保護的臨界區代碼必須從主內存中讀取共享變量。

鎖釋放與volatile寫有相同的內存語義;鎖獲取與volatile讀有相同的內存語義。

線程A釋放一個鎖,實質上是線程A向接下來將要獲取這個鎖的某個線程發出了(線程A對共享變量所做修改的)消息。•線程B獲取一個鎖,實質上是線程B接收了之前某個線程發出的(在釋放這個鎖之前對共享變量所做修改的)消息。線程A釋放鎖,隨後線程B獲取這個鎖,這個過程實質上是線程A通過主內存向線程B發送消息。

  1. 無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態
  2. 併發容器:CopyOnWriteArrayList
  3. 死鎖:多個線程各自佔有一些共享資源,並且互相等待其他線程佔有的資源才能進行,而導致兩個或多個線程都在等待對方釋放資源,都停止執行的情形。某一個同步代碼塊同時擁有"兩個以上對象的鎖"時,就可能會發生死鎖問題。

死鎖: 過多的同步可能造成相互不釋放資源。從而相互等待,一般發生於同步中持有多個對象的鎖
避免: 不要在同一個代碼塊中,同時持有多個對象的鎖解決方式:後一種 往外挪一下,不要鎖套鎖
47. 線程通信: volatile修飾成員變量,保證對線程的可見性。Synchronized修飾方法或塊,保證多個線程在同一時刻只能有一個線程處於方法或塊中,保證了線程對變量訪問的可見性和排他性。
應用場景-生產者和消費者問題
假設倉庫中只能存放一件產品,生產者將生產出來的產品放入倉庫,消費者將倉庫中的產品取走消費。
如果倉庫中沒有產品,則生產者將產品放入倉庫,否則停止生產並等待,直到倉庫中的產品被消費者取走爲止。
如果倉庫中放有產品,則消費者可以將產品取走消費,否則停止消費並等待,直到倉庫中再次放入產品爲止。
分析:這是一個線程同步問題,生產者和消費者共享同一個資源,並且生產者和消費者之間相互依賴互爲條件。
對於生產者,沒有生產產品之前,要通知消費者等待。而生產了產品之後,又要馬上通知消費者消費。
對於消費者,在消費之後,要通知生產者已經消費結束,需要繼續生產新的產品以供消費。
48. 在生產者消費者問題中,僅有synchronized是不夠的:
synchronized可以阻止併發更新同一個共享資源,實現了同步。
synchronized不能用來實現不同線程之間的消息傳遞(通信)
49. 解決方法1:併發協作模型"生產者/消費者模式"–》管程法:生產者:負責生產數據的模塊(這裏模塊可能是:方法、對象、線程、進程)消費者:負責處理數據的模塊(這裏模塊可能是:方法、對象、線程、進程) 緩衝區:消費者不能直接使用生產者的數據,他們都之間有個緩衝區。生產者將生產好的數據放入緩衝區,消費者從緩衝區中拿走要處理的數據。《根據容器進行交流的》緩衝區是實現併發的核心,緩衝區的設置有3個好處:
Ø 實現線程的併發協作
有了緩衝區以後,生產者線程只需要往緩衝區裏面放置數據,而不需要管消費者消費的情況;同樣,消費者只需要從緩衝區拿數據處理即可,也不需要管生產者生產的情況。 這樣,就從邏輯上實現了“生產者線程”和“消費者線程”的分離。
Ø 解耦了生產者和消費者
生產者不需要和消費者直接打交道。
Ø 解決忙閒不均,提高效率
生產者生產數據慢時,緩衝區仍有數據,不影響消費者消費;消費者處理數據慢時,生產者仍然可以繼續往緩衝區裏面放置數據 。
50. 解決方法2:併發協作模型"生產者/消費者模式"–》信號燈法:藉助標誌位
51. Java提供了三個方法解決線程之間的通信問題:等待/通知機制,是指一個線程A調用了對象O的wait()方法進入等待狀態,而另一個線程B調用了對象O的notify()或者notifyAll()方法,線程A收到通知後從對象O的wait()方法返回,進而執行後續操作。上述兩個線程通過對象O來完成交互,而對象上的wait()和notify/notifyAll()的關係就如同開關信號一樣,用來完成等待方和通知方之間的交互工作。
wait()表示線程一直等待,直到其他線程通知,與sleep不同,會釋放鎖
wait(long timeout)指定等待的毫秒數
notify()喚醒一個處於等待狀態的線程
notifyAll()喚醒同一個對象上所有調用
wait()方法的線程,優先級別高的線程優先調度
以上方法均是java.lang.Object類的方法;都只能在同步方法或者同步代碼塊中使用,否則會拋出異常

1) 使用wait()、notify()和notifyAll()時需要先對調用對象加鎖。
2)調用wait()方法後,線程狀態由RUNNING變爲WAITING,並將當前線程放置到對象的等待隊列。
3)notify()或notifyAll()方法調用後,等待線程依舊不會從wait()返回,需要調用notify()或notifAll()的線程釋放鎖之後,等待線程纔有機會從wait()返回。
4)notify()方法將等待隊列中的一個等待線程從等待隊列中移到同步隊列中,而notifyAll()方法則是將等待隊列中所有的線程全部移到同步隊列,被移動的線程狀態由WAITING變爲BLOCKED。
5)從wait()方法返回的前提是獲得了調用對象的鎖。
從上述細節中可以看到,等待/通知機制依託於同步機制,其目的就是確保等待線程從wait()方法返回時能夠感知到通知線程對變量做出的修改。WaitThread首先獲取了對象的鎖,然後調用對象的wait()方法,從而放棄了鎖並進入了對象的等待隊列WaitQueue中,進入等待狀態。由於WaitThread釋放了對象的鎖,NotifyThread隨後獲取了對象的鎖,並調用對象的notify()方法,將WaitThread從WaitQueue移到SynchronizedQueue中,此時WaitThread的狀態變爲阻塞狀態。NotifyThread釋放了鎖之後,WaitThread再次獲取到鎖並從wait()方法返回繼續執行。

2) 等待/通知的經典範式:
等待方(消費者)遵循如下原則。1)獲取對象的鎖。2)如果條件不滿足,那麼調用對象的wait()方法,被通知後仍要檢查條件。3)條件滿足則執行對應的邏輯。synchronized(對象) { while(條件不滿足) { 對象.wait(); } 對應的處理邏輯 }
通知方遵循如下原則。1)獲得對象的鎖。2)改變條件。3)通知所有等待在對象上的線程。synchronized(對象) { 改變條件 對象.notifyAll(); }

3) 等待超時模式:假設超時時間段是T,那麼可以推斷出在當前時間now+T之後就會超時。這時僅需要wait(T)即可,在wait(T)返回之後會將執行T=FUTURE–now。如果T小於等於0,表示已經超時,直接退出,否則將繼續執行wait(T)。可以看出,等待超時模式就是在等待/通知範式基礎上增加了超時控制,這使得該模式相比原有範式更具有靈活性,因爲即使方法執行時間過長,也不會“永久”阻塞調用者,而是會按照調用者的要求“按時”返回。

4) 使用等待超時模式來構造一個簡單的數據庫連接池,在示例中模擬從連接池中獲取、使用和釋放連接的過程,而客戶端獲取連接的過程被設定爲等待超時的模式,也就是在1000毫秒內如果無法獲取到可用連接,將會返回給客戶端一個null。設定連接池的大小爲10個,然後通過調節客戶端的線程數來模擬無法獲取連接的場景。

  1. java.util.Timer類:Timer類作用是類似鬧鐘的功能,也就是定時或者每隔一定時間觸發一次線程。其實,Timer類本身實現的就是一個線程,只是這個線程是用來實現調用其它線程的。t1.schedule(task1,3000); //3秒後執行;t1.schedule(task1,5000,1000);//5秒以後每隔1秒執行一次!t1.schedule(task1,calendar1.getTime()); //指定時間定時執行;構造方法
    Timer() Timer(boolean isDaemon) Timer(String name) Timer(String name,boolean isDaemon)
    方法
    cancel() purge() schedule(TimerTask task,long delay) schedule(TimerTask task,long delay,long period)
    schedule(TimerTask task,Date time) schedule(TimerTask task,Date firsttime,long period)

  2. java.util.TimerTask類:TimerTask類是一個抽象類,該類實現了Runnable接口,所以該類具備多線程的能力。在這種實現方式中,通過繼承TimerTask使該類獲得多線程的能力,將需要多線程執行的代碼書寫在run方法內部,然後通過Timer類啓動線程的執行。在實際使用時,一個Timer可以啓動任意多個TimerTask實現的線程,但是多個線程之間會存在阻塞。所以如果多個線程之間需要完全獨立的話,最好還是一個Timer啓動一個TimerTask實現。構造方法
    TimeTask()
    方法
    cancel() run() scheduledExecutionTime()

  3. quanz:任務定時調度框架。Schdule調度器,控制所有的調度Trigger 觸發條件,採用OSL模式JobDetail 需要處理的jobJob 執行邏輯
    DSL Domain-specific language領域特定語言,針對一個特定的領域,具有受限表達性的一種計算機程序語言,即領域專用語言,聲明式編程:
    1Method Chaining方法鏈 Fluent Style流暢風格 builder模式構建器
    2Nested Functions 嵌套函數
    3Lambda Expressions/Closures
    4Functional Sequence

  4. happenbefore:在JMM中,如果一個操作執行的結果需要對另一個操作可見,那麼這兩個操作之間必須要存在happens-before關係。你寫的代碼可能根本沒有按照你期望的順序執行,因爲編譯器和CPU會嘗試重排指令使得代碼更快的運行
    第一步從內存中獲取指令fetch將指令進行解碼翻譯
    第二步從寄存器中拿出對應的值、工作內存,需要拷貝
    第三步計算
    第四步同步到主存
    看到下一個指令與上一條無關,那麼就提前執行了指令重排對我們的指令是有影響的
    執行代碼的順序可能與編寫代碼不一致,即虛擬機優化代碼順序,則爲指令重排
    happen-before:即,編譯器或運行時環境爲了優化程序性能而採取的對指令進行重新排序執行的一種手段
    在虛擬機層面,爲了儘可能減少內存操作速度遠慢於CPU運行速度所帶來的CPU空置的影響,虛擬機會按照自己的一些規則將程序編寫順序打亂–
    即寫在後面的代碼在時間順序上可能會先執行,而寫在前面的代碼會後執行–以即可能充分的利用CPU。

•對於會改變程序執行結果的重排序,JMM要求編譯器和處理器必須禁止這種重排序。•對於不會改變程序執行結果的重排序,JMM對編譯器和處理器不做要求(JMM允許這種重排序)1)如果一個操作happens-before另一個操作,那麼第一個操作的執行結果將對第二個操作可見,而且第一個操作的執行順序排在第二個操作之前。2)兩個操作之間存在happens-before關係,並不意味着Java平臺的具體實現必須要按照happens-before關係指定的順序來執行。如果重排序之後的執行結果,與按happens-before關係來執行的結果一致,那麼這種重排序並不非法(也就是說,JMM允許這種重排序)。•as-if-serial語義保證單線程內程序的執行結果不被改變,happens-before關係保證正確同步的多線程程序的執行結果不被改變。•as-if-serial語義給編寫單線程程序的程序員創造了一個幻境:單線程程序是按程序的順序來執行的。happens-before關係給編寫正確同步的多線程程序的程序員創造了一個幻境:正確同步的多線程程序是按happens-before指定的順序來執行的。as-if-serial語義和happens-before這麼做的目的,都是爲了在不改變程序執行結果的前提下,儘可能地提高程序執行的並行度。
56. 雖然這裏有兩種情況:後面的代碼先於前面的代碼開始執行;前面的代碼先開始執行,但當效率較慢的時候,後面的代碼開始執行並先於前面的代碼執行結束。
不管誰先開始,總之後面的代碼在一些情況下存在先結束的可能。
在硬件層面,CPU會接收到的一些指令按照其規則重新排序,同樣是基於CPU速度比緩存速度塊的原因,和上面的一點的目的類似。只是虛擬機可以在更大層面更多指令範圍內重新排序。

  1. 數據依賴:如果兩個操作訪問同一個變量,且這兩個操作中有一個爲寫操作,此時這兩個操作之間就存在數據依賴。數據依賴分爲以下三種類型:
    1:寫後讀
    2:寫後寫
    3:讀後寫
    以上三種情況,只要重新排序兩個操作的執行順序,程序的執行結果將會被改變,所以,編譯器和處理器在重新排序時,會遵守數據依賴性,編譯器和處理器不會改變數據依賴關係的兩個操作的執行順序。這裏所說的數據依賴性僅針對單個處理器中執行的指令序列和單個線程中執行的操作,不同處理器之間和不同線程之間的數據依賴性不被編譯器和處理器考慮。

  2. 順序一致性:一個線程中的所有操作必須按照程序的順序來執行。(不管程序是否同步)所有線程都只能看到一個單一的操作執行順序。在順序一致性內存模型中,每個操作都必須原子執行且立刻對所有線程可見。

  3. volitale:是Java虛擬機提供的輕量級的同步機制(輕量級的synchronized)。它能保證線程之間的變量的可見行,簡單的說就是當一個線程對一個共享變量進行了修改後,另外一個線程能讀到這個修改的值。比synchronized成本低因爲它不會引起線程上下文切換和調度。如果一個字段被聲明成volatile,Java線程內存模型確保所有線程看到這個變量的值是一致的。它能保證可見性,不保證原子性,禁止指令重排。
    更詳細的說是要符合以下兩個規則:
    線程對變量進行修改之後,要立刻會寫到主內存
    線程對變量讀取的時候,要從主內存中讀,而不是緩存(這樣就形成了消息通信)
    Java線程—工作內存—save和load操作—主內存

  4. Volitale不保證原子性:對任意單個volatile變量的讀寫具有原子性,但類似於volatile++這種複合操作不具有原子性。原子性:不可分割、完整性,也就是某個線程正在做某個具體業務時,中間不可以被加塞或者被分割,需要整體完整,要麼同時成功,要麼同時失敗。拷貝回自己的內存空間,每個人都拿到0,寫回到主內存時,線程1寫回到的時候被掛起了,線程2的寫回了。然後線程1恢復後又寫回了一遍,把原來的1給覆蓋了。如何解決:1.使用同步方法2.使用atomic類進行解決。

  5. Volitale禁止指令重排:從而避免多線程環境下程序出現亂序執行的現象。內存屏障(Memory Barrier)又稱內存柵欄,是一個CPU指令,它的作用有兩個:一是保證特定操作的執行順序,二是保證某些變量的內存可見性(利用該特定實現volatile的內存可見性)。

由於編譯器和處理器都能執行指令重排優化。如果在指令間插入一條Memory Barrier則會告訴編譯器和CPU,不管什麼指令都不能和這條Memory Barrier指令重排序,也就是說通過插入內存屏障禁止在內存屏障前後的指令執行重排序優化。

內存屏障另外一個作用是強制刷出各種CPU的緩存數據,因此在任何CPU上的線程都能讀取到這些數據的最新版本。對volatile變量進行寫操作時,會在寫操作後加入一條store屏障指令,將工作內存中的共享變量值刷新回到主內存。對volatile變量進行讀操作時,會在讀操作前加入一條load屏障指令,從主內存中讀取共享變量。

《當第二個操作是volatile寫時,不管第一個操作是什麼都不能重排序。確保寫之前的操作不會被編譯重排序到寫之後。
當第一個操作是volatile讀時,不管第二個操作是什麼都不能重排序。確保讀之後的操作不會被編譯重排序到讀之前。
當第一個操作是volatile寫,第二個操作是volatile讀時,不能重排序,》

寫:前StoreStore<禁止上面的普通寫和下面的volitale寫重排序>後StoreLoad<防止上面的volatile寫和下面可能有的volitale讀/寫重排序> 讀:後:LoadLoad<禁止下面的所有普通讀操作和上面的volatile讀重排序>後LoadStore<禁止下面所有的寫操作和上面的volatile讀重排序>

  1. JMM:Java內存模型(Java Memory Model),本身是一種抽象的概念並不真實存在,它描述的是一組規則或規範,通過這組規範定義了程序中各個變量(包括實例字段、靜態字段和構成數組對象的元素—堆內存在線程之間共享)的訪問方式。局部變量、方法定義參數、異常處理器參數不會在線程間共享,不會有內存可見性問題,不受內存模型的影響。

特性:可見性、原子性、有序性。

線程通信:線程之間以何種機制來交換信息。共享內存(線程之間共享程序的公共狀態,通過寫-讀內存中的公共狀態進行隱式通信。)和消息傳遞(線程之間沒有公共狀態,線程之間必須通過發送消息來顯式進行通信。)。

線程同步:程序中用於控制不同線程間操作發生相對順序的機制。共享內存(同步是顯式進行的。程序員必須顯式指定某個方法或某段代碼需要在線程之間互斥執行。)和消息傳遞(由於消息的發送必須在消息的接收之前,因此同步是隱式進行的。)。

JMM是一個語言級的內存模型,處理器內存模型是硬件級的內存模型,順序一致性內存模型是一個理論參考模型。

  1. JMM內存模型的可見性:由於JVM運行程序的實體是線程,而每個線程創建時JVM都會爲其創建一個工作內存(棧空間),工作內存是每個線程的私有數據區域。而JMM中規定所有的變量都存儲在主內存,主內存時共享內存區域,所有的線程都可以訪問。但線程對變量的操作必須在工作內存中進行,首先要將變量從主內存拷貝到自己的工作內存空間,然後對變量進行操作,操作完成後再將變量寫回主內存,不能直接操作主內存中的變量,各個線程中的工作內存中存儲着主內存中的變量副本拷貝,因此不同的線程間無法訪問對方的工作內存,線程間的通信(傳值)必須通過主內存來完成。總結:各個線程對主內存中共享變量的操作都是各個線程各自拷貝到自己的工作內存進行操作後再寫回到主內存中的。《工作內存和主內存的同步延遲問題造成了可見性問題。》所以我們需要有一個機制:JMM內存模型的可見性,只要有一個線程改變數據後要寫回到主內存中,其它的線程馬上就會知道主內存中的數據已經改變了。

JMM關於同步的規定:1線程解鎖前,必須把共享變量的值刷新回主內存2線程加鎖前,必須讀取主內存的最新值到自己的工作內存3加鎖解鎖是同一把鎖。

  1. JMM內存模型的原子性:number++在多線程下是非線程安全的,如何不加synchronized解決?被拆分爲3個指令:執行getfield拿到原始的number,執行iadd進行加1操作,執行putfield寫把累加後的值寫回。三個線程都拿到1,都在各自的工作內存中加1,寫回到的時候,沒有拿到最新的值就又寫了,寫覆蓋。使用atomic類解決。

原子操作是指不可中斷的一個或一系列的操作,一般的處理器是利用總線加鎖或者緩存加鎖的方式實現多處理器之間的原子操作。

總線鎖定:處理器提供一個LOCK # 信號,剛一個處理器在總線上輸出一個信號時,其它的處理器請求就會被阻塞住,那麼該處理器就可以獨佔共享內存了。

緩存鎖定:內存區域如果被緩存在處理器的緩存行中,在Lock操作期間被鎖定,在執行鎖操作回寫到內存時,處理器不在總線上聲言LOCK #信號,而是修改內存地址,緩存的一致性機制會阻止同時修改由2個以上處理器緩存的內存區數據,其他的處理器成功回寫被鎖定的緩存行數據時,會使緩存行無效,其它的處理器就不能在使用這個緩存行了。

Java中是通過使用鎖和循環CAS(atomic包)的方式來實現原子操作。如:AtomiclBoolean、AtomicInteger、AtomicLong.。

CAS實現原子操作的三大問題:1. ABA問題->使用版本號,AtomicStampReference
2. 循環時間長開銷大
3. 只能保證一個共享變量的原子操作,可以考慮把多個變量合成一個共享變量,或者使用鎖:偏向鎖,輕量級鎖和互斥鎖。除了偏向鎖,其他實現鎖的方式都使用了循環CAS獲得和釋放鎖

  1. JMM內存模型的有序性:計算機在執行程序時,爲了提高性能,編譯器和處理器常常會對指令重排,編譯器優化的重排、指令並行的重排、內存系統的重排。單線程環境裏面確保程序最終執行結果和代碼順序執行的結果一致,處理器在執行重排序時必須考慮指令之間的數據依賴性。多線程環境中線程交替執行,由於編譯器優化重排的存在,兩個線程中使用的變量能否保證一致性是無法確定的,結果無法預測。Memory Barrier實現禁止指令重排。加了volatile之後是禁止指令重排。

  2. 線程安全性獲得保證:對於工作內存和主內存同步延遲線程導致的可見性問題,使用synchronized或volatile關鍵字解決,它們都可以使一個線程修改後的變量立即對其他線程可見。對於指令重排導致的可見性問題和有序性問題,可以利用volatile關鍵字解決,因爲它的另外一個作用就是禁止重排序優化。

  3. Volitale應用:DCL單例模式(Double Check Lock雙端檢鎖機制): 懶漢式套路基礎上加入併發控制,在加鎖之前和之後都進行一次檢測,保證在多線程環境下,對外存在一個對象。在多線程壞境下,單例模式出現了問題,如果加上synchronized,在多線程的環境控制住了,但是太重了,併發性下降了。
    1、構造器私有化 -->避免外部new構造器
    2、提供私有的靜態屬性 -->存儲對象的地址)(沒有volatile其他線程可能訪問一個沒有初始化的對象,保證同步更新。)
    3、提供公共的靜態方法 --> 獲取屬性(避免創建兩個對象,所以這裏同步,鎖定這個class;1、開闢空間 //2、初始化對象信息 //3、返回對象的地址給引用)

如果第一次檢查instance不爲null,那麼就不需要執行下面的加鎖和初始化操作。因此,可以大幅降低synchronized帶來的性能開銷。上面代碼表面上看起來,似乎兩全其美。
雙端檢測機制不一定線程安全,原因是有指令重排的存在,加入volatile可以禁止指令重排。原因在於某一個線程執行到第一次檢測,讀取到的instance不爲null時,instance的引用對象可能沒有完成初始化。Instance=new Singleton();可以分爲以下3個步驟完成:memory=allocate();instance(memory);instance=memory;1.分配對象內存空間2.初始化對象3.設置instance指向剛分配的內存之地,此時instance!=null。步驟2和3不存在數據依賴關係,而且無論重排前還是重排後程序的執行結果在單線程中並沒有改變,因此這種重排優化是允許的,所以就會導致對象還沒有完成初始化,但是instance就不爲null了,造成了線程安全問題。地址不爲空,但是內容爲空,所以要在instance變量上面加上volatile。
另一個併發執行的線程B就有可能在第4行判斷instance不爲null。線程B接下來將訪問instance所引用的對象,但此時這個對象可能還沒有被A線程初始化!此時,線程B將會訪問到一個還未初始化的對象。在知曉了問題發生的根源之後,我們可以想出兩個辦法來實現線程安全的延遲初始化。1)不允許2和3重排序。2)允許2和3重排序,但不允許其他線程“看到”這個重排序。
把instance聲明爲volatile型,就可以實現線程安全的延遲初始化。2和3之間的重排序,在多線程環境中將會被禁止。

還有一種方式是通過靜態內部類的方式實現:JVM在類的初始化階段(即在Class被加載後,且被線程使用之前),會執行類的初始化。在執行類的初始化期間,JVM會去獲取一個鎖。這個鎖可以同步多個線程對同一個類的初始化。2和3重排序,但不允許非構造線程(這裏指線程B)“看到”這個重排序。Java初始化一個類或接口的處理過程如下:
第1階段:通過在Class對象上同步(即獲取Class對象的初始化鎖),來控制類或接口的初始化。這個獲取鎖的線程會一直等待,直到當前線程能夠獲取到這個初始化鎖。第2階段:線程A執行類的初始化,同時線程B在初始化鎖對應的condition上等待。第3階段:線程A設置state=initialized,然後喚醒在condition中等待的所有線程。第4階段:線程B結束類的初始化處理。第5階段:線程C執行類的初始化的處理。

通過對比基於volatile的雙重檢查鎖定的方案和基於類初始化的方案,我們會發現基於類初始化的方案的實現代碼更簡潔。但基於volatile的雙重檢查鎖定的方案有一個額外的優勢:除了可以對靜態字段實現延遲初始化外,還可以對實例字段實現延遲初始化。字段延遲初始化降低了初始化類或創建實例的開銷,但增加了訪問被延遲初始化的字段的開銷。在大多數時候,正常的初始化要優於延遲初始化。如果確實需要對實例字段使用線程安全的延遲初始化,請使用上面介紹的基於volatile的延遲初始化的方案;如果確實需要對靜態字段使用線程安全的延遲初始化,請使用上面介紹的基於類初始化的方案。

  1. ThreadLocal:即線程變量,是一個以ThreadLocal對象爲鍵、任意對象爲值的存儲結構。這個結構被附帶在線程上,也就是說一個線程可以根據一個ThreadLocal對象查詢到綁定在這個線程上的一個值。可以通過set(T)方法來設置一個值,在當前線程下再通過get()方法獲取到原先設置的值。
    1.在多線程環境下,每個線程都有自己的數據。一個線程使用自己的局部變量比使用全局變量好,因爲局部變量只有線程自己能看見,不會影響其他線程。
    2.ThreadLocal能夠放一個線程級別的變量,其本身能夠被多個線程共享使用,並且又能夠達到線程安全的目的。說白了,ThreadLocal就是想在多線程環境下去保證成員變量的安全,常用的方法,就是get/set/initiaValue方法。
    3.JDK建議ThreadLocal定義爲private static
    4.ThreadLocal最常用的地方就是爲每個線程綁定一個數據庫連接,HTTP請求,用戶身份信息等,這樣一個線程的所有調用到的方法都可以非常方便的訪問這些資源(Hibernate的Session工具類HibernateUti、通過不同的線程對象設置Bean屬性,保證各個線程Bean對象的獨立性)

  2. ThreadLocal:每個線程自身的存儲本地、局部區域get/set/initialValue
    ThreadLocal:每個線程自身的數據,更改不會影響其他線程
    ThreadLocal:分析上下文 環境 起點1、構造器: 哪裏調用 就屬於哪裏 找線程體2、run方法:本線程自身的
    InheritableThreadLocal:繼承上下文 環境的數據 ,拷貝一份給子線程

  3. 可重入鎖:鎖作爲併發共享數據保證一致性的工具,大多數內置鎖都是可以重入的,也就是說,如果每個線程試圖獲取一個已經由它自己持有的鎖時,那麼這個請求會立刻成功,並且會將這個鎖的計數值加1,而當線程退出同步代碼塊時,計數器會遞減,當計數值等於0時,鎖釋放。如果沒有可重入鎖的支持,在第二次企圖獲得鎖時會進入死鎖狀態。可重入鎖隨處可見。

  4. CAS:對於併發控制而言,鎖是一種悲觀的策略,它總是假設每一次的臨界區操作會產生衝突,於是對每次操作都小心翼翼,而無鎖是一種樂觀策略,它會假設對資源的訪問是沒有衝突的,自然不需要等待,所以所有的線程都可以在不停頓的狀態下持續執行,遇到衝突的話,使用CAS來鑑別線程衝突,一旦檢測到衝突產生,就重試當前操作直到沒有衝突爲止。compare and set ,它是一條CPU併發原語。比較當前工作內存中的值和主內存中的值,如果相同則執行規定操作,否則繼續比較直到主內存和工作內存中的值一致爲止。這個過程是原子的。《多個線程去操作主內存中的數據。一個叫做期望值、一個叫做更新值。主內存的值,一個線程拷貝回去自己的工作內存,對它進行修改,然後寫回到主內存的時候,會進行比較和交換,如果和拷貝的數據一樣的話,就將改變後的數據寫回去;否則的話,就不進行寫回。》 CAS併發原語體現在Java語言中sun.misc.Unsafe類中的各個方法。調用Unsafe類中的CAS方法,JVM會幫我們實現出CAS彙編指令。這是一種完全依賴於硬件的功能,通過它實現了原子操作。原語屬於操作系統用語,是由若干條指令組成的,用於完成某個功能的一個過程,並且原語的執行必須是連續的,在執行過程中不允許被中斷,也就是說CAS是一條CPU的原子指令,不會造成所謂的數據不一致問題。例子:使用atomicInteger的compareAndSet()方法

  5. CAS底層原理?atomicInteger.getAndIncrement()方法的源代碼public final int getAndIncrement(){return unsafe.getAndInt(this,valueoffset,1)}《AtomicInteger 的getandincrement方法底層其實是CAS思想,套的是unsafe類的CPU原語來保證原子性,底層思想是比較並交換,真實值和期望值相等就交換成功,否則就失敗,失敗就再來,直到比較成功爲止。》
    compareAndSwapInt(Object o,long offset,int expected,int x)
    1.UnSafe類:UnSafe類是CAS的核心類,由於Java方法無法直接訪問底層系統,需要本地(native)方法來訪問,UnSafe相當於一個後門,基於該類可以直接操作特定內存的數據。UnSafe類存在於sun.misc包中,其內部方法操作可以像C的指針一樣直接操作內存,因爲Java中CAS操作的執行依賴於UnSafe類的方法。注意UnSafe類中的所有方法都是native修飾的,也就是說UnSafe類中的方法都直接調用操作系統底層資源執行相應任務。

2.變量valueOffset:表示該變量值在內存中的偏移地址,因爲UnSafe就是根據內存偏移地址獲取數據的。

3.變量value用volatile修飾,保證了多線程之間的內存可見性。

Unsafe.getAndAddInt():
public final int getAndAddInt(Object var1,long var2,int var4){int var5;do{var5=this.getIntVolatile(var1,var2);}while(!this.compareAndSwapInt(var1,var2,var5,var5+var4));return vat5;}
var1:AtomicInteger對象本身var2:該對象指的引用地址var4:需要變動的數量var5:是用var1var2找出的主內存中真實的值。用該對象當前的值與var5比較,如果相同,更新var5+var4並且返回true,如果不同,繼續取值然後再比較,直到更新完成。

如果是多線程同時執行getAndAddInt()方法,假設線程A和線程B同時執行getAndAddInt操作:1、AtomicInteger裏面的value原始值爲3,即主內存中的value值爲3,根據JMM模型,線程A和線程B各自持有一份值爲3的副本分別到各自的工作內存。2、線程A通過getIntVolatile(var1,var2)拿到value值3,此時線程A被掛起。3、線程B通過getIntVolatile(var1,var2)拿到value值3,此時線程B沒被掛起並執行compareAndSwapInt方法比較內存值也是3,成功被修改內存值爲4,線程B收工。4、此時線程A恢復,執行compareAndSwapInt方法比較,發現自己手裏的值3和主內存中的額值4不一致,說明該值已經被其他線程搶先一步修改過了,那麼線程A本次修改失敗,只能重新讀取重新來一遍了5、線程A重新獲取value值,因爲變量vlaue被volatile修飾,所以其他線程對它的修改,線程A總是能夠看到,線程A繼續執行compareAndSwapInt進行比較並替換,直到成功。

  1. CAS缺點:1.循環時間長開銷很大,使用dowhile語句,如果CAS失敗會一直嘗試,如果CAS長時間一直不成功,可能會被CPU帶來很大的開銷。2.只能保證一個共享變量的原子操作。對於多個共享變量操作,循環CAS就無法保證操作的原子性,這個時候就可以用鎖來保證原子性。3.引出ABA問題。原子類AtomicInteger的ABA問題。
    CAS—UnSafe—CAS底層原理—ABA—原子引用更新—如何規避ABA問題
  2. 鎖分爲兩種:悲觀鎖:synchronized是獨佔鎖即悲觀鎖,會導致其他所有需要鎖的線程掛起,等待持有鎖的線程釋放鎖。樂觀鎖:每次不加鎖而是假設沒有衝突而去完成某項操作,如果因爲衝突失敗就重試,直到成功爲止-CAS。
  3. 樂觀鎖的實現:
    有三個值:一個當前內存值V、舊的預期值A、將更新的值B。先獲取到內存當中當前的內存值V,
    再將內存值V和原值A進行比較,要是相等就修改爲要修改的值B並返回true,否則什麼都不做,並返回false
    CAS是一組原子操作,不會被外部打斷;
    屬於硬件級別的操作(利用CPU的CAS指令,同時藉助JNI來完成的非阻塞算法),效率比加鎖操作高。
  4. ABA問題:如果變量V初次讀取的時候是A,並且在準備賦值的時候檢查到它仍然是A,那能說明它的值沒有被其他線程修改過了麼?
    如果在這段期間曾經被改成B,然後又改回A,那麼CAS操作就會誤認爲它從來沒有被修改過。
  5. 原子類AtomicInteger的ABA問題:CAS會導致ABA問題,CAS算法實現一個重要前期需要取出內存中某時刻的數據並在當下時刻比較並替換,那麼在這個時間差類會導致數據的變化。一個線程1從內存位置V中取出A,另一個線程2也從內存中取出A,並且線程2進行了一些操作將值變成了B,然後線程2又將V位置的數據變成A,這時候線程1進行CAS操作發現內存中仍然是A,線程1操作成功。但整個過程是有問題的。
  6. AtomicReference原子引用:原子引用的泛型類AtomicReference a = new AtomicReference<>(); a.compareAndSet(z2,li4);
  7. AtomicStampedReference時間戳原子引用:–解決ABA問題,就是修改版本號,類似於時間戳。T1 100 1 T2 100 1 T2 101 2 T2 100 3 T1 1111 2AtomicStampedReference atomicStampedReference = new AtomicStampedReference<>(100,1);atomicStampedReference.compareAndSet(100,101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
  8. 13個原子操作類Atomic包裏的類基本都是使用Unsafe實現的包裝類。
    1 原子更新基本類型類
    •AtomicBoolean:原子更新布爾類型。
    •AtomicInteger:原子更新整型。
    •AtomicLong:原子更新長整型。
    int addAndGet(int delta):以原子方式將輸入的數值與實例中的值(AtomicInteger裏的value)相加,並返回結果。boolean compareAndSet(int expect,int update):如果輸入的數值等於預期值,則以原子方式將該值設置爲輸入的值。int getAndIncrement():以原子方式將當前值加1,注意,這裏返回的是自增前的值。void lazySet(int newValue):最終會設置成newValue,使用lazySet設置值後,可能導致其他線程在之後的一小段時間內還是可以讀到舊的值。int getAndSet(int newValue):以原子方式設置爲newValue的值,並返回舊值。

2 原子更新數組
•AtomicIntegerArray:原子更新整型數組裏的元素。
•AtomicLongArray:原子更新長整型數組裏的元素。
•AtomicReferenceArray:原子更新引用類型數組裏的元素。
int addAndGet(int i,int delta):以原子方式將輸入值與數組中索引i的元素相加。boolean compareAndSet(int i,int expect,int update):如果當前值等於預期值,則以原子方式將數組位置i的元素設置成update值。
3 原子更新引用
AtomicReference:原子更新引用類型。
AtomicReferenceFieldUpdater:原子更新引用類型裏的字段
AtomicMarkableReference:原子更新帶有標記位的引用類型。可以原子更新一個布爾類型的標記位和引用類型。構造方法:AtomicMarkableReference(V initialRef,Boolean initialMark)
4 原子更新字段類
•AtomicIntegerFieldUpdater:原子更新整型的字段的更新器。
•AtomicLongFieldUpdater:原子更新長整型字段的更新器。
AtomicStampedReference:原子更新帶有版本號的引用類型。該類將整數值與引用關聯起來,可用於原子的更新數據和數據的版本號,可以解決使用CAS進行原子更新時可能出現的ABA問題。
要想原子地更新字段類需要兩步。第一步,因爲原子更新字段類都是抽象類,每次使用的時候必須使用靜態方法newUpdater()創建一個更新器,並且需要設置想要更新的類和屬性。第二步,更新類的字段(屬性)必須使用public volatile修飾符。

  1. 併發集合:ConcurrentHashMap CopyOnWriteArrayList ConcurrentLinkedQueue BlockingQueue ConcurrentSkipListMap
  2. 跳錶:是一種可以用來快速查找的數據結構,有點類似於平衡樹。他們都可以對元素進行快速的查找,區別:對平衡樹的插入和刪除往往很可能導致平衡樹進行一次全局的調整。而對跳錶的插入和刪除只需要對整個數據結構的局部進行操作就可以。用跳錶實現一個Map。

跳錶的另一個特點是隨機算法,本質是同時維護了多個鏈表,並且鏈表是分層的。
最低層的鏈表維護了跳錶內所有的元素,沒上面一層鏈表都是下面一層的子集,一個元素插入哪些層是完全隨機的。跳錶內的所有鏈表的元素都是排序的,查找時,可以從頂級鏈表開始找,一旦發現被查找的元素大於當前鏈表中的取值,就會轉入下一層鏈表繼續找。以空間換時間的算法。
ConcurrentSkipListMap Node(key,value,next) 對node的所有操作使用的CAS方法
Index(Node,down,right) HeadIndex(表示鏈表頭部的第一個index)

  1. ArrayList是線程不安全的,故障現象:java.util.ConcurrentModificationException。導致原因:併發爭奪修改導致。解決方案:1、new Vector() 2、Collections.synchronizedList(new ArrayList<>())3、new CopyOnWriteArrayList()
  2. 寫時複製容器:讀寫分離思想,private transient volatile Object[] array;然後add()方法中使用了重用鎖,public Boolean add(E e){final ReentrantLock lock=this.lock; lock.lock(); try{Object[] elements=getArray(); int len=elements.length; Object[] newElements=Arrays.copyOf(elements,len+1); newElements[len]=e; setArray(newElements); return true;} finally{ lock.unlock();}}往一個容器添加元素時,不直接往當前容器Object[]中添加,而是先將當前容器Object[]進行Copy,複製出一個新的容器Object[] newElements,然後新的容器中添加元素,添加完元素之後,再將原容器的引用指向新的容器。這樣做的好處是可以對CopyOnWrite容器進行併發的讀,而不需要加鎖,因爲當前容器不會添加任何元素。所以是一種讀寫分離的思想,讀和寫不同的容器。

讀:讀寫鎖中讀操作會受到寫操作的阻礙。爲了讓讀的性能發揮到極致。寫時複製是的寫時完全不用加鎖的,寫入也不會阻塞讀取操作。只有寫入和寫入之間需要進行同步等待。就是當列表需要修改時,我並不修改原有的內容而是對原有的數據進行一次複製,將修改的內容寫入副本中,寫完之後,再將修改完的副本替換原有的數據。這樣可以保證寫操作不會影響讀了。
85. HashSet線程不安全,解決:1Collections.synchroniedSet(new HashSet<>())2CopyOnWriteArraySet,它的底層是CopyOnWriteArrayList
86. HashMap線程不安全,解決:ConcurrentHashMap<>()
在併發編程中使用HashMap可能導致程序死循環。而使用線程安全的HashTable效率又非常低下,基於以上兩個原因,便有了ConcurrentHashMap的登場機會。ConcurrentHashMap的鎖分段技術可有效提升併發訪問率:HashTable容器在競爭激烈的併發環境下表現出效率低下的原因是所有訪問HashTable的線程都必須競爭同一把鎖,假如容器裏有多把鎖,每一把鎖用於鎖容器其中一部分數據,那麼當多線程訪問容器裏不同數據段的數據時,線程間就不會存在鎖競爭,從而可以有效提高併發訪問效率,這就是ConcurrentHashMap所使用的鎖分段技術。首先將數據分成一段一段地存儲,然後給每一段數據配一把鎖,當一個線程佔用鎖訪問其中一個段數據的時候,其他段的數據也能被其他線程訪問。一個ConcurrentHashMap被進一步細分爲16個段。

減小鎖粒度:如果需要在ConcurrentHashMap終止增加一個新的表項,並不是將整個HashMap加鎖,而是首先根據hashcode得到該表項應該被存放到哪個段中,然後對該段加鎖,並完成put操作,在多線程環境中,如果多個線程同時進行put操作,只要被加入的表項不存在同一個段中,則線程間便可以做到真正的並行。

ConcurrentHashMap是由Segment數組結構和HashEntry數組結構組成。Segment是一種可重入鎖(ReentrantLock),在ConcurrentHashMap裏扮演鎖的角色;HashEntry則用於存儲鍵值對數據。一個ConcurrentHashMap裏包含一個Segment數組。Segment的結構和HashMap類似,是一種數組和鏈表結構。一個Segment裏包含一個HashEntry數組,每個HashEntry是一個鏈表結構的元素,每個Segment守護着一個HashEntry數組裏的元素,當對HashEntry數組的數據進行修改時,必須首先獲得與它對應的Segment鎖。

ConcurrentHashMap初始化方法是通過initialCapacity、loadFactor和concurrencyLevel等幾個參數來初始化segment數組、段偏移量segmentShift、段掩碼segmentMask和每個segment裏的HashEntry數組來實現的。
1.初始化segments數組segments數組的長度ssize是通過concurrencyLevel計算得出的。爲了能通過按位與的散列算法來定位segments數組的索引,必須保證segments數組的長度是2的N次方(power-of-two size),所以必須計算出一個大於或等於concurrencyLevel的最小的2的N次方值來作爲segments數組的長度。
2.初始化segmentShift和segmentMask
3.初始化每個segment輸入參數initialCapacity是ConcurrentHashMap的初始化容量,loadfactor是每個segment的負載因子,在構造方法裏需要通過這兩個參數來初始化數組中的每個segment。

定位Segment:既然ConcurrentHashMap使用分段鎖Segment來保護不同段的數據,那麼在插入和獲取元素的時候,必須先通過散列算法定位到Segment。可以看到ConcurrentHashMap會首先使用Wang/Jenkins hash的變種算法對元素的hashCode進行一次再散列。之所以進行再散列,目的是減少散列衝突,使元素能夠均勻地分佈在不同的Segment上,從而提高容器的存取效率。假如散列的質量差到極點,那麼所有的元素都在一個Segment中,不僅存取元素緩慢,分段鎖也會失去意義。

ConcurrentHashMap的操作:get操作、put操作和size操作。
get操作:Segment的get操作實現非常簡單和高效。先經過一次再散列,然後使用這個散列值通過散列運算定位到Segment,再通過散列算法定位到元素。get操作的高效之處在於整個get過程不需要加鎖,除非讀到的值是空纔會加鎖重讀。我們知道HashTable容器的get方法是需要加鎖的,那麼ConcurrentHashMap的get操作是如何做到不加鎖的呢?原因是它的get方法裏將要使用的共享變量都定義成volatile類型,如用於統計當前Segement大小的count字段和用於存儲值的HashEntry的value。定義成volatile的變量,能夠在線程之間保持可見性,能夠被多線程同時讀,並且保證不會讀到過期的值,但是隻能被單線程寫(有一種情況可以被多線程寫,就是寫入的值不依賴於原值),在get操作裏只需要讀不需要寫共享變量count和value,所以可以不用加鎖。之所以不會讀到過期的值,是因爲根據Java內存模型的happen before原則,對volatile字段的寫入操作先於讀操作,即使兩個線程同時修改和獲取volatile變量,get操作也能拿到最新的值,這是用volatile替換鎖的經典應用場景。

put操作:由於put方法裏需要對共享變量進行寫入操作,所以爲了線程安全,在操作共享變量時必須加鎖。put方法首先定位到Segment,然後在Segment裏進行插入操作。插入操作需要經歷兩個步驟,第一步判斷是否需要對Segment裏的HashEntry數組進行擴容,第二步定位添加元素的位置,然後將其放在HashEntry數組裏。(1)是否需要擴容在插入元素前會先判斷Segment裏的HashEntry數組是否超過容量(threshold),如果超過閾值,則對數組進行擴容。值得一提的是,Segment的擴容判斷比HashMap更恰當,因爲HashMap是在插入元素後判斷元素是否已經到達容量的,如果到達了就進行擴容,但是很有可能擴容之後沒有新元素插入,這時HashMap就進行了一次無效的擴容。(2)如何擴容 在擴容的時候,首先會創建一個容量是原來容量兩倍的數組,然後將原數組裏的元素進行再散列後插入到新的數組裏。爲了高效,ConcurrentHashMap不會對整個容器進行擴容,而只對某個segment進行擴容。

size操作:如果要統計整個ConcurrentHashMap裏元素的大小,就必須統計所有Segment裏元素的大小後求和。Segment裏的全局變量count是一個volatile變量,那麼在多線程場景下,是不是直接把所有Segment的count相加就可以得到整個ConcurrentHashMap大小了呢?不是的,雖然相加時可以獲取每個Segment的count的最新值,但是可能累加前使用的count發生了變化,那麼統計結果就不準了。所以,最安全的做法是在統計size的時候把所有Segment的put、remove和clean方法全部鎖住,但是這種做法顯然非常低效。因爲在累加count操作過程中,之前累加過的count發生變化的機率非常小,所以ConcurrentHashMap的做法是先嚐試2次通過不鎖住Segment的方式來統計各個Segment大小,如果統計的過程中,容器的count發生了變化,則再採用加鎖的方式來統計所有Segment的大小。那麼ConcurrentHashMap是如何判斷在統計的時候容器是否發生了變化呢?使用modCount變量,在put、remove和clean方法裏操作元素前都會將變量modCount進行加1,那麼在統計size前後比較modCount是否發生變化,從而得知容器的大小是否發生變化。

  1. ConcurrentLinkedQueue: 如何使用非阻塞的方式來實現線程安全隊列ConcurrentLinkedQueue。ConcurrentLinkedQueue是一個基於鏈接節點的無界線程安全隊列,它採用先進先出的規則對節點進行排序,當我們添加一個元素的時候,它會添加到隊列的尾部;當我們獲取一個元素時,它會返回隊列頭部的元素。它採用了“wait-free”算法(即CAS算法)來實現,該算法在Michael&Scott算法上進行了一些修改。

ConcurrentLinkedQueue由head節點和tail節點組成,每個節點(Node)由節點元素(item)和指向下一個節點(next)的引用組成,節點與節點之間就是通過這個next關聯起來,從而組成一張鏈表結構的隊列。默認情況下head節點存儲的元素爲空,tail節點等於head節點。

入隊列就是將入隊節點添加到隊列的尾部。整個入隊過程主要做兩件事情:第一是定位出尾節點;第二是使用CAS算法將入隊節點設置成尾節點的next節點,如不成功則重試。
一、定位尾節點:tail節點並不總是尾節點,所以每次入隊都必須先通過tail節點來找到尾節點。尾節點可能是tail節點,也可能是tail節點的next節點。代碼中循環體中的第一個if就是判斷tail是否有next節點,有則表示next節點可能是尾節點。獲取tail節點的next節點需要注意的是p節點等於p的next節點的情況,只有一種可能就是p節點和p的next節點都等於空,表示這個隊列剛初始化,正準備添加節點,所以需要返回head節點。
二、設置入隊節點爲尾節點:p.casNext(null,n)方法用於將入隊節點設置爲當前隊列尾節點的next節點,如果p是null,表示p是當前隊列的尾節點,如果不爲null,表示有其他線程更新了尾節點,則需要重新獲取當前隊列的尾節點。《底層還是unsafe.compareAndSwapObject()》

出隊列:隊列的就是從隊列裏返回一個節點元素,並清空該節點對元素的引用。首先獲取頭節點的元素,然後判斷頭節點元素是否爲空,如果爲空,表示另外一個線程已經進行了一次出隊操作將該節點的元素取走,如果不爲空,則使用CAS的方式將頭節點的引用設置成null,如果CAS成功,則直接返回頭節點的元素,如果不成功,表示另外一個線程已經進行了一次出隊操作更新了head節點,導致元素髮生了變化,需要重新獲取頭節點。

  1. 傳值還是傳引用問題:age屬於main方法的,然後調用方法時複印了一份傳給它,然後方法把複印件給改動了,我只是給你複印了一份值,原件根本沒動,所以第一個age還是20;person是main的,傳引用傳內存地址給方法,兩個引用指向了同一個地址,這時把這個地址的值改動了;str是屬於main方法的,這個池子裏有了abc這個池子裏面沒有xxx,那麼就重新創建一個指向它。

  2. 公平鎖:是指多個線程按照申請鎖的順序來獲取鎖,先來後到。

  3. 非公平鎖:是指多個線程獲取鎖的順序並不是按照申請鎖的順序,有可能後申請的線程比先申請的線程優先獲取鎖,在高併發的情況下,有可能會造成優先級反轉或者飢餓現象。

  4. 公平鎖和非公平鎖的區別:併發包ReentrantLock的創建可以指定構造函數的boolean類型來得到公平鎖或非公平鎖,默認是非公平鎖,無參數:非公平鎖,有參數true:公平鎖。公平鎖就是很公平,在併發環境中,每個線程在獲取鎖時會先查看此鎖維護的等待隊列,如果爲空,或者當前線程是等待隊列的第一個,就佔有鎖,否則就會加入到等待隊列中,此後會按照FIFO的規則從隊列中取到自己。非公平鎖比較粗魯,上來就直接嘗試佔有鎖,如果嘗試失敗,就再採用類似公平鎖那種方式。ReentrantLock,非公平鎖的優點在於吞吐量比公平鎖大,而公平鎖要求系統維護一個有序隊列,成功高性能低。Synchronized,等同於鎖,是一種非公平鎖。

  5. 可重入鎖(又稱遞歸鎖):ReentrantLock。指的是同一線程外層函數獲得鎖之後,內層遞歸函數仍然能獲取該鎖的代碼,在同一個線程在外層方法獲取鎖的時候,在進入內層方法會自動獲取鎖。也就是說,線程可以進入任何一個它已經擁有的鎖所同步着的代碼塊。ReentrantLock與Synchronized都是典型的可重入鎖。可重入鎖的最大的作用是避免死鎖。

  6. 自旋鎖:SpinLock,Unsafe類+CAS思想。是指嘗試獲取鎖的線程不會立即阻塞,而是採用循環的方式去嘗試獲取鎖,這樣的好處是減少線程上下文切換的消耗,缺點是循環會消耗CPU。getAndAddInt例子。通過CAS操作完成自旋鎖例子:A線程先進來調用mylock方法自己持有鎖5秒鐘,B隨後進來發現當前有線程持有鎖,不是null,所以只能通過自旋等待,直到A釋放鎖後B隨即搶到。A線程進來,發現期望的是空的,那麼while的條件就是false,於是不進入循環,直接拿到了鎖。B線程進來,發現期望的值不是空,那麼while的條件就是true,於是它進入鎖中,一直會循環的判斷,直到期望的值是空了,才能推出循環,獲得鎖。AtomicReference atomicReference = new AtomicReference<>();加鎖:while (!atomicReference.compareAndSet(null,thread)){}解鎖:atomicReference.compareAndSet(thread,null);

  7. 獨佔鎖:指該鎖一次只能被一個線程所持有。ReentrantLock和Synchronized而言都是獨佔鎖。

  8. 共享鎖:指該鎖可以被多個線程所持有。對ReentrantReadWriteLock其讀鎖是共享鎖,其寫鎖是獨佔鎖。讀鎖的共享鎖可保證併發讀是非常高效的,讀寫、寫讀、寫寫的過程是互斥的。以前使用鎖和synchronized讀和寫通通不能併發執行,數據一致量可以保證,但併發性急劇下降。鎖不能進行細粒度的劃分,只能把全部進行封殺。讀寫鎖:多個線程同時讀一個資源類沒有任何問題,所以爲了滿足併發量,讀取共享資源應該可以同時讀,但是,如果有一個線程想去寫共享資源來,就不應該再有其他線程可以對該資源進行讀或寫。private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();readWriteLock.writeLock().lock();readWriteLock.writeLock().unlock();readWriteLock.readLock().lock();readWriteLock.readLock().unlock();

  9. Java中的併發工具類:CountDownLatch、CyclicBarrier、Semaphore。工具類提供了一種併發流程控制的手段。Exchanger工具類則提供了在線程間交換數據的一種手段。

  10. CountDownLatch:讓一個或一些線程阻塞直到另一些線程完成一系列操作之後才被喚醒。CountDownLatch的構造函數接收一個int類型的參數作爲計數器,如果你想等待N個點完成,這裏就傳入N。主要有兩個方法,其他線程調用countDown方法會將計數器減1(調用countDown方法的線程不會阻塞),當一個或多個線程調用await方法時,調用線程會被阻塞。當計數器的變爲零時,因調用await方法被阻塞的線程會被喚醒,繼續執行。《做減法、倒計時(教室關燈)》如果有某個解析sheet的線程處理得比較慢,我們不可能讓主線程一直等待,所以可以使用另外一個帶指定時間的await方法——await(long time,TimeUnit unit),這個方法等待特定時間後,就會不再阻塞當前線程。join也有類似的方法。

  11. CyclicBarrier:可循環使用的屏障。它要做的事情是,讓一組線程到達一個屏障(也可以叫同步點)時被阻塞,直到最後一個線程到達屏障時,屏障纔會開門,所有被屏障攔截的線程纔會繼續幹活。CyclicBarrier默認的構造方法是CyclicBarrier(int parties),其參數表示屏障攔截的線程數量,每個線程調用await方法告訴CyclicBarrier我已經到達了屏障,然後當前線程被阻塞。CyclicBarrier還提供一個更高級的構造函數CyclicBarrier(int parties,Runnable barrier-Action),用於在線程到達屏障時,優先執行barrierAction,方便處理更復雜的業務場景。線程進入屏障通過CyclicBarrier的await()方法。《做加法、累積(收集龍珠)》
    CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
    System.out.println(“召喚龍珠”);
    }); cyclicBarrier.await();
    CyclicBarrier可以用於多線程計算數據,最後合併計算結果的場景。

  12. CyclicBarrier和CountDownLatch的區別:1.CountDownLatch的計數器只能使用一次,而CyclicBarrier的計數器可以使用reset()方法重置。所以CyclicBarrier能處理更爲複雜的業務場景。例如,如果計算髮生錯誤,可以重置計數器,並讓線程重新執行一次。2. CyclicBarrier還提供其他有用的方法,比如getNumberWaiting方法可以獲得Cyclic-Barrier阻塞的線程數量。isBroken()方法用來了解阻塞的線程是否被中斷。

  13. Semaphore:《信號量是對鎖的擴展,無論是內部鎖synchronized還是重入鎖ReentreantLock,都是一次只允許一個線程訪問一個資源,而信號量卻可以指定多個線程同時訪問某一個資源。》信號量是用來控制同時訪問特定資源的線程數量,它通過協調各個線程,以保證合理的使用公共資源。
    引用:Semaphore可以用於做流量控制,特別是公用資源有限的應用場景,比如數據庫連接。假如有一個需求,要讀取幾萬個文件的數據,因爲都是IO密集型任務,我們可以啓動幾十個線程併發地讀取,但是如果讀到內存後,還需要存儲到數據庫中,而數據庫的連接數只有10個,這時我們必須控制只有10個線程同時獲取數據庫連接保存數據,否則會報錯無法獲取數據庫連接。這個時候,就可以使用Semaphore來做流量控制。在代碼中,雖然有30個線程在執行,但是隻允許10個併發執行。
    Semaphore的構造方法Semaphore(int permits)Semaphore(int permits,Boolean fair)接受一個整型的數字,表示可用的許可證數量。Semaphore(10)表示允許10個線程獲取許可證,也就是最大併發數是10。
    Semaphore的用法也很簡單,首先線程使用Semaphore的acquire()方法獲取一個許可證,若無法獲得則線程會等待,直到有線程釋放一個許可或者當前線程被中斷。使用完之後調用release()方法歸還許可證。還可以用tryAcquire()方法嘗試獲取許可證,成功true失敗false,它不會等待,而是立即返回。Semaphore還提供一些其他方法:acquireUninterruptibly()\tryAcquire(long,unit)。intavailablePermits():返回此信號量中當前可用的許可證數。intgetQueueLength():返回正在等待獲取許可證的線程數。intgetQueueLength():返回正在等待獲取許可證的線程數。booleanhasQueuedThreads():是否有線程正在等待獲取許可證。void reducePermits(int reduction):減少reduction個許可證,是個protected方法。Collection getQueuedThreads():返回所有等待獲取許可證的線程集合,是個protected方法。

信號量主要用於兩個目的,一是用於多個共享資源的互斥使用,另一個用於併發線程數的控制。《多個線程搶多個資源,信號燈,資源爲1時就退化成synchronized。》acquire()方法用於:搶佔信號量獲得信號的允許,阻塞直到信號量可用releanse()方法用於:釋放信號量。

  1. 線程間交換數據的Exchanger:Exchanger(交換者)是一個用於線程間協作的工具類。Exchanger用於進行線程間的數據交換。它提供一個同步點,在這個同步點,兩個線程可以交換彼此的數據。這兩個線程通過exchange方法交換數據,如果第一個線程先執行exchange()方法,它會一直等待第二個線程也執行exchange方法,當兩個線程都到達同步點時,這兩個線程就可以交換數據,將本線程生產出來的數據傳遞給對方。Exchanger可以用於遺傳算法,遺傳算法裏需要選出兩個人作爲交配對象,這時候會交換兩人的數據,並使用交叉規則得出2個交配結果。Exchanger也可以用於校對工作,比如我們需要將紙製銀行流水通過人工的方式錄入成電子銀行流水,爲了避免錯誤,採用AB崗兩人進行錄入,錄入到Excel之後,系統需要加載這兩個Excel,並對兩個Excel數據進行校對,看看是否錄入一致。如果兩個線程有一個沒有執行exchange()方法,則會一直等待,如果擔心有特殊情況發生,避免一直等待,可以使用exchange(V x,longtimeout,TimeUnit unit)設置最大等待時長。

  2. 阻塞隊列:首先它是一個隊列,而一個阻塞隊列在數據結構中所起的作用應該是,線程1往阻塞隊列中添加元素,而線程2從阻塞隊列中移除元素。當阻塞隊列是空時,從隊列中獲取元素的操作將會被阻塞;當阻塞隊列是滿時,往隊列裏添加元素的操作將會被阻塞。試圖從空的阻塞隊列中獲取元素的線程將會被阻塞,直到其他的線程往空的隊列插入新的元素;試圖往已滿的阻塞隊列中添加新元素的線程同樣也會被阻塞,直到其他的線程從隊列中移除一個或者多個元素或者完全清空隊列後使隊列重新變得空閒起來並後續新增。阻塞隊列常用於生產者和消費者的場景,生產者是向隊列裏添加元素的線程,消費者是從隊列裏取元素的線程。阻塞隊列就是生產者用來存放元素、消費者用來獲取元素的容器。

  3. 阻塞隊列的作用和好處:在多線程領域,所謂阻塞,就是在某些情況下會掛起線程(即阻塞),一旦條件滿足,被掛起的線程又會自動被喚醒。好處是:我們不需要關心什麼時候需要阻塞線程,什麼時候需要喚醒線程,因爲這一切阻塞隊列都給你一手包辦了。在concurrent包發佈之前,在多線程環境下,我們每個程序員都必須自己去控制這些細節,尤其還要兼顧效率和線程安全,而這會給我們的程序帶來不小的複雜度。

  4. 阻塞隊列架構:Iterable->Collection->Queue->BlockingQueue->LinkedBlockingDeque/PriorityBlockingQueue/SynchronousQueue/DelayQueue/ArrayblockingQueue/LinkedTransferQueue

  5. ArrayblockingQueue:由數組結構組成的有界阻塞隊列。
    LinkedBlockingQueue :由鏈表結構組成的無界阻塞隊列。此隊列的默認和最大長度爲Integer.MAX_VALUE。此隊列按照先進先出的原則對元素進行排序。
    PriorityBlockingQueue :支持優先級排序的無界阻塞隊列。默認情況下元素採取自然順序升序排列。也可以自定義類實現compareTo()方法來指定元素排序規則,或者初始化PriorityBlockingQueue時,指定構造參數Comparator來對元素進行排序。需要注意的是不能保證同優先級元素的順序。
    DelayQueue :使用優先級隊列實現的延遲無界阻塞隊列。
    SynchronousQueue :不存儲元素的阻塞對壘,也即單個元素的對壘。
    LinkedTransferQueue :由鏈表結構組成的無界阻塞隊列。
    LinkedBlockingDeque:由鏈表結構組成的雙向阻塞隊列。

  6. ArrayblockingQueue:ArrayBlockingQueue是一個用數組實現的有界阻塞隊列。此隊列按照先進先出(FIFO)的原則對元素進行排序。默認情況下不保證線程公平的訪問隊列,所謂公平訪問隊列是指阻塞的線程,可以按照阻塞的先後順序訪問隊列,即先阻塞線程先訪問隊列。非公平性是對先等待的線程是非公平的,當隊列可用時,阻塞的線程都可以爭奪訪問隊列的資格,有可能先阻塞的線程最後才訪問隊列。爲了保證公平性,通常會降低吞吐量。我們可以使用以下代碼創建一個公平的阻塞隊列。ArrayBlockingQueue fairQueue = new ArrayBlockingQueue(1000,true); 訪問者的公平性是使用可重入鎖實現的。

  7. LinkedBlockingQueue:鎖分離的案例。Take()和put()分別實現了從隊列中取數據和往隊列中增加數據的功能。分別作用於隊列的前端和尾端,理論上兩者不衝突,如果使用獨佔鎖在運行時就不是真正的併發。所以使用兩把不同的鎖。分裂這兩個操作。takeLock putLock notEmpty notFull

  8. DelayQueue:DelayQueue是一個支持延時獲取元素的無界阻塞隊列。隊列使用PriorityQueue來實現。隊列中的元素必須實現Delayed接口,在創建元素時可以指定多久才能從隊列中獲取當前元素。只有在延遲期滿時才能從隊列中提取元素。DelayQueue非常有用,可以將DelayQueue運用在以下應用場景。•緩存系統的設計:可以用DelayQueue保存緩存元素的有效期,使用一個線程循環查詢DelayQueue,一旦能從DelayQueue中獲取元素時,表示緩存有效期到了。•定時任務調度:使用DelayQueue保存當天將會執行的任務和執行時間,一旦從DelayQueue中獲取到任務就開始執行,比如TimerQueue就是使用DelayQueue實現的。(1)如何實現Delayed接口。DelayQueue隊列的元素必須實現Delayed接口。我們可以參考ScheduledThreadPoolExecutor裏ScheduledFutureTask類的實現,一共有三步。第一步:在對象創建的時候,初始化基本數據。使用time記錄當前對象延遲到什麼時候可以使用,使用sequenceNumber來標識元素在隊列中的先後順序。第二步:實現getDelay方法,該方法返回當前元素還需要延時多長時間,單位是納秒。第三步:實現compareTo方法來指定元素的順序。例如,讓延時時間最長的放在隊列的末尾。(2)如何實現延時阻塞隊列延時阻塞隊列的實現很簡單,當消費者從隊列裏獲取元素時,如果元素沒有達到延時時間,就阻塞當前線程。

  9. LinkedTransferQueue:LinkedTransferQueue是一個由鏈表結構組成的無界阻塞TransferQueue隊列。相對於其他阻塞隊列,LinkedTransferQueue多了tryTransfer和transfer方法。(1)transfer方法:如果當前有消費者正在等待接收元素(消費者使用take()方法或帶時間限制的poll()方法時),transfer方法可以把生產者傳入的元素立刻transfer(傳輸)給消費者。如果沒有消費者在等待接收元素,transfer方法會將元素存放在隊列的tail節點,並等到該元素被消費者消費了才返回。(2)tryTransfer方法:tryTransfer方法是用來試探生產者傳入的元素是否能直接傳給消費者。如果沒有消費者等待接收元素,則返回false。和transfer方法的區別是tryTransfer方法無論消費者是否接收,方法立即返回,而transfer方法是必須等到消費者消費了才返回。對於帶有時間限制的tryTransfer(E e,long timeout,TimeUnit unit)方法,試圖把生產者傳入的元素直接傳給消費者,但是如果沒有消費者消費該元素則等待指定的時間再返回,如果超時還沒消費元素,則返回false,如果在超時時間內消費了元素,則返回true。

  10. LinkedBlockingDeque是一個由鏈表結構組成的雙向阻塞隊列。所謂雙向隊列指的是可以從隊列的兩端插入和移出元素。雙向隊列因爲多了一個操作隊列的入口,在多線程同時入隊時,也就減少了一半的競爭。相比其他的阻塞隊列,LinkedBlockingDeque多了addFirst、addLast、offerFirst、offerLast、peekFirst和peekLast等方法,以First單詞結尾的方法,表示插入、獲取(peek)或移除雙端隊列的第一個元素。以Last單詞結尾的方法,表示插入、獲取或移除雙端隊列的最後一個元素。另外,插入方法add等同於addLast,移除方法remove等效於removeFirst。但是take方法卻等同於takeFirst,不知道是不是JDK的bug,使用時還是用帶有First和Last後綴的方法更清楚。在初始化LinkedBlockingDeque時可以設置容量防止其過度膨脹。另外,雙向阻塞隊列可以運用在“工作竊取”模式中。

  11. 阻塞隊列的實現原理:使用通知模式實現。所謂通知模式,就是當生產者往滿的隊列裏添加元素時會阻塞住生產者,當消費者消費了一個隊列中的元素後,會通知生產者當前隊列可用。通過查看JDK源碼發現ArrayBlockingQueue使用了Condition來實現。

  12. BlockingQueue的核心方法:插入add(e)成功會返回true當阻塞隊列滿時再加會拋出illegalStateException:Queue full。 offer(e)插入成功返回true失敗返回false。 put(e)當阻塞隊列爲滿時生產者線程繼續往隊列里加,隊列會一直阻塞生產線程直到中斷退出。offer(e,time,unit)當阻塞隊列滿時,隊列會阻塞生產者線程一定時間,超時後生產者線程會退出。
    移除remove()成功會返回對應元素當阻塞隊列爲空時再移除會拋出NoSuchElementException。 poll()移除成功返回出隊列的元素,隊列裏沒有就返回null。 take()當阻塞隊列爲空時,消費者線程試圖從隊列裏取元素隊列會一直阻塞消費者線程直到隊列可用。 poll(time,unit)
    檢查element()檢查隊列的首元素是誰,只是顯示是誰並不是彈出,若隊列爲空則拋出NoSuchElementException peek()檢查隊列的首元素是誰,只是顯示是誰並不是彈出,若隊列爲空則顯示null

  13. SynchronousQueue:同步阻塞對列,沒有容量,它是一個不存儲元素的BlockingQueue,每一個put操作必須要等待一個take操作,否則不能繼續添加元素,反之亦然。它支持公平訪問隊列。默認情況下線程採用非公平性策略訪問隊列。使用以下構造方法可以創建公平性訪問的SynchronousQueue,如果設置爲true,則等待的線程會採用先進先出的順序訪問隊列。SynchronousQueue可以看成是一個傳球手,負責把生產者線程處理的數據直接傳遞給消費者線程。隊列本身並不存儲任何元素,非常適合傳遞性場景。Transfer()_SynchronousQueue的吞吐量高於LinkedBlockingQueue和ArrayBlockingQueue。

  14. 阻塞隊列的用途:一、生產者消費者模式 二、線程池 三、消息中間件

  15. 生產者消費者模式傳統版:synchronized wait notify 或 lock condition await singal例子:一個初始值爲0的變量,兩個線程對其交替操作,一個加1一個減1,來五輪。線程操作資源類、資源類判斷、幹活、通知。防止虛假喚醒機制。所以說,使用await()時必須放在循環中!

  16. Lock接口:Lock能夠提供比使用synchronized更加廣泛的鎖操作。它允許更靈活的結構,可以有不同的屬性,可以支持與多個Condition對象相關聯。《鎖是用來控制多個線程訪問共享資源的方式,一般來說,一個鎖能夠防止多個線程同時訪問共享資源(但是有些鎖可以允許多個線程併發的訪問共享資源,比如讀寫鎖)。在Lock接口出現之前,Java程序是靠synchronized關鍵字實現鎖功能的,併發包中新增了Lock接口(以及相關實現類)用來實現鎖功能,它提供了與synchronized關鍵字類似的同步功能,只是在使用時需要顯式地獲取和釋放鎖。雖然它缺少了隱式獲取釋放鎖的便捷性,但是卻擁有了鎖獲取與釋放的可操作性、可中斷的獲取鎖以及超時獲取鎖等多種synchronized關鍵字所不具備的同步特性。針對一個場景,手把手進行鎖獲取和釋放,先獲得鎖A,然後再獲取鎖B,當鎖B獲得後,釋放鎖A同時獲取鎖C,當鎖C獲得後,再釋放B同時獲取鎖D,以此類推。這種場景下,synchronized關鍵字就不那麼容易實現了,而使用Lock卻容易許多。》不要將獲取鎖的過程寫在try塊中,因爲如果在獲取鎖(自定義鎖的實現)時發生了異常,異常拋出的同時,也會導致鎖無故釋放。

方法:lock() lockInteruptibly() tryLock() unlock() newCondition()
通過聚合了一個同步器的子類來完成線程訪問控制的。

  1. 隊列同步器:AbstractQueuedSynchronizer是用來構建鎖或者其他同步組件的基礎框架,它使用了一個int成員變量表示同步狀態,通過內置的FIFO隊列來完成資源獲取線程的排隊工作。
    同步器的主要使用方式是繼承,子類通過繼承同步器並實現它的抽象方法來管理同步狀態,在抽象方法的實現過程中免不了要對同步狀態進行更改,這時就需要使用同步器提供的3個方法(getState()獲取當前同步狀態、setState(int newState)設置當前同步狀態和compareAndSetState(int expect,int update)使用CAS設置當前狀態保證狀態設置的原子性)來進行操作,因爲它們能夠保證狀態的改變是安全的。子類推薦被定義爲自定義同步組件的靜態內部類,同步器自身沒有實現任何同步接口,它僅僅是定義了若干同步狀態獲取和釋放的方法來供自定義同步組件使用,同步器既可以支持獨佔式地獲取同步狀態,也可以支持共享式地獲取同步狀態,這樣就可以方便實現不同類型的同步組件(ReentrantLock、ReentrantReadWriteLock和CountDownLatch等 )。
    同步器是實現鎖(也可以是任意同步組件)的關鍵,在鎖的實現中聚合同步器,利用同步器實現鎖的語義。可以這樣理解二者之間的關係:鎖是面向使用者的,它定義了使用者與鎖交互的接口(比如可以允許兩個線程並行訪問),隱藏了實現細節;同步器面向的是鎖的實現者,它簡化了鎖的實現方式,屏蔽了同步狀態管理、線程的排隊、等待與喚醒等底層操作。鎖和同步器很好地隔離了使用者和實現者所需關注的領域。

同步器的設計是基於模板方法模式的,也就是說,使用者需要繼承同步器並重寫指定的方法,隨後將同步器組合在自定義同步組件的實現中,並調用同步器提供的模板方法,而這些模板方法將會調用使用者重寫的方法。同步器可重寫的方法:tryAcquire(int) tryRelease(int) tryAcquireShared(int) tryReleaseShared(int) 同步器提供的模板方法:同步器提供的模板方法基本上分爲3類:獨佔式獲取與釋放同步狀態、共享式獲取與釋放同步狀態和查詢同步隊列中的等待線程情況。自定義同步組件將使用同步器提供的模板方法來實現自己的同步語義。

同步器依賴內部的同步隊列(一個FIFO雙向隊列)來完成同步狀態的管理,當前線程獲取同步狀態失敗時,同步器會將當前線程以及等待狀態等信息構造成爲一個節點(Node)並將其加入同步隊列,同時會阻塞當前線程,當同步狀態釋放時,會把首節點中的線程喚醒,使其再次嘗試獲取同步狀態。同步隊列中的節點(Node)用來保存獲取同步狀態失敗的線程引用、等待狀態以及前驅和後繼節點。同步器包含了兩個節點類型的引用,一個指向頭節點,而另一個指向尾節點。 同步器提供了一個基於CAS的設置尾節點的方法:compareAndSetTail(Node expect,Node update),它需要傳遞當前線程“認爲”的尾節點和當前節點,只有設置成功後,當前節點才正式與之前的尾節點建立關聯。同步隊列遵循FIFO,首節點是獲取同步狀態成功的節點,首節點的線程在釋放同步狀態時,將會喚醒後繼節點,而後繼節點將會在獲取同步狀態成功時將自己設置爲首節點。

獨佔式同步狀態獲取與釋放:通過調用同步器的acquire(int arg)方法可以獲取同步狀態,該方法對中斷不敏感,也就是由於線程獲取同步狀態失敗後進入同步隊列中,後續對線程進行中斷操作時,線程不會從同步隊列中移出,主要邏輯是:首先調用自定義同步器實現的tryAcquire(int arg)方法,該方法保證線程安全的獲取同步狀態,如果同步狀態獲取失敗,則構造同步節點(獨佔式Node.EXCLUSIVE,同一時刻只能有一個線程成功獲取同步狀態)並通過addWaiter(Node node)方法將該節點加入到同步隊列的尾部,最後調用acquireQueued(Node node,int arg)方法,使得該節點以“死循環”的方式獲取同步狀態。如果獲取不到則阻塞節點中的線程,而被阻塞線程的喚醒主要依靠前驅節點的出隊或阻塞線程被中斷來實現。總結:在獲取同步狀態時,同步器維護一個同步隊列,獲取狀態失敗的線程都會被加入到隊列中並在隊列中進行自旋;移出隊列(或停止自旋)的條件是前驅節點爲頭節點且成功獲取了同步狀態。在釋放同步狀態時,同步器調用tryRelease(int arg)方法釋放同步狀態,然後喚醒頭節點的後繼節點。

共享式同步狀態獲取與釋放:共享式獲取與獨佔式獲取最主要的區別在於同一時刻能否有多個線程同時獲取到同步狀態。
獨佔式超時獲取同步狀態:

  1. ReentrantLock類:就是支持重進入的鎖,它表示該鎖能夠支持一個線程對資源的重複加鎖。除此之外,該鎖的還支持獲取鎖時的公平和非公平性選擇。實現Lock接口。Lock對象調用lock()方法上鎖,調用unlock()方法解鎖。
    方法:lock()獲得鎖,如果鎖已經被佔用則等待、lockInterruptibly()獲得鎖,但優先響應中斷、tryLock()嘗試獲得鎖如果成功返回true,失敗false,該方法不等待立即返回、tryLock(long,unit)在給定時間內嘗試獲得鎖、unlock()釋放鎖

ReentrantLock的實現依賴於Java同步器框架AbstractQueuedSynchronizer(本文簡稱之爲AQS)。AQS使用一個整型的volatile變量(命名爲state)來維護同步狀態,馬上我們會看到,這個volatile變量是ReentrantLock內存語義實現的關鍵。

使用公平鎖時,加鎖方法lock()調用軌跡如下。1)ReentrantLock:lock()。2)FairSync:lock()。3)AbstractQueuedSynchronizer:acquire(int arg)。4)ReentrantLock:tryAcquire(int acquires)。在第4步真正開始加鎖,從上面源代碼中我們可以看出,加鎖方法首先讀volatile變量state。
在使用公平鎖時,解鎖方法unlock()調用軌跡如下。1)ReentrantLock:unlock()。2)AbstractQueuedSynchronizer:release(int arg)。3)Sync:tryRelease(int releases)。在第3步真正開始釋放鎖,從上面的源代碼可以看出,在釋放鎖的最後寫volatile變量state。公平鎖在釋放鎖的最後寫volatile變量state,在獲取鎖時首先讀這個volatile變量。根據volatile的happens-before規則,釋放鎖的線程在寫volatile變量之前可見的共享變量,在獲取鎖的線程讀取同一個volatile變量後將立即變得對獲取鎖的線程可見。

非公平鎖的釋放和公平鎖完全一樣,所以這裏僅僅分析非公平鎖的獲取。使用非公平鎖時,加鎖方法lock()調用軌跡如下。1)ReentrantLock:lock()。2)NonfairSync:lock()。3)AbstractQueuedSynchronizer:compareAndSetState(int expect,int update)。在第3步真正開始加鎖。該方法以原子操作的方式更新state變量,本文把Java的compareAndSet()方法調用簡稱爲CAS,如果當前狀態值等於預期值,則以原子方式將同步狀態設置爲給定的更新值。此操作具有volatile讀和寫的內存語義。編譯器不會對volatile讀與volatile讀後面的任意內存操作重排序;編譯器不會對volatile寫與volatile寫前面的任意內存操作重排序。組合這兩個條件,意味着爲了同時實現volatile讀和volatile寫的內存語義,編譯器不能對CAS與CAS前面和後面的任意內存操作重排序。

總結:•公平鎖和非公平鎖釋放時,最後都要寫一個volatile變量state。•公平鎖獲取時,首先會去讀volatile變量。•非公平鎖獲取時,首先會用CAS更新volatile變量,這個操作同時具有volatile讀和volatile寫的內存語義。

原子狀態:使用CAS存儲當前鎖的狀態,判斷鎖是否已經被別的線程持有。
等待隊列:所有沒有請求到鎖的線程,會進入等待隊列進行等待。待有線程釋放鎖後,系統就從隊列中喚醒一個線程,繼續工作。
阻塞原語park()/unpark():用來掛起和恢復線程,沒有得到鎖的線程會被掛起。

  1. LockSupport工具:LockSupport定義了一組的公共靜態方法,這些方法提供了最基本的線程阻塞和喚醒功能,而LockSupport也成爲構建同步組件的基礎工具。《與Thread.suspend()相比,它彌補了由於rersume()在前發生導致線程無法繼續執行的情況;和Object.wait()相比,它不需要先獲得某個對象的鎖也不會拋出異常》LockSupport定義了一組以park開頭的方法用來阻塞當前線程,以及unpark(Thread thread)方法來喚醒一個被阻塞的線程。

  2. ReentrantLock是如何實現重進入:重進入是指任意線程在獲取到鎖之後能夠再次獲取該鎖而不會被鎖所阻塞,該特性的實現需要解決以下兩個問題。1)線程再次獲取鎖。鎖需要去識別獲取鎖的線程是否爲當前佔據鎖的線程,如果是,則再次成功獲取。2)鎖的最終釋放。線程重複n次獲取了鎖,隨後在第n次釋放該鎖後,其他線程能夠獲取到該鎖。鎖的最終釋放要求鎖對於獲取進行計數自增,計數表示當前鎖被重複獲取的次數,而鎖被釋放時,計數自減,當計數等於0時表示鎖已經成功釋放。ReentrantLock是通過組合自定義同步器來實現鎖的獲取與釋放,該方法增加了再次獲取同步狀態的處理邏輯:通過判斷當前線程是否爲獲取鎖的線程來決定獲取操作是否成功,如果是獲取鎖的線程再次請求,則將同步狀態值進行增加並返回true,表示獲取同步狀態成功。成功獲取鎖的線程再次獲取鎖,只是增加了同步狀態值,這也就要求ReentrantLock在釋放同步狀態時減少同步狀態值如果該鎖被獲取了n次,那麼前(n-1)次tryRelease(int releases)方法必須返回false,而只有同步狀態完全釋放了,才能返回true。可以看到,該方法將同步狀態是否爲0作爲最終釋放的條件,當同步狀態爲0時,將佔有線程設置爲null,並返回true,表示釋放成功。

  3. 公平性獲取鎖的特性:公平性與否是針對獲取鎖而言的,如果一個鎖是公平的,那麼鎖的獲取順序就應該符合請求的絕對時間順序,也就是FIFO。tryAcquire方法與nonfairTryAcquire(int acquires)比較,唯一不同的位置爲判斷條件多了hasQueuedPredecessors()方法,即加入了同步隊列中當前節點是否有前驅節點的判斷,如果該方法返回true,則表示有線程比當前線程更早地請求獲取鎖,因此需要等待前驅線程獲取並釋放鎖之後才能繼續獲取鎖。

  4. 讀寫鎖ReentrantReadWriteLock:ReadWriteLock僅定義了獲取讀鎖和寫鎖的兩個方法,即readLock()方法和writeLock()方法。
    讀讀不互斥、讀寫互斥、寫寫互斥。如果系統中讀操作次數遠遠大於寫操作在,則讀寫鎖就可以發揮最大的功效。

讀寫鎖同樣依賴自定義同步器來實現同步功能,而讀寫狀態就是其同步器的同步狀態。
寫鎖是一個支持重進入的排它鎖。如果當前線程已經獲取了寫鎖,則增加寫狀態。如果當前線程在獲取寫鎖時,讀鎖已經被獲取(讀狀態不爲0)或者該線程不是已經獲取寫鎖的線程,則當前線程進入等待狀態。該方法除了重入條件(當前線程爲獲取了寫鎖的線程)之外,增加了一個讀鎖是否存在的判斷。如果存在讀鎖,則寫鎖不能被獲取,原因在於:讀寫鎖要確保寫鎖的操作對讀鎖可見,如果允許讀鎖在已被獲取的情況下對寫鎖的獲取,那麼正在運行的其他讀線程就無法感知到當前寫線程的操作。因此,只有等待其他讀線程都釋放了讀鎖,寫鎖才能被當前線程獲取,而寫鎖一旦被獲取,則其他讀寫線程的後續訪問均被阻塞。寫鎖的釋放與ReentrantLock的釋放過程基本類似,每次釋放均減少寫狀態,當寫狀態爲0時表示寫鎖已被釋放,從而等待的讀寫線程能夠繼續訪問讀寫鎖,同時前次寫線程的修改對後續讀寫線程可見。
讀鎖是一個支持重進入的共享鎖,它能夠被多個線程同時獲取,在沒有其他寫線程訪問(或者寫狀態爲0)時,讀鎖總會被成功地獲取,而所做的也只是(線程安全的)增加讀狀態。如果當前線程已經獲取了讀鎖,則增加讀狀態。如果當前線程在獲取讀鎖時,寫鎖已被其他線程獲取,則進入等待狀態。獲取讀鎖的實現從Java 5到Java 6變得複雜許多,主要原因是新增了一些功能,例如getReadHoldCount()方法,作用是返回當前線程獲取讀鎖的次數。讀狀態是所有線程獲取讀鎖次數的總和,而個線程各自獲取讀鎖的次數只能選擇保存在ThreadLocal中,由線程自身維護,這使獲取讀鎖的實現變得複雜。在tryAcquireShared(int unused)方法中,如果其他線程已經獲取了寫鎖,則當前線程獲取讀鎖失敗,進入等待狀態。如果當前線程獲取了寫鎖或者寫鎖未被獲取,則當前線程(線程安全,依靠CAS保證)增加讀狀態,成功獲取讀鎖。讀鎖的每次釋放(線程安全的,可能有多個讀線程同時釋放讀鎖)均減少讀狀態,減少的值是(1<<16)。
鎖降級指的是寫鎖降級成爲讀鎖。如果當前線程擁有寫鎖,然後將其釋放,最後再獲取讀鎖,這種分段完成的過程不能稱之爲鎖降級。鎖降級是指把持住(當前擁有的)寫鎖,再獲取到讀鎖,隨後釋放(先前擁有的)寫鎖的過程。

  1. Condition接口:《任意一個Java對象,都擁有一組監視器方法(定義在java.lang.Object上),主要包括wait()、wait(long timeout)、notify()以及notifyAll()方法,這些方法與synchronized同步關鍵字配合,可以實現等待/通知模式。Condition接口也提供了類似Object的監視器方法,與Lock配合可以實現等待/通知模式》
    通過lock對象調用newConditon()方法產生,利用condition對象,我們就可以讓線程在合適的時間等待或者在某一個特定時間得到通知繼續執行。condition對象調用await()等待當前線程會釋放鎖並在此等待,或者當前線程被中斷也能跳出等待。condition對象調用signalAll()喚醒其他線程調用通知當前線程後,當前線程才從await方法返回並在返回前已經獲取了鎖。一個鎖可以鎖定多個Condition,實現分組喚醒,用誰去調用await()誰等待,用誰去調用signal()誰被精準喚醒。
    方法:await()、awaitUninterruptibly()、awaitNanos(long)、await(long,unit)、awaitUtil(Date)、signal()、signalAll()

ConditionObject是同步器AbstractQueuedSynchronizer的內部類,因爲Condition的操作需要獲取相關聯的鎖,所以作爲同步器的內部類也較爲合理。每個Condition對象都包含着一個隊列(以下稱爲等待隊列),該隊列是Condition對象實現等待/通知功能的關鍵。
等待隊列:等待隊列是一個FIFO的隊列,在隊列中的每個節點都包含了一個線程引用,該線程就是在Condition對象上等待的線程,如果一個線程調用了Condition.await()方法,那麼該線程將會釋放鎖、構造成節點加入等待隊列並進入等待狀態。事實上,節點的定義複用了同步器中節點的定義,也就是說,同步隊列和等待隊列中節點類型都是同步器的靜態內部類AbstractQueuedSynchronizer.Node。Condition擁有首尾節點的引用,而新增節點只需要將原有的尾節點nextWaiter指向它,並且更新尾節點即可。上述節點引用更新的過程並沒有使用CAS保證,原因在於調用await()方法的線程必定是獲取了鎖的線程,也就是說該過程是由鎖來保證線程安全的。在Object的監視器模型上,一個對象擁有一個同步隊列和等待隊列,而併發包中的Lock(更確切地說是同步器)擁有一個同步隊列和多個等待隊列。
等待:調用Condition的await()方法(或者以await開頭的方法),會使當前線程進入等待隊列並釋放鎖,同時線程狀態變爲等待狀態。當從await()方法返回時,當前線程一定獲取了Condition相關聯的鎖。如果從隊列(同步隊列和等待隊列)的角度看await()方法,當調用await()方法時,相當於同步隊列的首節點(獲取了鎖的節點)移動到Condition的等待隊列中。
通知:調用Condition的signal()方法,將會喚醒在等待隊列中等待時間最長的節點(首節點),在喚醒節點之前,會將節點移到同步隊列中。

應用:ArrayBlockingQueue的put()insert()take()extract(),有notEmpty和notFull兩個Condition。
124. synchronized和lock有什麼區別:
1.原始構成:synchronized是關鍵字屬於JVM層面,monitorenter底層是通過monitor對象來完成,其實wait/notify等方法也依賴於monitor方法只有在同步塊或者方法中才能調用。Monitorexit。lock是具體的類(java.util.concurrent.locks.lovk)是API層面的鎖。
2.使用方法:synchronized不需要用戶去手動釋放鎖,當synchronized代碼執行完後系統會自動釋放鎖。ReentrantLock則需要用戶去手動釋放鎖,若沒有主動釋放鎖,就有可能導致出現死鎖現象,需要lock()和unlock()方法配合try/finally語句來完成。
3.等待是否可中斷:synchronized不可中斷,除非拋出異常或者正常運行完成。所以如果一個線程在等待鎖,那麼結果只有兩種可能,要麼它獲得這把鎖繼續執行,要麼它就保持等待。ReentrantLock可中斷,一是可以通過設置超時方法if(lock.tryLock(long timeout,TimeUnit unit));二是lock.lockInterruotibly()放代碼塊中,後面線程調用interrupt()方法可中斷。提供了這樣一個機制,如果一個線程正在等待鎖,那麼它依然可以收到一個通知,被告知無需再等待,可以停止工作了,這種情況對於處理死鎖是有一定幫助的。
4.加鎖是否公平:synchronized爲非公平鎖。ReentrantLock兩者都可以,默認爲非公平鎖,構造方法可以傳入boolean值,true爲公平鎖。
5.鎖綁定多個條件Condition:synchronized沒有。ReentrantLock用來實現分組喚醒需要喚醒的線程們,可以精確喚醒,而不是像synchronized要麼隨機喚醒一個線程要麼喚醒全部線程。

  1. 生產者而消費者模式:生產者和消費者模式是通過一個容器來解決生產者和消費者的強耦合問題。生產者和消費者彼此之間不直接通信,而是通過阻塞隊列來進行通信,所以生產者生產完數據之後不用等待消費者處理,直接扔給阻塞隊列,消費者不找生產者要數據,而是直接從阻塞隊列裏取,阻塞隊列就相當於一個緩衝區,平衡了生產者和消費者的處理能力。

阻塞隊列版:資源類:volatile修飾flag表明生產消費動作是否執行;atomicInteger作爲原子變量;阻塞隊列作爲容器;生產atomicInteger.incrementAndGet,blockingQueue.offer();消費blockingQueue.poll()當取得內容爲空時再等兩秒,然後消費退出;停止置flag爲false。生產者線程生產、消費者線程消費、過幾秒後叫停。

Java中的線程池類其實就是一種生產者和消費者模式的實現方式,但是我覺得其實現方式更加高明。生產者把任務丟給線程池,線程池創建線程並處理任務,如果將要運行的任務數大於線程池的基本線程數就把任務扔到阻塞隊列裏,這種做法比只使用一個阻塞隊列來實現生產者和消費者模式顯然要高明很多,因爲消費者能夠處理直接就處理掉了,這樣速度更快,而生產者先存,消費者再取這種方式顯然慢一些。

102.線程池:線程池做的工作主要是控制運行的線程的數量,處理過程中將任務放入隊列,然後在線程創建後啓動這些任務,如果線程數量超過了最大數量,超出數量的線程排隊等候,等其他線程執行完畢,再從隊列中取出任務來執行。線程池的特點:線程複用、控制最大併發數、管理線程。線程池的優點:第一降低資源消耗,通過重複利用已創建的線程降低線程創建和銷燬造成的消耗。第二提高響應速度,當任務到達時,任務可以不需要等到線程創建就能立即執行。第三提高線程的可管理性,線程是稀缺資源,如果無限制的創建,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一的分配、調優和監控。
103.線程池架構說明:線程池是通過Executor框架實現的,該框架中用到了Executor、Executors、ExecutorService(AbstractExecutorService/ ScheduledExecutorService)、ThreadPoolExecutor(ScheduledThreadPoolExecutor)這幾個類。

104.線程池編碼:Executors.newScheduledThreadPool() java8:Executors.newWorkStealingPoll(int)java8新增,使用目前機器上可用的處理器作爲它的並行級別

ExecutorService threadPool=Executors.newFixedThreadPool(int)一池固定線程 適用於爲了滿足資源管理的要求,而需要限制當前線程數量的應用場景,它適用於負載比較重的服務器。執行長期的任務,性能好很多。
threadPool.execute(()->){執行語句}); threadPool.shutdown();
public static ExecutorService newFixedThreadPool(int nThreads){return new ThreadPoolExecutor (nThreads, nThreads,0L,TimeUnit,new LinkedBlockingQueue());}
特點:1.創建一個定長的線程池,可控制線程最大併發數,超出的線程會在隊列中等待
2.創建的線程池corePoolSize和maximumPoolSize時相等的,
3.它使用的是LinkedBlockingQueue(無界的)。
4.當線程池中的線程數大於corePoolSize時,keepAliveTime爲多餘的空閒線程等待新任務的最長時間,超過這個時間後多餘的線程將被終止。這裏把keepAliveTime設置爲0L,意味着多餘的空閒線程會被立即終止。
1)如果當前運行的線程數少於corePoolSize,則創建新線程來執行任務。
2)在線程池完成預熱之後(當前運行的線程數等於corePoolSize),將任務加入LinkedBlockingQueue。
3)線程執行完1中的任務後,會在循環中反覆從LinkedBlockingQueue獲取任務來執行。FixedThreadPool使用無界隊列LinkedBlockingQueue作爲線程池的工作隊列(隊列的容量爲Integer.MAX_VALUE)。使用無界隊列作爲工作隊列會對線程池帶來如下影響。
1)當線程池中的線程數達到corePoolSize後,新任務將在無界隊列中等待,因此線程池中的線程數不會超過corePoolSize。
2)由於1,使用無界隊列時maximumPoolSize將是一個無效參數。
3)由於1和2,使用無界隊列時keepAliveTime將是一個無效參數。4)由於使用無界隊列,運行中的FixedThreadPool(未執行方法shutdown()或shutdownNow())不會拒絕任務(不會調用RejectedExecutionHandler.rejectedExecution方法)。

Executors.newSingleThreadExecutor()一池一線程 適用於需要保證順序地執行各個任務,並且在任意時間點,不會有多個線程是活動的應用場景。一個任務一個任務執行的場景。
public static ExecutorService newSingleThreadExecutor(){return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor (1, 1,0L,TimeUnit,new LinkedBlockingQueue()));}
特點:
1.創建一個單線程化的線程池,它只會用唯一的工作線程來執行任務,保證所有任務都按照指定的順序執行
2.創建的線程池corePoolSize和maximumPoolSize都設置爲1,它使用的是LinkedBlockingQueue。
SingleThreadExecutor使用無界隊列LinkedBlockingQueue作爲線程池的工作隊列(隊列的容量爲Integer.MAX_VALUE)。SingleThreadExecutor使用無界隊列作爲工作隊列對線程池帶來的影響與FixedThreadPool相同。
1)如果當前運行的線程數少於corePoolSize(即線程池中無運行的線程),則創建一個新線程來執行任務。
2)在線程池完成預熱之後(當前線程池中有一個運行的線程),將任務加入Linked-BlockingQueue。
3)線程執行完1中的任務後,會在一個無限循環中反覆從LinkedBlockingQueue獲取任務來執行。

Executors.newCachedThreadPool()一池多線程 適用於執行很多的短期異步的小程序或者負載較輕的服務器。
public static ExecutorService newCachedThreadPool (){return new ThreadPoolExecutor (0, Integer.MAX_VALUE,60L,TimeUnit,new SynchronousQueue());}
特點:
1.創建一個可緩存線程池,如果線程池長度超過處理需要,可以靈活回收空閒線程,若無可回收,則新建線程
2.創建的線程池corePoolSize設置爲0、maximumPoolSize設置爲最大值
3.它使用的是SynchronousQueue,也就說來了任務就創建線程運行,當線程空閒時間超過60s就銷燬線程。
CachedThreadPool使用沒有容量的SynchronousQueue作爲線程池的工作隊列,但CachedThreadPool的maximumPool是無界的。這意味着,如果主線程提交任務的速度高於maximumPool中線程處理任務的速度時,CachedThreadPool會不斷創建新線程。極端情況下,CachedThreadPool會因爲創建過多線程而耗盡CPU和內存資源。
1)首先執行SynchronousQueue.offer(Runnable task)。如果當前maximumPool中有空閒線程正在執行SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS),那麼主線程執行offer操作與空閒線程執行的poll操作配對成功,主線程把任務交給空閒線程執行,execute()方法執行完成;否則執行下面的步驟2)。
2)當初始maximumPool爲空,或者maximumPool中當前沒有空閒線程時,將沒有線程執行SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)。這種情況下,步驟1)將失敗。此時CachedThreadPool會創建一個新線程執行任務,execute()方法執行完成。3)在步驟2)中新創建的線程將任務執行完後,會執行SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)。這個poll操作會讓空閒線程最多在SynchronousQueue中等待60秒鐘。如果60秒鐘內主線程提交了一個新任務(主線程執行步驟1)),那麼這個空閒線程將執行主線程提交的新任務;否則,這個空閒線程將終止。由於空閒60秒的空閒線程會被終止,因此長時間保持空閒的CachedThreadPool不會使用任何資源。前面提到過,SynchronousQueue是一個沒有容量的阻塞隊列。每個插入操作必須等待另一個線程的對應移除操作,反之亦然。CachedThreadPool使用SynchronousQueue,把主線程提交的任務傳遞給空閒線程執行。

Executor框架:
Executor框架的兩級調度模型:在HotSpot VM的線程模型中,Java線程(java.lang.Thread)被一對一映射爲本地操作系統線程。Java線程啓動時會創建一個本地操作系統線程;當該Java線程終止時,這個操作系統線程也會被回收。操作系統會調度所有線程並將它們分配給可用的CPU。在上層,Java多線程程序通常把應用分解爲若干個任務,然後使用用戶級的調度器(Executor框架)將這些任務映射爲固定數量的線程;在底層,操作系統內核將這些線程映射到硬件處理器上。應用程序通過Executor框架控制上層的調度;而下層的調度由操作系統內核控制,下層的調度不受應用程序的控制。

Executor的結構:1.任務:包括被執行任務需要實現的接口,Runnable接口或Callable接口。2.任務的執行:包括任務執行機制的核心接口Executor,以及繼承自Executor的ExecutorService接口。Executor框架有兩個關鍵類實現了ExecutorService接口(ThreadPoolExecutor和ScheduledThreadPoolExecutor)。3.異步計算的結果:包括接口Future和實現Future接口的FutureTask類。

Executors是一個接口,它是Executor框架的基礎,它將任務的提交與任務的執行分離開來。扮演者線程池工廠的角色。通過Executors可以取得一個擁有特定功能的線程池。
ThreadPoolExecutor是線程池的核心實現類,用來執行被提交的任務。
ScheduledThreadPoolExecutor是一個實現類,可以在給定的延遲後運行命令,或者定期執行命令。ScheduledThreadPoolExecutor比Timer更靈活,功能更強大。
Future接口和實現Future接口的FutureTask類,代表異步計算的結果。
Runnable接口和Callable接口的實現類,都可以被ThreadPoolExecutor或Scheduled-ThreadPoolExecutor執行。

主線程首先要創建實現Runnable或者Callable接口的任務對象。工具類Executors可以把一個Runnable對象封裝爲一個Callable對象(Executors.callable(Runnable task)或Executors.callable(Runnable task,Object resule))。然後可以把Runnable對象直接交給ExecutorService執行ExecutorService.execute(Runnable command));或者也可以把Runnable對象或Callable對象提交給ExecutorService執行(Executor-Service.submit(Runnable task)或ExecutorService.submit(Callabletask))。如果執行ExecutorService.submit(…),ExecutorService將返回一個實現Future接口的對象(到目前爲止的JDK中,返回的是FutureTask對象)。由於FutureTask實現了Runnable,程序員也可以創建FutureTask,然後直接交給ExecutorService執行。最後,主線程可以執行FutureTask.get()方法來等待任務執行完成。主線程也可以執行FutureTask.cancel(boolean mayInterruptIfRunning)來取消此任務的執行。

Executor框架包含的成員組件:ThreadPoolExecutor、ScheduledThreadPoolExecutor、Future接口、Runnable接口、Callable接口和Executors。
ThreadPoolExecutor:通常使用工廠類Executors來創建。Executor框架最核心的類是ThreadPoolExecutor,它是線程池的實現類,主要由下列4個組件構成。corePool:核心線程池的大小。maximumPool:最大線程池的大小。BlockingQueue:用來暫時保存任務的工作隊列。RejectedExecutionHandler:當ThreadPoolExecutor已經關閉或ThreadPoolExecutor已經飽和時(達到了最大線程池大小且工作隊列已滿),execute()方法將要調用的Handler。通過Executor框架的工具類Executors可以創建3種類型的ThreadPoolExecutor:SingleThreadPool、FixedThreadPool和CachedThreadPool。

ScheduledThreadPoolExecutor:通常使用工廠類Executors來創建。ScheduledThreadPoolExecutor繼承自ThreadPoolExecutor。它主要用來在給定的延遲之後運行任務,或者定期執行任務。ScheduledThreadPoolExecutor的功能與Timer類似,但ScheduledThreadPoolExecutor功能更強大、更靈活。Timer對應的是單個後臺線程,而ScheduledThreadPoolExecutor可以在構造函數中指定多個對應的後臺線程數。
Executors可以創建2種類型的ScheduledThreadPoolExecutor。ScheduledThreadPoolExecutor,包含若干個線程的ScheduledThreadPoolExecutor,適用於需要多個後臺線程執行週期任務,同時爲了滿足資源管理的需求而需要限制後臺線程的數量的應用場景。SingleThreadScheduledExecutor,只包含一個線程的ScheduledThreadPoolExecutor,適用於需要單個後臺線程執行週期任務,同時需要保證順序地執行各個任務的應用場景。
ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(n)不一定會立即執行任務,是起到了計劃任務的作用,它會在指定的時間對任務進行調度。
方法:schedule(Runnable command,long delay,unit)會對任務進行一次調度 scheduleAtFixedRate(Runnable command,long initialDelay,long period,unit) 會對任務進行週期性的調度,任務調度的頻率是一定的,它是以上一個任務開始執行時間爲起點,之後的period時間調度下一次任務。
scheduleWithFixedDelay(Runnable command,long initialDelay,long period,unit)會對任務進行週期性的調度,在上一個任務結束後,再經過delay時間進行任務調度。

ScheduledThreadPoolExecutor的運行機制:DelayQueue是一個無界隊列,所以ThreadPoolExecutor的maximumPoolSize在Scheduled-ThreadPoolExecutor中沒有什麼意義(設置maximumPoolSize的大小沒有什麼效果)。
ScheduledThreadPoolExecutor的執行主要分爲兩大部分。
1)當調用ScheduledThreadPoolExecutor的scheduleAtFixedRate()方法或者scheduleWithFixedDelay()方法時,會向ScheduledThreadPoolExecutor的DelayQueue添加一個實現了RunnableScheduledFutur接口的ScheduledFutureTask。
2)線程池中的線程從DelayQueue中獲取ScheduledFutureTask,然後執行任務。ScheduledThreadPoolExecutor爲了實現週期性的執行任務,對ThreadPoolExecutor做了如下的修改。
•使用DelayQueue作爲任務隊列。
•獲取任務的方式不同(後文會說明)。
•執行週期任務後,增加了額外的處理(後文會說明)。

ScheduledThreadPoolExecutor的實現:前面我們提到過,ScheduledThreadPoolExecutor會把待調度的任務(ScheduledFutureTask)放到一個DelayQueue中。ScheduledFutureTask主要包含3個成員變量,如下。
•long型成員變量time,表示這個任務將要被執行的具體時間。
•long型成員變量sequenceNumber,表示這個任務被添加到ScheduledThreadPoolExecutor中的序號。
•long型成員變量period,表示任務執行的間隔週期。
DelayQueue封裝了一個PriorityQueue,這個PriorityQueue會對隊列中的Scheduled-FutureTask進行排序。排序時,time小的排在前面(時間早的任務將被先執行)。如果兩個ScheduledFutureTask的time相同,就比較sequenceNumber,sequenceNumber小的排在前面(也就是說,如果兩個任務的執行時間相同,那麼先提交的任務將被先執行)。

首先,讓我們看看ScheduledThreadPoolExecutor中的線程執行週期任務的過程。ScheduledThreadPoolExecutor中的線程1執行某個週期任務的4個步驟。下面是對這4個步驟的說明。
1)線程1從DelayQueue中獲取已到期的ScheduledFutureTask(DelayQueue.take())。到期任務是指ScheduledFutureTask的time大於等於當前時間。
2)線程1執行這個ScheduledFutureTask。
3)線程1修改ScheduledFutureTask的time變量爲下次將要被執行的時間。
4)線程1把這個修改time之後的ScheduledFutureTask放回DelayQueue中(Delay-Queue.add())。

接下來,讓我們看看上面的步驟1)獲取任務的過程。下面是DelayQueue.take()方法的源代碼實現。獲取任務分爲3大步驟。
1)獲取Lock。
2)獲取週期任務。
•如果PriorityQueue爲空,當前線程到Condition中等待;否則執行下面的2.2。
•如果PriorityQueue的頭元素的time時間比當前時間大,到Condition中等待到time時間;否則執行下面的2.3。
•獲取PriorityQueue的頭元素(2.3.1);如果PriorityQueue不爲空,則喚醒在Condition中等待的所有線程(2.3.2)。
3)釋放Lock。ScheduledThreadPoolExecutor在一個循環中執行步驟2,直到線程從PriorityQueue獲取到一個元素之後(執行2.3.1之後),纔會退出無限循環(結束步驟2)。

最後,讓我們看看ScheduledThreadPoolExecutor中的線程執行任務的步驟4,把ScheduledFutureTask放入DelayQueue中的過程。下面是DelayQueue.add()的源代碼實現。
添加任務分爲3大步驟。
1)獲取Lock。
2)添加任務。
•向PriorityQueue添加任務。
•如果在上面2.1中添加的任務是PriorityQueue的頭元素,喚醒在Condition中等待的所有線程。
3)釋放Lock。

Future接口和實現Future接口的FutureTask類:用來表示異步計算的結果。當我們把Runnable接口或Callable接口的實現類提交(submit)給ThreadPoolExecutor或ScheduledThreadPoolExecutor時,ThreadPoolExecutor或ScheduledThreadPoolExecutor會向我們返回一個FutureTask對象。FutureTask除了實現Future接口外,還實現了Runnable接口。因此,FutureTask可以交給Executor執行,也可以由調用線程直接執行(FutureTask.run())。根據FutureTask.run()方法被執行的時機,FutureTask可以處於下面3種狀態。1)未啓動。FutureTask.run()方法還沒有被執行之前,FutureTask處於未啓動狀態。當創建一個FutureTask,且沒有執行FutureTask.run()方法之前,這個FutureTask處於未啓動狀態。2)已啓動。FutureTask.run()方法被執行的過程中,FutureTask處於已啓動狀態。3)已完成。FutureTask.run()方法執行完後正常結束,或被取消(FutureTask.cancel(…)),或執行FutureTask.run()方法時拋出異常而異常結束,FutureTask處於已完成狀態。當FutureTask處於未啓動或已啓動狀態時,執行FutureTask.get()方法將導致調用線程阻塞;當FutureTask處於已完成狀態時,執行FutureTask.get()方法將導致調用線程立即返回結果或拋出異常。當FutureTask處於未啓動狀態時,執行FutureTask.cancel()方法將導致此任務永遠不會被執行;當FutureTask處於已啓動狀態時,執行FutureTask.cancel(true)方法將以中斷執行此任務線程的方式來試圖停止任務;當FutureTask處於已啓動狀態時,執行FutureTask.cancel(false)方法將不會對正在執行此任務的線程產生影響(讓正在執行的任務運行完成);當FutureTask處於已完成狀態時,執行FutureTask.cancel(…)方法將返回false。

FutureTask的使用:可以把FutureTask交給Executor執行;也可以通過ExecutorService.submit(…)方法返回一個FutureTask,然後執行FutureTask.get()方法或FutureTask.cancel(…)方法。除此以外,還可以單獨使用FutureTask。當一個線程需要等待另一個線程把某個任務執行完後它才能繼續執行,此時可以使用FutureTask。假設有多個線程執行若干任務,每個任務最多隻能被執行一次。當多個線程試圖同時執行同一個任務時,只允許一個線程執行任務,其他線程需要等待這個任務執行完後才能繼續執行。

FutureTask的實現:基於AbstractQueuedSynchronizer(以下簡稱爲AQS)。java.util.concurrent中的很多可阻塞類(比如ReentrantLock)都是基於AQS來實現的。AQS是一個同步框架,它提供通用機制來原子性管理同步狀態、阻塞和喚醒線程,以及維護被阻塞線程的隊列。JDK 6中AQS被廣泛使用,基於AQS實現的同步器包括:ReentrantLock、Semaphore、ReentrantReadWriteLock、CountDownLatch和FutureTask。每一個基於AQS實現的同步器都會包含兩種類型的操作,如下。•至少一個acquire操作。這個操作阻塞調用線程,除非/直到AQS的狀態允許這個線程繼續執行。FutureTask的acquire操作爲get()/get(long timeout,TimeUnit unit)方法調用。•至少一個release操作。這個操作改變AQS的狀態,改變後的狀態可允許一個或多個阻塞線程被解除阻塞。FutureTask的release操作包括run()方法和cancel(…)方法。基於“複合優先於繼承”的原則,FutureTask聲明瞭一個內部私有的繼承於AQS的子類Sync,對FutureTask所有公有方法的調用都會委託給這個內部子類。AQS被作爲“模板方法模式”的基礎類提供給FutureTask的內部子類Sync,這個內部子類只需要實現狀態檢查和狀態更新的方法即可,這些方法將控制FutureTask的獲取和釋放操作。具體來說,Sync實現了AQS的tryAcquireShared(int)方法和tryReleaseShared(int)方法,Sync通過這兩個方法來檢查和更新同步狀態。Sync是FutureTask的內部私有類,它繼承自AQS。創建FutureTask時會創建內部私有的成員對象Sync,FutureTask所有的的公有方法都直接委託給了內部私有的Sync。FutureTask.get()方法會調用AQS.acquireSharedInterruptibly(int arg)方法,這個方法的執行過程如下。1)調用AQS.acquireSharedInterruptibly(int arg)方法,這個方法首先會回調在子類Sync中實現的tryAcquireShared()方法來判斷acquire操作是否可以成功。acquire操作可以成功的條件爲:state爲執行完成狀態RAN或已取消狀態CANCELLED,且runner不爲null。2)如果成功則get()方法立即返回。如果失敗則到線程等待隊列中去等待其他線程執行release操作。3)當其他線程執行release操作(比如FutureTask.run()或FutureTask.cancel(…))喚醒當前線程後,當前線程再次執行tryAcquireShared()將返回正值1,當前線程將離開線程等待隊列並喚醒它的後繼線程(這裏會產生級聯喚醒的效果,後面會介紹)。4)最後返回計算的結果或拋出異常。FutureTask.run()的執行過程如下。1)執行在構造函數中指定的任務(Callable.call())。2)以原子方式來更新同步狀態(調用AQS.compareAndSetState(int expect,int update),設置state爲執行完成狀態RAN)。如果這個原子操作成功,就設置代表計算結果的變量result的值爲Callable.call()的返回值,然後調用AQS.releaseShared(int arg)。3)AQS.releaseShared(int arg)首先會回調在子類Sync中實現的tryReleaseShared(arg)來執行release操作(設置運行任務的線程runner爲null,然會返回true);AQS.releaseShared(int arg),然後喚醒線程等待隊列中的第一個線程。4)調用FutureTask.done()。當執行FutureTask.get()方法時,如果FutureTask不是處於執行完成狀態RAN或已取消狀態CANCELLED,當前執行線程將到AQS的線程等待隊列中等待(見下圖的線程A、B、C和D)。當某個線程執行FutureTask.run()方法或FutureTask.cancel(…)方法時,會喚醒線程等待隊列的第一個線程(見圖10-16所示的線程E喚醒線程A)。假設開始時FutureTask處於未啓動狀態或已啓動狀態,等待隊列中已經有3個線程(A、B和C)在等待。此時,線程D執行get()方法將導致線程D也到等待隊列中去等待。當線程E執行run()方法時,會喚醒隊列中的第一個線程A。線程A被喚醒後,首先把自己從隊列中刪除,然後喚醒它的後繼線程B,最後線程A從get()方法返回。線程B、C和D重複A線程的處理流程。最終,在隊列中等待的所有線程都被級聯喚醒並從get()方法返回。

Runnable接口和Callable接口的實現類:都可以被ThreadPoolExecutor或Scheduled-ThreadPoolExecutor執行。它們之間的區別是Runnable不會返回結果,而Callable可以返回結果。除了可以自己創建實現Callable接口的對象外,還可以使用工廠類Executors來把一個Runnable包裝成一個Callable。public static Callable callable(Runnable task)或public static Callable callable(Runnable task, T result)。當我們把一個Callable對象(比如上面的Callable1或Callable2)提交給ThreadPoolExecutor或ScheduledThreadPoolExecutor執行時,submit(…)會向我們返回一個FutureTask對象。我們可以執行FutureTask.get()方法來等待任務執行完成。當任務成功完成後FutureTask.get()將返回該任務的結果。例如,如果提交的是對象Callable1,FutureTask.get()方法將返回null;如果提交的是對象Callable2,FutureTask.get()方法將返回result對象。

  1. 我們可以通過ThreadPoolExecutor來創建一個線程池。ThreadPoolExecutor:public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue workQueue){this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,Executors.defaultThreadFactory(),defaultHandler);}

public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler){ }

corePoolSize,線程池中的常駐核心線程數。1.在創建了線程池後,當有請求任務來之後,就會安排池中的線程去執行請求任務,近似理解爲今日當值線程;2.當線程池中的線程數目達到corePoolSize後,就會把到達的任務放到緩存隊列中;3. 當前線程池數量超過corePoolSize時,當空閒時間達到keepAliveTime時,多餘空閒線程會被銷燬直到只剩下corePoolSize個線程爲止。默認情況下,只有當線程池中的線程數大於corePoolSize時keepAliveTime纔會起作用,直到線程池中的線程不大於corePoolSize。

maximumPoolSize,線程池中能夠容納同時執行的最大線程數,此值必須大於等於1。如果隊列滿了,並且已創建的線程數小於最大線程數,則線程池會再創建新的線程執行任務。值得注意的是,如果使用了無界的任務隊列這個參數就沒什麼效果。

keepAliveTime,多餘空閒線程的存活時間,當前線程池數量超過corePoolSize時,當空閒時間達到keepAliveTime時,多餘空閒線程會被銷燬直到只剩下corePoolSize個線程爲止
unit, keepAliveTime的單位。所以,如果任務很多,並且每個任務執行的時間比較短,可以調大時間,提高線程的利用率。

TimeUnit(線程活動保持時間的單位):可選的單位有天(DAYS)、小時(HOURS)、分鐘(MINUTES)、毫秒(MILLISECONDS)、微秒(MICROSECONDS,千分之一毫秒)和納秒(NANOSECONDS,千分之一微秒)。

workQueue 任務隊列,被提交但尚未被執行的任務,它是一個BlockingQueue接口的對象,僅用於存放Runnable對象,可以選擇以下幾個阻塞隊列:
1.有界的任務隊列ArrayBlockingQueue:是一個基於數組結構的有界阻塞隊列,此隊列按FIFO(先進先出)原則對元素進行排序。
2.無界的任務隊列LinkedBlockingQueue:一個基於鏈表結構的阻塞隊列,此隊列按FIFO排序元素,吞吐量通常要高於ArrayBlockingQueue。靜態工廠方法Executors.newFixedThreadPool()使用了這個隊列。不存在任務入隊失敗的情況除非資源耗盡。
3.直接提交的隊列SynchronousQueue:一個不存儲元素的阻塞隊列。每個插入操作必須等到另一個線程調用移除操作,否則插入操作一直處於阻塞狀態,吞吐量通常要高於Linked-BlockingQueue,靜態工廠方法Executors.newCachedThreadPool使用了這個隊列。提交的任務不會被真實的保存,而總是將新任務提交給線程執行,如果沒有空閒的線程則嘗試創建新的線程,若已達最大值執行拒絕策略。所以通常要設置很大的最大值否則會很容易執行拒絕策略。
4.優先任務隊列PriorityBlockingQueue:一個具有優先級的無限阻塞隊列。可以根據任務自身的優先級順序先後執行。

threadFactory表示生成線程池中工作線程的線程工廠,用於創建線程一般用默認線程工廠。可以通過線程工廠給每個創建出來的線程設置更有意義的名字。使用開源框架guava提供的ThreadFactoryBuilder可以快速給線程池裏的線程設置有意義的名字new ThreadFactoryBuilder().setNameFormat(“XX-task-%d”).build();
是一個接口,它只有一個方法用來創建線程Thread newThread(Runnable r);自定義線程池可以跟蹤何時創建線程、自定義線程名稱、組、優先級、守護線程。

handler拒絕策略,表示當隊列滿了並且工作線程大於等於線程池的最大線程數這個策略默認情況下是AbortPolicy,表示無法處理新任務時拋出異常。

106.線程池底層工作原理:1.在創建了線程池後,等待提交過來的任務請求。2.當調用ThreadPoolExecutor執行execute()方法添加一個請求任務時,線程池會做如下判斷:2.1如果正在運行的線程數量小於corePoolSize,那麼馬上創建線程運行這個任務;《執行這一步驟需要獲取全局鎖》2.2如果正在運行的線程數量大於或等於corePoolSize,那麼將這個任務放入隊列BlockQueue;2.3如果這時候隊列滿了且正在運行的線程數量還小於maximumPoolSize,那麼還是要創建非核心線程立刻運行這個任務;《執行這一步驟需要獲取全局鎖》2.4如果隊列滿了且正在運行的線程數量大於或等於maximumPoolSize,那麼線程池會啓動飽和拒絕策略來執行RejectedExecutionHandler.rejectedExecution()。(核心線程池是否已滿:沒滿->創建線程執行任務,滿了->隊列是否已經滿了:沒滿->將任務存儲在隊列裏,滿了->線程池是否已滿:沒滿->創建線程執行任務,滿了->按照策略處理無法執行的任務)3.當一個線程完成任務時,它會從隊列中取下一個任務來執行。4.當一個線程無事可做超過一定的時間keepAliveTime,線程池會判斷:如果當前運行的線程數大於corePoolSize,那麼這個線程就被停掉。所以線程池的所有任務完成後它最終會收縮到corePoolSize的大小。

工作線程:線程池創建線程時,會將線程封裝成工作線程Worker,Worker在執行完任務後,還會循環獲取工作隊列裏的任務來執行。我們可以從Worker類的run()方法裏看到這點。

線程池中的線程執行任務分兩種情況,如下。1)在execute()方法中創建一個線程時,會讓這個線程執行當前任務。2)這個線程執行完上圖中1的任務後,會反覆從BlockingQueue獲取任務來執行。

ThreadPoolExecutor核心代碼:
Public void execute(Runnable command){
if(command==null)throw new NullPointerException();
int c=ctl.get();
if(workerCountOf©<corePoolSize){//當前線程池的線程總數小於核心線程數
if(addWorker(command,true)) return //會將任務調度執行
c=cti.get()
}
if(isRunning©&&workQueue.offer(command){//否則進入等待隊列
int recheck=ct1.get();
if(!isRunning(recheck)&&remove(command) ) reject(command);
else if(workerCountOf(recheck))==0 addWorker(null,false);
}
else if(!addWorker(command,false)) reject(command))//進入等待隊列失敗就將任務直接提交給線程池,否則提交失敗
}

107.線程池的拒絕策略:等待隊列已經排滿了再也塞不下新任務了,同時,線程池中的max線程也達到了,無法繼續爲新任務服務,這時候我們就需要拒絕策略機制合理的處理。AbortPolicy:默認,直接拋出RejectedExecutionException異常阻止系統正常運行。 CallerRunsPolicy:調用者運行,一種調節機制,該策略既不會拋棄任務,也不會拋出異常,而是將某些任務回退到調用者,從而降低任務流量,比如說返回給main線程。 DiscardOldestPolicy:拋棄隊列中等待最久的任務,然後把當前任務加入隊列中嘗試再次提交當前任務。
DiscardPolicy:直接丟棄任務,不予任何處理也不拋出異常。如果允許任務丟失,這是最好的一種方案。
以上內置拒絕策略均實現了RejectedExecutionHandler接口。當然,也可以根據應用場景需要來實現RejectedExecutionHandler接口自定義策略,public interface RejectedExecutionHandler{void rejectedExecution(Runnable r,ThreadPoolExecutor executor)}。
實例:如記錄日誌或持久化存儲不能處理的任務。

108.線程池的方法,你用哪個?一個都不用,生產上只能使用自定義的。自定義過線程池使用:線程池不允許使用Execuyors去創建,而是通過ThreadPoolExecutor的方式,這樣的處理方式讓寫的同學更加明確線程池的運行規則,規避資源消耗的風險。

  1. 可以使用兩個方法向線程池提交任務,分別爲execute()和submit()方法。
    線程池對象.execute():execute()方法用於提交不需要返回值的任務,所以無法判斷任務是否被線程池執行成功。通過以下代碼可知execute()方法輸入的任務是一個Runnable類的實例。threadsPool.execute(new Runnable() { @Override public void run() { } });

submit()方法:用於提交需要返回值的任務。線程池會返回一個future類型的對象,通過這個future對象可以判斷任務是否執行成功,並且可以通過future的get()方法來獲取返回值,get()方法會阻塞當前線程直到任務完成,而使用get(long timeout,TimeUnit unit)方法則會阻塞當前線程一段時間後立即返回,這時候有可能任務沒有執行完。Future future = executor.submit(harReturnValuetask); try { Object s = future.get(); } catch (InterruptedException e) { // 處理中斷異常 } catch (ExecutionException e) { // 處理無法執行任務異常 } finally { // 關閉線程池 executor.shutdown(); }

可以通過調用線程池的shutdown或shutdownNow方法來關閉線程池。它們的原理是遍歷線程池中的工作線程,然後逐個調用線程的interrupt方法來中斷線程,所以無法響應中斷的任務可能永遠無法終止。但是它們存在一定的區別,shutdownNow首先將線程池的狀態設置成STOP,然後嘗試停止所有的正在執行或暫停任務的線程,並返回等待執行任務的列表,而shutdown只是將線程池的狀態設置成SHUTDOWN狀態,然後中斷所有沒有正在執行任務的線程。只要調用了這兩個關閉方法中的任意一個,isShutdown方法就會返回true。當所有的任務都已關閉後,才表示線程池關閉成功,這時調用isTerminaed方法會返回true。至於應該調用哪一種方法來關閉線程池,應該由提交到線程池的任務特性決定,通常調用shutdown方法來關閉線程池,如果任務不一定要執行完,則可以調用shutdownNow方法。

109.擴展線程池:ThreadPoolExecutor也是一個可擴展的線程池,它提供了beforeExecute()、afterExecutr()、terminated()三個接口對線程池進行控制。ThreadPoolExecutor.Worker是內部類,它是一個實現了Runnable接口的類,是工作線程。Worker.runTask()方法會被線程池以多線程模式異步調用,即Worker.runTask()會同時被多個線程訪問。可以輸出一些有用的信息用於錯誤排查等等。

110.線程池配置合理線程數:
1.任務的性質:CPU密集型任務、IO密集型任務和混合型任務。性質不同的任務可以用不同規模的線程池分開處理。
CPU密集型:CPU密集的意思是該任務需要大量的運算,而沒有阻塞,CPU一直全速運行。CPU密集任務只有在真正的多核CPU上纔可能得到加速,通過多線程。而在單核CPU上,無論你開幾個模擬的多線程該任務都不可能得到加速。CPU密集型任務配置儘可能少的線程數量,CPU核數+1個線程的線程池。

IO密集型:即該任務需要大量的IO,即大量的阻塞。在單線程上運行IO密集型的任務會導致浪費大量的CPU運行能力,所以在IO密集型任務中使用多線程可以大大的加速程序運行,即使在單核CPU上,這種加速主要就是利用了被浪費掉的阻塞時間。IO密集型大部分線程都阻塞,所以需要多配置線程數,由於IO密集型任務線程並不是一直在執行任務,則應配置儘可能多的線程,如2*Ncpu。
混合型的任務:如果可以拆分,將其拆分成一個CPU密集型任務和一個IO密集型任務,只要這兩個任務執行的時間相差不是太大,那麼分解後執行的吞吐量將高於串行執行的吞吐量。如果這兩個任務執行時間相差太大,則沒必要進行分解。可以通過Runtime.getRuntime ().availableProcessors()方法獲得當前設備的CPU個數。

2.任務的優先級:高、中和低。
優先級不同的任務可以使用優先級隊列PriorityBlockingQueue來處理。它可以讓優先級高的任務先執行。
3.任務的執行時間:長、中和短。
執行時間不同的任務可以交給不同規模的線程池來處理,或者可以使用優先級隊列,讓執行時間短的任務先執行。
4.任務的依賴性:是否依賴其他系統資源,如數據庫連接。
依賴數據庫連接池的任務,因爲線程提交SQL後需要等待數據庫返回結果,等待的時間越長,則CPU空閒時間就越長,那麼線程數應該設置得越大,這樣才能更好地利用CPU。建議使用有界隊列。有界隊列能增加系統的穩定性和預警能力,可以根據需要設大一點兒,比如幾千。有一次,我們系統裏後臺任務線程池的隊列和線程池全滿了,不斷拋出拋棄任務的異常,通過排查發現是數據庫出現了問題,導致執行SQL變得非常緩慢,因爲後臺任務線程池裏的任務全是需要向數據庫查詢和插入數據的,所以導致線程池裏的工作線程全部阻塞,任務積壓在線程池裏。如果當時我們設置成無界隊列,那麼線程池的隊列就會越來越多,有可能會撐滿內存,導致整個系統不可用,而不只是後臺任務出現問題。當然,我們的系統所有的任務是用單獨的服務器部署的,我們使用不同規模的線程池完成不同類型的任務,但是出現這樣問題時也會影響到其他任務。

111.線程池的監控:如果在系統中大量使用線程池,則有必要對線程池進行監控,方便在出現問題時,可以根據線程池的使用狀況快速定位問題。可以通過線程池提供的參數進行監控,在監控線程池的時候可以使用以下屬性。taskCount:線程池需要執行的任務數量。completedTaskCount:線程池在運行過程中已完成的任務數量,小於或等於taskCount。largestPoolSize:線程池裏曾經創建過的最大線程數量。通過這個數據可以知道線程池是否曾經滿過。如該數值等於線程池的最大大小,則表示線程池曾經滿過。getPoolSize:線程池的線程數量。如果線程池不銷燬的話,線程池裏的線程不會自動銷燬,所以這個大小隻增不減。getActiveCount:獲取活動的線程數。通過擴展線程池進行監控。可以通過繼承線程池來自定義線程池,重寫線程池的beforeExecute、afterExecute和terminated方法,也可以在任務執行前、執行後和線程池關閉前執行一些代碼來進行監控。例如,監控任務的平均執行時間、最大執行時間和最小執行時間等。這幾個方法在線程池裏是空方法。protected void beforeExecute(Thread t, Runnable r) { }

110.死鎖:是指兩個或兩個以上的進程在執行過程中,因爭奪資源而造成的一種互相等待的現象,若無外力干涉那他們都將無法推進下去,如果系統資源充足,進程的資源請求都能夠得到滿足,死鎖出現的可能性就很低,否則就會因爭奪有限的資源而陷入死鎖。產生死鎖的原因:系統資源不足、進程運行推進的順序不合適、資源分配不當。
111.如何解決死鎖:jps -l(java ps)命令定位進程號、jstack得到線程堆棧找到死鎖查看。Linux中是通過ps –ef|grep xxxx ls –l。如何避免死鎖,除了使用無鎖的函數以外,另外一種有效的做法是使用重入鎖,通過重入鎖的中斷或者限時等待可以有效規避死鎖帶來的問題。

112.線上問題定位:有很多問題只有在線上或者預發環境才能發現,而線上又不能調試代碼,所以線上問題定位就只能看日誌、系統狀態和dump線程。1)在Linux命令行下使用TOP命令查看每個進程的情況2)再使用top的交互命令數字1查看每個CPU的性能數據。3)使用top的交互命令H查看每個線程的性能信息。•第一種情況,某個線程CPU利用率一直100%,則說明是這個線程有可能有死循環,那麼請記住這個PID。
•第二種情況,某個線程一直在TOP 10的位置,這說明這個線程可能有性能問題。
•第三種情況,CPU利用率高的幾個線程在不停變化,說明並不是由某一個線程導致CPU偏高。如果是第一種情況,也有可能是GC造成,可以用jstat命令看一下GC情況,看看是不是因爲持久代或年老代滿了,產生Full GC,導致CPU利用率持續飆高,命令和回顯如下。還可以把線程dump下來,看看究竟是哪個線程、執行什麼代碼造成的CPU利用率高。執行以下命令,把線程dump到文件dump17裏。執行如下命令。dump出來的線程ID(nid)是十六進制的,而我們用TOP命令看到的線程ID是十進制的,所以要用printf命令轉換一下進制。然後用十六進制的ID去dump裏找到對應的線程。

  1.  

1 標準訪問,請問先打印短信還是email?(兩個線程同時訪問同一個資源,誰先誰後是不一定的;在兩個線程之間加上Thread.sleep(100);就能確定先後了)
也就是說如果一個實例對象的非靜態同步方法獲取鎖後,該實例對象的其他非靜態同步方法必須等待獲取鎖的方法釋放鎖後才能獲取鎖,
鎖的是當前對象this,被鎖定後,其它的線程都不能進入到當前對象的其它的synchronized方法
所有的非靜態同步方法用的都是同一把鎖——實例對象本身,
2 sendSMS()睡覺4秒鐘,請問先打印短信還是email?(剛開始等了4秒,然後先短信後email)(同一個同學同一時刻只能使用手機的一個功能!)
也就是說如果一個實例對象的非靜態同步方法獲取鎖後,該實例對象的其他非靜態同步方法必須等待獲取鎖的方法釋放鎖後才能獲取鎖,
鎖的是當前對象this,被鎖定後,其它的線程都不能進入到當前對象的其它的synchronized方法
所有的非靜態同步方法用的都是同一把鎖——實例對象本身,
3 新增普通方法openPC,請問先打印短信還是openPhone?(先開機【因爲這個沒有同步方法啊】,等了4秒,然後打印短信)
加個普通方法後發現和同步鎖無關
4 有兩部手機,請問先打印短信還是email?(先打印郵件,等了4秒,然後打印短信)
可是別的實例對象的非靜態同步方法因爲跟該實例對象的非靜態同步方法用的是不同的鎖,所以毋須等待該實例對象已獲取鎖的非靜態同步方法釋放鎖就可以獲取他們自己的鎖。
換成兩個對象後,不是同一把鎖了,情況立刻變化。
所有的 非靜態同步方法用的都是同一把鎖——實例對象本身,
5 兩個靜態同步方法,同一部手機,請問先打印短信還是email?(等了4秒,先打印短信,然後打印郵件)
6 兩個靜態同步方法,2部手機,請問先打印短信還是email?(等了4秒,先打印短信,然後打印郵件)
但是一旦一個靜態同步方法獲取鎖後,其他的靜態同步方法都必須等待該方法釋放鎖後才能獲取鎖,而不管是同一個實例對象的靜態同步方法之間,還是不同的實例對象的靜態同步方法之間,只要它們同一個類的實例對象!
所有的靜態同步方法用的也是同一把鎖——類對象本身,
7 1個靜態同步方法,1個普通同步方法,同一部手機,請問先打印短信還是email?(先打印郵件,等了4秒,打印短信)
8 1個靜態同步方法,1個普通同步方法,2部手機,請問先打印短信還是email?(先打印郵件,等了4秒,打印短信)
這兩把鎖是兩個不同的對象,所以靜態同步方法與非靜態同步方法之間是不會有競態條件的。

一個對象裏面如果有多個synchronized方法,某一個時刻內,只要一個線程去調用其中的一個synchronized方法了,
其它的線程都只能等待,換句話說,某一個時刻內,只能有唯一一個線程去訪問這些synchronized方法
鎖的是當前對象this,被鎖定後,其它的線程都不能進入到當前對象的其它的synchronized方法

加個普通方法後發現和同步鎖無關
換成兩個對象後,不是同一把鎖了,情況立刻變化。

都換成靜態同步方法後,情況又變化
所有的非靜態同步方法用的都是同一把鎖——實例對象本身,

也就是說如果一個實例對象的非靜態同步方法獲取鎖後,該實例對象的其他非靜態同步方法必須等待獲取鎖的方法釋放鎖後才能獲取鎖,
可是別的實例對象的非靜態同步方法因爲跟該實例對象的非靜態同步方法用的是不同的鎖,
所以毋須等待該實例對象已獲取鎖的非靜態同步方法釋放鎖就可以獲取他們自己的鎖。

所有的靜態同步方法用的也是同一把鎖——類對象本身,
這兩把鎖是兩個不同的對象,所以靜態同步方法與非靜態同步方法之間是不會有競態條件的。
但是一旦一個靜態同步方法獲取鎖後,其他的靜態同步方法都必須等待該方法釋放鎖後才能獲取鎖,而不管是同一個實例對象的靜態同步方法之間,
還是不同的實例對象的靜態同步方法之間,只要它們同一個類的實例對象!

120.有助於提高鎖的性能的幾點建議:1.減小鎖持有時間2.減小鎖粒度3.讀寫分離鎖來替換獨佔鎖4.鎖分離5.鎖粗化
121.Java虛擬機對鎖優化所做的努力:1.鎖偏向2.輕量級鎖3.自旋鎖4.鎖消除
122.悲觀鎖—sychronized\lock樂觀鎖—無鎖CAS
123.生產着消費者模式;
125.Future模式:是多線程開發中非常常見的一種設計模式,它的核心思想是異步調用。當我們需要調用一個函數方法時,如果這個函數執行很慢,那麼我們就要進行等待。但有時候,我們可能並不急着要結果。因此,我們可以讓被調者立即返回,讓它在後臺慢慢處理這個請求。對於調用者來說,則可以先處理一些其他任務,在真正需要數據的場合再去嘗試獲得需要的數據。對於Future模式,雖然它無法立即給出你需要的數據,但是它會返回你一個契約,將來你可以憑藉這個契約去重新獲取你需要的信息。
主要角色:main:系統啓動調用client發出請求client:返回Data對象,立即返回futureData,並開啓clentThread線程裝配realData data:返回數據的接口 futureData:future數據,構造很快,但是是一個虛擬的數據,需要裝配realData realData:真實數據,其構造是比較慢的
實現:核心接口data就是客戶端需要獲取的數據。兩個實現,realData真實數據futureData是提取realData的一個訂單可以立即返回得到的
Data接口:public interface Data{public String getResult();}
futureData:實現了一個快速返回的realData包裝,當使用其getResult方法時,如果實際的數據沒有準備好那麼程序就會阻塞,等待realdata準備好並注入到futuredata中,才最終返回數據。是realdata的代理,封裝了獲取realdata的等待過程。
realData:實現了Data接口。
Client:實現了獲取futureData,並開啓構造realdata的線程。並在接收請求後,很快的返回futuredata。
Main:負責調用client發起七個請求,並消費返回的數據。
126. Future模式實例:Callable創建線程時,構造了FutureTask對象,表示這個任務是有返回值的,使用Callable接口,告訴FutureTask我們需要的數據該如何產生。將FutureTask提交給線程池,會有返回值。接下來我們不用關心數據是如何產生的,可以去做一些額外的事情,在需要的時候通過Future.get()得到實際的數據。
Future接口:契約、訂單,通過它你可以得到真實的拘束
RunnableFuture:繼承了Future和Runnable兩個接口,run()用於構造真實的數據
FutureTask類:實現了RunnableFutur,有一個內部類sync一些實質性的工作會委託給sync類實現。而sync類最終會調用callable接口,完成實際數據的組裝工作。
Callable接口:只有一個方法call(),它會返回需要構造的實際數據。《是future框架和應用程序之間的重要接口》如果我們要實現自己的業務系統,通常需要實現自己的callable對象。此外,futureTask類也與應用密切相關,通常,我們會使用callable實例構造一個futureTask實例,並將它提交給線程池。
127. Java NIO:是new IO的簡稱,是一種可以代替Java IO的一套新的IO機制。與併發並無直接的關係,但是可以大大提高線程的使用效率。
基礎內容:通道、緩衝區、文件IO、網絡IO
網絡IO:標準的網絡IO是基於Socket的服務端的多線程模式。爲了讓服務器可以支持更多的客戶端連接,通常的做法是爲每一個客戶端連接開啓一個線程。缺點:服務線程在等待IO。想辦法將網絡的等待時間從線程中分離出來。
NIO:通道:類似於流,一個通道可以和文件或者網絡Socket對應。如果通道對應着一個Socket,那麼往這個通道中寫數據,就等於向Socket中寫入數據。緩衝區:一個內存區域或者byte數據,數據需要包裝成這樣的形式才能和通道交互。選擇器:

併發編程的藝術:
1.併發編程的挑戰:1.上下文切換的問題2.死鎖的問題3.硬件和軟件的資源限制的問題,
軟件資源限制:有數據庫的鏈接數和socket連接數等,硬件的資源限制有帶寬的上傳/下載速度、硬盤讀寫速度和CPU處理速度。
2.如何減少上下文切換
1、無鎖併發編程。多線程競爭鎖時,會引起上下文切換,沒有競爭到鎖的線程,一直處於等待狀態,所以上下文切換的時間會比較長。所以可以使用一些方法避免使用鎖。例如數據的ID利用Hash算法取模,不同的線程取不同的數據段。

2、CAS 算法。Java 的Atomic包就是使用的CAS算法更新數據,而不加鎖。

3、使用最少線程。避免創建不必要的線程。

4、協程。在單線程裏實現多任務的調度,並在單線程裏維持多個任務間的切換。
3.常見避免死鎖的方式。
1、避免一個線程同時獲得多個鎖
2、保證一個鎖只佔用一個資源(避免一個線程在鎖內同時佔用多個資源)
3、嘗試使用定時鎖,使用lock.tryLock(timeout)來替代使用內部鎖機制
4、數據褲鎖,加鎖和解鎖必須在同一個數據庫連接中
4.資源限制的挑戰
硬件資源的限制:可以用集羣執行程序
軟件資源的限制:使用連接池資源的複用

  1. concurrent包的實現
    由於Java的CAS同時具有volatile讀和volatile寫的內存語義,因此Java線程之間的通信現在有了下面4種方式。
    1)A線程寫volatile變量,隨後B線程讀這個volatile變量。
    2)A線程寫volatile變量,隨後B線程用CAS更新這個volatile變量。
    3)A線程用CAS更新一個volatile變量,隨後B線程用CAS更新這個volatile變量。
    4)A線程用CAS更新一個volatile變量,隨後B線程讀這個volatile變量。
    Java的CAS會使用現代處理器上提供的高效機器級別的原子指令,這些原子指令以原子方式對內存執行讀-改-寫操作,這是在多處理器中實現同步的關鍵。同時,volatile變量的讀/寫和CAS可以實現線程之間的通信。把這些特性整合在一起,就形成了整個concurrent包得以實現的基石。如果我們仔細分析concurrent包的源代碼實現,會發現一個通用化的實現模式。
    首先,聲明共享變量爲volatile。
    然後,使用CAS的原子條件更新來實現線程之間的同步。
    同時,配合以volatile的讀/寫和CAS所具有的volatile讀和寫的內存語義來實現線程之間的通信。
    AQS,非阻塞數據結構和原子變量類(java.util.concurrent.atomic包中的類),這些concurrent包中的基礎類都是使用這種模式來實現的,而concurrent包中的高層類又是依賴於這些基礎類來實現的。
  2. final
    與鎖和volatile相比,對final域的讀寫更像是普通變量的訪問,下面介紹final域的內存語義:
    對於final域,編譯器和處理器要遵循兩個重排序規則:
    1、在構造函數內對一個final域的寫入,與隨後把這個被構造函數對象的引用賦值給一個引用變量,這兩個操作之間不能重排序;
    這樣可以保證對象引用爲任意線程可見之前,保證對象的final域已經被正確的初始化了,而普通的域沒有這個保障。

2、初次讀一個包含final域的對象的引用,與隨後初次讀這個fina域,這兩個操作不能重排序。
這樣可以保證在讀一個對象的final域之前,一定會先讀包含這個對象的引用,防止在讀普通的域時,這個域還沒有被寫線程寫入。

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