底層的併發功能與併發語義不存在一一對應的關係。同步和條件等底層機制在實現應用層協議與策略須始終保持一致。(須要設計級別策略。----底層機制與設計級策略不一致問題)。java
簡介node
1.併發簡史。(資源利用率/公平性/便利性),進程通訊經過粗粒度通訊機制:文件/套接字/信號量/信號處理器/共享內存。高效作事----串行和異步好的平衡。linux
線程共享文件句柄和內存句柄,都有本身的程序計數器、棧、局部變量;都訪問堆中內存,須要更細粒度的內存共享機制。c++
2.線程優點程序員
下降程序開發維護成本,提高性能(將異步工做流轉爲串行工做流,模擬人類交互;下降代碼複雜度)web
(1)發揮多處理器計算能力算法
(2)建模的簡單性(多項任務,串行編寫--在關鍵位置同步)。將執行邏輯與調度機制的細節、交替執行的操做、異步IO以及資源等待等問題分離。Servlet/RMI.spring
(3)異步事件的簡化處理.使用線程和同步IO。(非阻塞io的複雜性高,不如多線程用阻塞Io)。java.nio (早期os可創建的線程少,須要非阻塞io).數據庫
(4)響應更靈敏的用戶界面。編程
3.線程風險
(1)線程安全. 竟態條件,多線程下可否返回惟一的值。
(2)活躍性問題。 無心中形成的無限循環,使得循環後的代碼沒法執行。 (活躍性的目標——-正確的事情最終會發生)。死鎖、飢餓、活鎖等。(依賴不一樣線程事件發生時序)
(3)性能問題。頻繁出現上下文切換(保存和恢復上下文,丟失局部性,更多時間在線程調度非運行)。(同步機制讓內存緩存數據無效,增長總線同步流量)
4.線程普遍使用。gui建立線程管理用戶界面事件;Timer將建立線程執行延遲任務。(框架建立線程池調用線程方法)。
訪問框架多線程的應用程序狀態的代碼路徑必須線程安全。 線程安全的要求源自多線程模塊,可能會延伸到整個程序。
(1)Timer. TimerTask在Timer管理的線程運行,不禁應用程序管理。TimerTask訪問應用程序其餘線程會訪問的數據,都須要用線程安全的方式訪問。(最好把線程安全封裝在共享對象內部)
(2)Servlet/jsp。Servlet須要知足同時被多個線程調用,須要線程安全的。與其餘servlet共享的信息,如serveltContext中的對象/HttpSession.(當多個servlet訪問其中的對象時保證線程安全)。
(3)rmi。同一個遠程對象的遠程方法會不會被多個rmi線程同時調用。(協同多個對象中共享的狀態,對遠程對象自己狀態的訪問)
(4)swing/awt. JTable不是線程安全的,事件處理器訪問由其餘線程訪問的狀態要考慮。
1、併發編程的基本理論
(一)線程安全性
核心對狀態訪問的操做進行管理(特別可變的和共享的狀態)
可變性:狀態可變的。狀態不只依賴於對象的域,還包括依賴對象的域。
共享:訪問模式(多線程訪問),同步機制synchronized、volatile、顯式鎖、原子變量。
修復線程問題的辦法:不共享變量;狀態變量改成不可變;訪問狀態變量時用同步。一開始設計線程安全類,比後改容易的多(找那些位置訪問同一個變量複雜)
封裝有助於寫出線程安全的類,易對全部訪問實現同步、易找出變量在哪些條件訪問,封裝越好越易實現線程安全。設計線程安全類起做用的特性:面向對象/不可修改性/明晰的不變性規則。(先讓程序跑起來,再優化)
1.線程安全
多個線程訪問某個類時,無論運行環境用哪一種調度方式或者這些線程將如何交替執行,而且在主調函數裏不須要額外的同步或協同,這個類都能表現出正確的行爲,就稱這個類是線程安全的。
無狀態的servlet,沒有狀態變量。大部分servelt線程安全
2.原子性
(1)竟態條件。計算的正確性取決於多線程交替執行的順序時。狀況:先檢查後執行、讀取修改寫入。
(2)複合操做。若是竟態條件的複合操做是原子操做,竟態條件就不會發生。單個狀態變量時可使用原子變量類。如AtomicLong/AutomicReference.
3.加鎖機制
(1)多線程操做的類(如servlet)有多個狀態的時候用原子類知足不了要求了,須要用加鎖機制。由於竟態條件又出現。不可變條件不能破壞(如因式分解的積與因子的不變性)
(2)內置鎖。synchronized. 加在實例方法上,默認是用的本實例作鎖對象,用在類方法(靜態方法)上用本類對象作鎖對象。synchronized(lock){}
至關於互斥體,鎖保護的代碼會原子執行。任意一個執行同步代碼塊的線程都不能看到有其餘線程在執行由同一個鎖保護的同步代碼塊。
(3)重入:獲取鎖操做的粒度是線程不是調用,實現方式是爲每一個鎖關聯一個獲取計數值和一個全部者線程。有了重入機制,子類重寫了synchronized方法時調用父類方法不會死鎖。【與pthread-posix線程不一樣,pthread以調用爲粒度】
4.用鎖保護狀態
鎖保護代碼以串行的方式訪問。
(1)僅用鎖不夠,用同步來協調對某個變量的訪問,那麼在訪問這個變量的全部位置上都要同步且用同一個鎖。(同時保證原子性和可見性)
只對寫操做同步也不對,須要在讀操做上用同步才能保證可見性。
(2)內置鎖與狀態不要緊,用內置鎖爲了不顯式的建立對象;每一個共享可變變量都要用同一個鎖,且使維護人員指導。FindBugs會找出相似bug.
(3)對含有多個變量的不變性條件,其中涉及的全部變量都要用同一個鎖保護。
5.活躍性與性能
(1)縮小同步塊的範圍,也不能太小。
(2)必須用synchronized的地方不要用原子類。 同步塊大小須要簡單性/安全性/性能平衡。
(3)執行時間長的計算或者可能沒法快速完成的操做(網絡io/控制檯io)必定不要持有鎖。
(二)對象的共享(可見性和共享對象的發佈)。同步的另外一方面:內存可見性。
1.可見性
代碼會重排序,不能對操做的執行順序進行判斷。使用正確的同步保證多線程共享。
(1)失效數據。缺少同步程序可能錯誤的狀況。(幾個變量可能僅有一部分失效),引入使人困惑的故障。
對get也要synchronized後纔會得到可見性。
(2)非原子的64位操做。long/double. 除非synchronize或者volatile保證。
(3)加鎖與可見性
加鎖不僅實現原子性還包括可見性,確保全部線程都能看到共享變量的最新值,全部執行讀操做或者寫操做的線程必須在同一個鎖上同步。
(4)volatile關鍵字
保證可見性和內存操做不會重排序。volatile關鍵字修飾的變量不會存在與寄存器或者其餘處理器不可見的地方。volatile不進行加鎖就沒有阻塞,輕量級的同步。
寫入volatile變量至關於退出鎖,讀取至關於進入鎖。不要過分依賴volatile變量。
只有能簡化代碼實現或者對同步策略驗證時才用:確保自身狀態可見性,確保引用狀態可見性,標識生命週期事件的發生(初始化或關閉)。
volatile一般用於標識某個操做完成/中斷或者其餘狀態標誌。
知足下面全部條件才用volatile
---對變量寫入不依賴原來值或者只單線程更新
--變量不會與其餘變量一塊兒歸入不變性條件
--訪問變量時不須要加鎖
2.發佈和逸出
(1)發佈位置:一個指向該對象的引用保存到其餘代碼能夠訪問的地方,或者在某個非私有方法中返回該引用,或者引用傳遞到其餘方法中。不應發佈的對象發佈叫逸出。
(2)公共靜態變量
(3)返回自身引用
(4)傳入外部方法。【不是private也不是final的方法】
這些狀況必須假定會誤用該對象,須要使用封裝的主要緣由。封裝:使得對正確性分析可能,無心中破壞設計約束條件變難。
(5)發佈一個內部類的實例。不要在構造函數讓this引用逸出,不正確構造。 this引用常見錯誤,在構造函數啓動線程。(能夠構造可是不要在構造函數啓動)
構造函數註冊監聽器或啓動線程,額能夠用私有構造函數和公共的工廠方法來實現。
3.線程封閉
單線程訪問就不須要同步了。 如connnection不是線程安全,可讓獲取connection的方法安全,每一個線程一個connection,不可能同時幾個線程共用便可。
(1)ad-hoc線程封閉。線程封閉性的職責由程序實現承擔。Ad-hoc封閉脆弱。事實上對線程封閉的對象常保存在公有變量中。
一般使用特定的子系統實現爲單線程子系統(如gui). volatile變量存在特有的線程封閉,保證只有一個線程寫便可。
(2)棧封閉。 封閉在執行線程中,只有局部變量訪問對象,任何方法都沒法得到對基本類型的引用,適合作棧封閉。棧封閉須要程序員保證不會逸出。
(3)TheadLocal類。用於防止對可變的單實例變量和全局變量共享。
如能夠將jdbc鏈接放入threadlocal中。
這種機制很方便,常常濫用(做爲隱藏方法參數手段),可是會下降代碼的可重用性。
4.不變性
(1)不可變定義:建立之後不能改狀態;全部字段final;對象正確建立(this沒逸出)。 不可變對象和不可變對象引用有差別,對象不可變可是引用能夠變化。
(2)final域。c++ const受限版本。java內存模型能保證final域初始化過程的安全性,能夠不受限制的訪問不可變對象,共享時不須要同步。能Final的都聲明final.
volatile類型發佈不可變對象。(多個狀態字段組成一個不可變對象,每次從新構建一個,而後volatile修飾)
5.安全發佈
(1)不正確發佈,正確對象破壞。 (放到公共區域的字段容易出現)
(2)不可變對象提供初始化安全性保證,沒有同步也能夠安全訪問。(發佈時也可不用同步)。沒有知足三個條件,只是Final類型的域若是可變還須要同步。
(3)安全發佈經常使用模式。
在靜態初始化塊中初始化對象引用;
將對象應用保存到volatile類型的域或者AtomicReference域中;
對象引用保存到某個正確構造對象的final類型域中;
對象引用保存到一個由鎖保存的域中。
【向線程安全的集合類加對象會保證安全發佈; 還有數據傳遞機制--Future/Exchanger】
(4)事實不可變對象。沒有額外同步,任何線程均可以安全用被安全發佈的事實不可變對象。
(5)可變對象。安全發佈只能確保發佈當時的狀態;不只發佈時要同步,每次訪問都要用同步保證修改操做可見性。
對象發佈取決於它的可見性:不可變對象任意發佈;事實不可變經過安全方式發佈;可變對象安全發佈,必須線程安全或者鎖機制保護起來。
(6)安全的共享對象。
發佈對象時說明對象的訪問方式。
線程封閉。
只讀共享(不可變和事實不可變)
線程安全共享(線程安全的對象內部實現同步)
保護對象(經過持有鎖訪問)。
(三)對象的組合。(不但願每次寫都要分析確保安全,但願組裝線程安全類)
1.設計線程安全的類。封裝使不對程序分析就能夠判斷類的安全。
(1)設計線程安全類,須要包含三個基本要素:找出構成對象的全部變量;找出約束變量的不變性;創建對象狀態訪問管理策略。
同步策略:不違背對象不變性或者後驗條件的狀況下對狀態訪問協同。(規定了如何用不可變性,封閉性,加鎖等結合,規定哪些變量由哪些鎖保護,確保方便分析維護(同步策略造成文檔)
(2)收集同步需求。狀態區間儘可能少(多用Final). 後驗條件確保遷移有效(++,17下一個條件18). ------------不變性條件和狀態轉換施加了不少約束,須要同步和封裝。
上下界NumberRange/原子性需求。 (知足約束和有效條件,藉助於原子性和封裝性)
(3)依賴狀態的操做。(先驗條件)等待某個條件爲真的各類內置機制都與加鎖機制有關(含等待和通知機制)。 使用現有庫(BlockingQueue/信號量semaphore)來實現依賴行爲。
(4)狀態全部權。根節點對象圖的子集構成狀態。(只考慮對象擁有的數據,全部權在java中沒有很好體現 hashmap---本身和Map.entry哪些對象)。
gc讓咱們避免處理全部權問題,減小了許多引用共享方面常見的錯誤。全部權與封裝性關連--對象擁有狀態,對他封裝狀態有全部權;狀態變量的全部者決定採用何種枷鎖協議維持狀態完整性,全部權意味着控制權;發佈應用,共享控制權;構造函數或者方法中傳遞進來的對象類一般不擁有他們,除非設計來轉移全部權---如同步容器封裝的工場方法。
容器類--全部權分離:容器類擁有自身狀態;客戶代碼擁有容器中對象的狀態。如ServletContext 註冊和get都必須線程安全,使用裏面的對象須要同步,容器只是暫時管理。防止併發引發的問題須要保護起來或者線程安全對象。
2.實例封閉
(1)對象封裝在類的內部,能夠將數據的訪問限制在方法上,更容易確保線程訪問數據時總持有正確的鎖。(封裝後代碼路徑已知). 對象必定不要超出他們既定的做用域(超出就逸出)。
實例封閉是構建線程安全類最簡單方式,不一樣的狀態能夠用不一樣的鎖來鎖定。本該封閉的對象發佈出去,也會破壞封閉性。
java中有不少線程封閉的實例,Collections.synchronized*()方法的用途就是把不安全的變成線程安全的。(經過裝飾器模式)---對底層的訪問都經過包裝器進行。
(2)監視器模式:將對象的全部可變狀態封裝起來,主要優點簡單。這種模式是對代碼的一種約定,對於任何一種鎖對象自始至終都要用這個對象。如private final Object myLock=new Object(). 用私有的鎖,有更好封裝性,讓客戶端得不到鎖(也能夠共有方法提供,參與同步策略);若是獲取了錯誤鎖,會引起活躍性問題。
(3)全部用到可變對象的地方都加入synchronized。 監視器模式有時性能有問題。
3.線程安全性的委託(用現有的安全類)
(1)包裝一個不變的對象作底層(如不變的Point類)。容器對象使用concurrentMap. 若是修改不變的Point類,每次用replace替換一個新值。(這個會反應實時變化;若是不要反應變化能夠返回拷貝。)
(2)多個獨立的狀態變量,能夠委託到底層安全類。多個狀態變量變量相關時,委託機制會失效。(能夠將多個變量封裝成一個類,維護不變性)----------一個類由多個獨立且線程安全的狀態變量組成,在全部操做中都不包含無效狀態變換,能夠安全性委託給底層的狀態變量(跟volatile規則相似);若是可能破壞,須要枷鎖機制。
(3)發佈底層狀態變量。狀態變量發佈的條件,它是安全的;沒有任何條件來約束它的值,變量上的操做也不存在任何狀態轉換,就能夠發佈這個變量。
似有構造函數的捕獲模式。能夠用Private A(Int[]) 來讓public A(int ,int)不產生競態條件。
4.現有的線程安全類添加功能
(1)客戶端加鎖:得到一個跟原有類同樣的鎖再添加。(或者修改原有類/擴展類)
直接用synchronize並不能得到線程安全性,與原類方法使用了不一樣的鎖,須要知道要擴展的類的鎖才能夠。
(2)組合。上面的方法強耦合。
5.將同步策略文檔化
在文檔中說明客戶代碼瞭解的線程安全性,以及代碼維護人員瞭解的同步策略。(synchronized/volatile/其餘安全類都對應於某種策略,用於併發時的數據安全性。
包括--哪些操做原子,哪些聲明爲volatile/哪些變量鎖保護/鎖保護哪些變量。 沒有聲明安全就不要認爲安全。
有些接口的安全性從基本功能上能猜想---httpsession/datasource/servletContext.
(四)基礎構建模塊
1.同步容器類. Vector/HashTable/Collections.synchronized*()方法。(這些類實現方式:狀態封裝,每一個共有方法同步)
(1)問題:ArrayIndexOutOfBoundsException,在讀取的同時,其餘線程修改容器。修改辦法----客戶端加鎖的辦法,獲取同步容器類的實例作鎖。size()和get()迭代時,仍是可能拋出異常,迭代時加鎖會下降併發性。
(2)ConcurrentModificationException. Iterator遍歷時容器發生改變。(hasNext/next會訪問是否改變的狀態,變化了就拋出這個異常)
解決:客戶端加鎖,迭代時加鎖影響併發。 另外一個解決辦法就是克隆容器,在副本迭代。(也有性能開銷,好壞取決於大小/元素上執行的操做/迭代的頻率)
(3)隱藏迭代器:必須對全部用到迭代器地方都要加鎖。hashcode/equals/tostring/containsAll/removeAll/retainAll.
2.併發容器.增長常見操做,增長併發性。(QUEUE/BlockingQueue/ConcurrentLinkingQueue/PriorityQueue) ConcurrentSkipListMap,ConcurrentSkipListSet(對應treeset/treemap)
(1)ConcurrentHashMap:使用了不一樣於同步的hashmap的策略,不是在每一個方法上同步並使得只有一個線性容器;使用了更細粒度的加鎖機制來實現更大程度共享,稱爲分段鎖(lock stripping)。
這種機制下任意數量的讀取線程能夠併發訪問Map,必定數量的寫入線程能夠併發修改map(多線程更高吞吐量,單線程輕微損耗)。
不會有ConcurrentModificationException。 返回的迭代器有弱一致性,並不是及時失敗(弱一致性容忍併發修改;遍歷容器能夠但不保證修改反應給容器);size/isempty的結果可能已通過期。
沒有實現加鎖進行獨佔訪問。(不常見的狀況須要這個功能:如保證迭代的順序)。 只有須要對map獨佔訪問時纔不用這個類。
(2)額外的map操做:putIfAbsent/remove 映射到v才刪除/replace (k,o,n) 值是o才替換爲N/replace(k,n)有值時才替換。
(3)copyOnWriteList:遍歷爲主的操做用它。每次修改就發佈一個副本。迭代多於修改時才用它。
3.阻塞隊列和生產者-消費者模式
(1)常見:線程池與工做隊列的組合;Executor執行框架。
offer方法數據項不能添加到隊列中,會返回失敗狀態;能夠用來處理負載過載的狀況,如減輕負載,將多餘的工做序列化寫入磁盤,減小生產者線程數量(經過某種方式抑制生產者線程)。設置右邊界的阻塞隊列考慮的越早越好,若是阻塞隊列不知足條件,還能夠用semaphore建立更多的阻塞數據結構。
實現:LinkedBlockingQueue/ArrayBlockingQueue/PriorityBlockingQueue(Comparable/comparator). SynchronousQueue沒有存儲(take/put一直阻塞,不用入隊和出隊;足夠的消費者時適合用這種)
(2)分解爲更簡單的組件。消費者線程永遠不會退出,程序沒法終止,後面介紹如何解決;還能夠經過Executor任務執行框架實現(自己也是生產消費者模式)。
(3)串行線性封閉。 全部權的安全遞交。除了阻塞隊列,ConcurrentMap的原子方法remove或者AutomicReference的原子方法CompareAndSet來完成工做。
(4)雙端隊列,工做密取:Deque/BolockingDeque. (生產者消費者中全部消費者有一個共享的工做隊列,工做密取模式中每一個消費者都有各自的雙端隊列,一個線程完成了雙端隊列的全部工做,能夠從其餘消費者的雙端隊列祕密的獲取工做----更高的伸縮性,不在單個共享的隊列上操做,在本身的隊列操做,須要從其餘線程獲取時不從頭從尾部獲取)。適應:既是生產者又是消費者的問題---如網頁爬蟲,爬着發現有更多的頁面要處理。 搜索圖算法--垃圾回收階段對堆進行標記(工做密取高效並行)----加入本身隊列(或在工做共享模式中加入其它工做者線程的隊列)。
4.阻塞方法與中斷方法。Blocked/waiting/timed_waiting.(與執行長時間的方法區別---阻塞是由於必須等待不受控制的事件完成)
BlockingQueue的put/take等方法會拋出異常---拋出InterruptedException時表示是一個阻塞方法。Thread的interrupt方法,用於中斷線程或查詢線程是否已經終端(每一個線程有個表示是否中斷的屬性,表示這個狀態或者終端時設置)。
中斷是協做機制,一個線程不能強制其餘線程停止(A終端B,僅僅是要求B在能夠暫停的地方中止正在運行的操做,前提B願意停下來).中斷最經常使用的操做仍是取消這個操做(中斷請求響應度越高,越容易及時取消執行時間很長的操做)。調用了拋出InterruptedException異常方法本身的方法就成了阻塞方法,就必須對中斷處理,庫代碼處理的兩種方式:傳遞InterruptedException,恢復中斷(Thread.currentThread().interrupt())。
5.同步工具類:能夠是任何對象,只要它能根據本身的狀態協調線程的控制流。
(1)閉鎖(Latch)。CountDownLatch. latch.countDown() ----等latch降到0時,在latch.await()方法處全部的線程均可以經過。
(2)FutureTask. 經過Callable實現,相似於能夠產生結果的Runnable. 能夠有三種狀態:等待運行、正在運行和運行完成。
執行結束的狀態:正常/異常結束/取消結束。Future.get行爲沒有完成會阻塞等到完成。完成了會返回結果或者拋異常。FutureTask常在executor框架中表示異步任務,還能夠執行時間較長的運算。futureTask能夠傳遞給Thread.
-----ExecutionException的處理三種狀況:Callable拋出的受檢查異常;RuntimeException;Error.
(3)信號量。Semaphore.能夠用實現資源池。new Semaphore(bound). remove是移除一個許可,release是建立許可(semaphore並不受限於建立時的大小)
(2)柵欄(Barrier)。與閉鎖的區別:全部線程都到達柵欄纔會繼續(阻塞直到某個事件發生。CylicBarrier.(全部線程都調用await()));閉鎖用於等待事件,柵欄用於等待其餘線程。
全部線程經過柵欄後,柵欄能夠重置(await中斷,全部的await拋出BrokenBarrierException.) 成功經過柵欄,await會爲每一個線程返回一個惟一的到達索引號,能夠用這些索引選擇領導者線程,並在下次迭代中由該領導線程執行一些特殊的工做。還能夠給cylincBarrier傳遞runnable在經過柵欄後執行。new Barrier的時候要指定barrier的數量和runnable.
另外一種形式的柵欄是Exchanger,一種兩方柵欄,各方在柵欄位置處交換數據(兩方執行不對稱的操做時,Exchanger會頗有用;如一個線程向緩衝區寫入數據,另外一個讀入數據-線程能夠用exchanger匯合,能夠把這倆對象安全發佈到另外一方。)
數據交換的時機取決於應用程序響應需求。最簡單緩衝區滿的時候,由填充任務進行交換;空的時候由清空任務進行交換(這樣交換次數降到了最低)。(另外一個方法填滿交換---填充到必定程度保持一段時間也要交換)。
6.構建高效且可伸縮的結果緩存
(1)計算任務緩存。 ConcurrentHashMap /Futuretask來緩存任務(putIfAbsent())。
(2)緩存逾期的問題。經過FutureTask解決,爲每一個結果指定一個逾期時間,按期掃描逾期元素。(沒有解決緩存清理問題,移除舊的計算結果)
(五)總結-主要概念和原則
1.可變狀態是相當重要的(全部併發問題均可以歸結爲如何協調併發狀態訪問。可變狀態越少,越容易確保線程安全性)。
2.儘可能將域聲明爲final類型,除非須要他們是可變的。
3.不可變對象必定是線程安全的。不可變對象能極大地下降併發編程的複雜性。他們更爲簡單且安全,能夠任意共享而無須使用加鎖或保護性複製等機制。
4.封裝有利於管理複雜性。編寫線程安全的程序時,雖能夠都保存在全局變量中,但封裝在對象中更易於維持不變性條件:將同步機制封裝在對象中,更容易遵循同步策略
5.用鎖來保護每一個可變變量。
6.當保護同一個不變性條件中的全部變量時,要使用同一個鎖。
7.在執行復合操做期間,要持有鎖。
8.若是從多個線程中訪問同一個可變變量時沒有同步機制,那麼程序會出現問題。
9.不要故做聰明地推斷出不使用同步。
10.在設計過程當中考慮線程安全,或者在文檔中明確地指出它不是線程安全的。
11.將同步策略文檔化。
2、併發應用程序的構造理論
(一)任務執行
1.線程中執行的任務。串行/每一個請求一個線程.. 天然的事務邊界。
(1)清洗的任務邊界和明確的任務執行策略。
(2)結論:任務處理過程從主線程中分離出來,使得主線程能更快的等待下一個到來的鏈接,提升響應;同時服務多個線程;代碼必須線程安全。
(3)每一個任務分配線程不足:線程生命週期開銷很高;資源消耗;穩定性。
2.Executor執行框架。任務是一組邏輯工做單元,線程是任務異步執行機制。java.util.concurrent提供了靈活的線程池做爲executor一部分。任務執行的主要對象不是thread,而是executor. public interface Executor {void executo(Runnable command);} 異步執行框架的基礎。(這個框架支持不一樣類型的執行策略)。
(1)任務的提交和執行解耦;Executor的實現還提供對聲明週期的支持,統計信息收集,應用程序管理機制和性能監視等機制。
executor基於生產者消費者模式。提交任務至關於生產者,執行任務至關於消費者。execute(runnable)-提交;execute方法的具體實現是執行。
executor的配置是一次性的,而提交任務的代碼會不斷擴充到整個程序。
(2)執行策略:what/when/where/how. 執行策略是一種資源管理工具,最佳策略取決於可用資源及對服務質量的需求。(限制併發任務數量). new Thread(runnable).start均可以用executor代替thread.
(3)線程池。 工做隊列(work queue).
Executors 靜態工場方法:newFixedThreadPool/newCachedThreadPool/newSingleThreadExecutor/newScheduledThreadPool.
newFixedThreadPool/newCachedThreadPool 返回ThreadPoolExecutor實例,能夠用用來構造專門用途的Executor.
(4)Executor的生命週期。 ExecutorService.(關閉粗暴/平緩).
public interface ExecutorService extends Executor {
void shutdown(); //平緩 關閉後提交的任務由拒絕執行處理器(rejected Execution handler來處理,拋棄任務)。
List<Runnable> shutdownNow();//粗暴
boolean isShutdown();
boolean isTerminated();
boolean awaitTermination(long timeout, TimeUnit unit) //等待到終止狀態。
throws InterruptedException;
// ... additional convenience methods for task submission
}
(5)延遲任務與週期任務。Timer負責這類任務,可是存在缺陷,應該考慮用ScheduledThreadPoolExecutor代替它。ScheduledThreadPoolExecutor的搞糟函數或者經過new ScheduledThreadPool工場方法建立。
Timer缺點:只在一個線程Timer管理的線程中運行,長時間任務會影響執行;若是拋出異常,Timer會關閉(新任務不調度--線程泄漏);只支持絕對時間,對系統時鐘變化敏感。
若是要本身構建調度任務,可使用DelayQueue,它實現了BlockingQueue,併爲ScheduledThreadPoolExecutor提供調度功能。DelayQueue管理一組Delay對象,delay對象都有一個相應的延遲時間;delayqueue只有元素逾期後才能從delayqueue中執行take.從delayQueue中返回的對象將根據延遲時間排序。
3.找出可利用的併發性。(1)異構任務併發沒有太大優點。只有大量獨立同構的任務能夠併發處理時,才能體現出將程序的工做負載分配到多個任務帶來的提高。
(2)攜帶結果的任務Callable與Future. Executor一些輔助方法能將其餘類型的任務封裝爲Callable,如Runnable/java.security.PriviledgedAction.
Callable/Runnable都是抽象的計算任務,有範圍(都有明確起始點,而且會結束)---建立/提交/開始/完成。
Executor框架中,已提交未開始的任務能夠取消,已經開始的只能響應中斷取消。
Future表示一個任務的聲明週期,提供了相應的方法,提供方法判斷完成/取消,以及得到任務的結果和取消任務等。
有許多方法能夠建立Future描述任務,ExecutorService的submit方法會返回一個Future.(提交Runnable/Callable). ---調用newTaskFor/--而後調用new FutureTask.
Future get有狀態依賴特性,調用者不依賴任務的狀態(提交和得到結果包含安全發佈屬性也確保這個方法是線程安全的)。Future.get()---兩個問題(任務自己遇到Exception/get結果以前被中斷)。
(3)CompletionService/BlockingQueue. ExecutorCompletionService. 將executor與blockingqueue融合在一塊兒,將Callable任務提交給他來執行,用相似隊列take/poll方法獲取結果,結果完成時封裝爲Future.
ExecutorCompletionService實現簡單,在構造函數中建立一個BlockingQueue,當計算完成時,調用FutureTask的done方法(重寫done,繼承FutureTask,包裝爲QueueingFuture改寫done)
多個ExecutorCompletionService能夠共享一個Executor.
(4)爲任務設置時限。不在指定的時間內完成就不須要結果了。支持時間先吃的Future.get方法Future.get(timenum,unit)
使用限時任務注意,,中止超時的任務(超時會拋出TimeoutException 可在異常處理中調用Future.cancel())。若是編寫的任務是能夠取消的能夠用Future.cancel()。
(5)建立n個任務給線程池,保留n個future.使用限時的get方法經過Future獲取每一個結果。還能夠用線程池的invokeAll方法。返回future的列表。
(二)取消與關閉
java只提供了中斷協做機制,使一個線程終止另外一個線程的當前工做。
1.任務取消。外部代碼能在某個操做正常完成以前就將其置入「完成」狀態,那麼這個操做就能夠稱爲可取消的(Cancellable).
取消的緣由:用戶請求取消;有限時間的操做;應用程序事件;錯誤;關閉。java中沒有搶佔式方法中止線程,遵循商量好的協議。
(1)方法一請求取消標誌。任務按期的查看已請求取消標誌,若是設置了它那麼任務提早結束)。cacelled=true.
可取消的任務要有取消策略(how/when/what. 其餘代碼如何請求)
(2)中斷。用標誌法一個阻塞任務可能永遠不檢查取消標誌,永遠不會結束。特殊的阻塞庫方法支持中斷(中斷是協做機制)經過這種機制通知另外一個線程,告訴它在合適的或者可能狀況下中止線程)。 雖然沒有將中斷與取消語義關聯,取消之外的其餘操做使用中斷,通常都是不合適的--難以支撐大應用。
每一個線程都有一個boolean的中斷狀態,中斷時這個線程的這個狀態爲true.(Thread中包含了中斷線程/檢查線程中斷狀態方法)
public class Thread {
public void interrupt() { ... }
public boolean isInterrupted() { ... }
public static boolean interrupted() { ... } //清除中斷狀態。
...
}庫方法Thread.sleep/Object.wait相應中斷時的操做,清除中斷狀態,拋出InterruptedException表示因爲中斷而結束。
線程在非阻塞下中斷,中斷狀態會被設置;而後根據被取消的操做輪詢這個狀態監測中斷,這樣若是沒有拋出InterruptedException,這個狀態會一直保留直到顯式清除狀態。(調用interrupt並不意味當即中止進行工做,只是傳遞請求中斷消息。)
中斷操做的正確理解:並不會真正中斷在運行的線程,只是發出中斷請求,而後線程的下一個合適時機中斷本身(這些時機稱爲取消點)-wait/sleep/join嚴格處理這種請求,本身設計好的方法能夠忽略只要用代碼對終端請求進行某種處理便可。
靜態的interrupted當心,消除狀態,調用時返回true,出發想屏蔽,必須進行處理(能夠跑出InterruptedException/或者再interrupt恢復中斷狀態).
兩個位置能夠判斷中斷:阻塞的方法調用中和循環開始處查詢中斷狀態時(顯式檢查狀態對快速響應起到必定幫助) while(true)改成while(Thread.currentThread().isInterrupted())
(3)中斷策略:正如任務Future/ExecuteService應該有取消策略同樣,線程應該有中斷策略。中斷策略:規定線程如何解釋某個中斷請求。
合理的中斷策略是線程級別或者服務級別(service level). (儘快推出,執行必要清理,通知全部者線程已經推出;其餘策略-重啓/暫停服務;對於非標準策略的線程/線程池只能用於瞭解策略的任務中)。
區分任務和線程的對中斷反應很重要。一箇中斷請求能夠有一個或多個接受者(取消當前任務或者關閉工做者線程)。
任務不是在本身擁有的線程中執行,而在某個服務(如線程池)擁有的線程中執行。對於非線程全部者的代碼應當心保留線程的狀態。(擁有線程的代碼才能處理中斷),即便非全部者線程也能響應。----大多數可阻塞的庫函數只是拋出InterruptedException.(不知道本身由哪一個線程運行,這裏體現了最合理策略:儘快推出,將中斷傳遞給調用者)。
檢查中斷請求,任務不須要放棄全部操做,能夠推遲中斷處理,能夠先執行完,而後拋出異常。
任務也不該該對執行任務線程的中斷策略進行假設,出發該任務專門設計爲服務中運行,服務包含中斷策略。拋出InterruptedException策略外,還能夠調用Thread.currentThread().interrupt();
任務代碼不應對線程中斷假設,取消代碼也不應對線程中斷策略作假設。線程應該由全部者中斷。全部者能夠將中斷策略封裝到合適的取消機制中,如shutdown方法。(線程自身擁有中斷線程,除非知道中斷含義,不然就不應中斷。)
(4)響應中斷。最多見:傳遞INterruptedException/恢復中斷interupt方法。
不想或沒法傳遞想另外的辦法----一種是恢復狀態interrupt()方法。只有實現了線程中斷策略的代碼能夠屏蔽中斷請求(常規任務和庫代碼都不應這樣作)。
(三)線程池的使用
1.任務和執行策略之間的隱性耦合:依賴性任務;線程封閉機制的任務;響應性敏感的任務;使用ThreadLocal的任務(線程池不太適合用-丟了會新生成一個,只有線程本地值受限於任務的生命週期,才適合用,線程池線程間不適合用threadlocal任務間傳遞值);
----線程飢餓死鎖:單線程executor中執行依賴任務,須要將線程池足夠大才可能不死鎖;運行時間長的任務---不死鎖等待也久,緩解辦法---限時辦法(等待時間長,將其標記失敗,從新放入隊列;總充滿了阻塞任務,代表線程池過小)
2.設置線程池大小
(1)取決於提交任務的類型以及部署的特性。
(2)是否使用jdbc/file等稀缺資源。 (與連接池等關連)
(3)不受限制的。nthreads=ncpu* ucpu* (1+w/c). ucpu是cpu使用率,w/c阻塞/計算。 (計算密集型,ncpu+1比較好)。 ncpu=Runtime.getRuntime().availableProcessors()
3.配置ThreadPoolExecutor.
(1) public ThreadPoolExecutor(int corePoolSize,int maxmumpoolsize,long keepalivetime,TimeUnit unit,Blockingqueue,threadfactory,rejectexecutionhandler).
newCachedThreadPool將線程池最大大小設爲最大,存活時間設置爲1分鐘(線程池無限擴展),需求下降自動收縮。 空閒時間超過了存活時間---就會回收超出coresize部分。
(2)管理隊列任務:無限建立線程,將致使不穩定,用固定大小的線程也會耗費掉內存。(都提交到隊列中了)。
基本任務排隊方法:有界隊列、無界隊列和同步移交。
若是用有界隊列,到了最大大小,將會根據默認的rejectexecutionhandler會拒絕,(只有線程池無界或能夠拒絕任務時synchronosQueue纔有實際價值)
(3)飽和策略:有四個:AbortPolicy/CallerRunnsPolicy/DiscardPolicy/DiscardOldPolicy. abort默認會拋出RejectedExecutionException.
調用者運行(CallerRunsPolicy)實現了一種調節機制,既不會拋棄也不會拋出異常,跑到調用者,下降新任務量。(執行時無法收到新任務,如server.accept()無法運行,新請求會保存在tcp隊列再也不應用程序隊列)---過載蔓延:線程工做隊列-應用程序-tcp層-客戶端。
除了這些策略還能夠在啓動線程的executor中限制提交速率。 用semaphore. semophore.acqure()----- semophore.release().
(4)線程工廠:ThreadFactory. (能夠定義UncaughtExceptionHandler/維護統計信息)
*/
public class MyThreadFactory implements ThreadFactory {
private final String poolName;
public MyThreadFactory(String poolName) {
this.poolName = poolName;
}
public Thread newThread(Runnable runnable) {
return new MyAppThread(runnable, poolName);
}
}應用程序中利用安全策略控制代碼庫對某些特殊代碼的訪問權限,經過executor中的privilegedThreadFactory定製本身的線程工場。這種方式建立出來的線程將與privilegedThreadFactory有相同的訪問權限、AccessControlContext、contextClassLoader. (沒有它會從submit/execute中繼承)
(5)調用構造函數後定製ThreadPoolExecutor。setter修改構造參數。 executors工廠函數建立的對象能夠顯轉換。(single等或者其餘不想轉換能夠用工廠函數executors.unconfigurableExecutorService包裝,只暴露executorService的方法)
4.擴展ThreadPoolExecutor。beforeExecute/afterExecute/terminated.
beforeExecute/afterExecute添加日誌監控等信息,線程池關閉會調用terminated,全部任務完成後,能夠用來executors釋放生命週期內分配的資源,發送通知/記錄日誌/收集finalzie統計信息。
如統計信息(beforeExecute/afterExecute 算一個任務的時間。 terminated計算平均時間)。
5.併發的謎題解答器:
}
}缺點:遍歷完了尚未結果,將會永遠等下去。解決方案之一:記錄活動任務的數量(該值爲0時解答設爲null).
解決器的幾個約束條件:時間限制-易實現(valueLatch裏面用限時的getvalue---限時await),超時關閉executor並聲明失敗;另外一個結束條件--特定於謎題標準(搜索特定數量的位置,另外取消機制)。
}併發執行任務,executor是強而靈活框架,提供大量可調節的選項(建線程/關閉線程策略,處理任務策略,處理多任務的策略,鉤子方法擴展行爲newTaskFor/before/after..)
(四)圖形GUI應用程序
1.gui單線程緣由:輸入事件的處理過程與gui的面向對象模型存在錯誤的交互。(響應事件--與重繪界面是相反的控制流程,容易引發死鎖;另外一方面mvc. 控制兩邊都調用,順序不一樣,死鎖。)。
2.串行事件處理:(1)封閉在單線程(2)幾個方法能從其餘線程安全調用。SwingUtilities.isEventDispatchThread. SwingUtilities.invokeLater(). SwingUtilities.invokeAndWait().
SwingUtilities.repaint/revalidation; SwingUtilities.添加移除監聽。
能夠用SingleThreadExecutor建立出相似SwingUtilities的工具。
3.短期的gui任務直接在事件線程執行。
4.長時間的gui任務。
(1)藉助Executors/Future. 在執行主線程建立executor,提交listener.
(2)取消:cacelButteon.addlistner (....runtask.cancel)
(3)速度標識和進度標識。FutureTask (done方法---提示。---onCompletion()) BackGround包裝FutureTask
(4)swingworker
5.共享數據模型 (1)線程安全的數據模型 (2)分解數據模型(增量變化)。
3、併發編程的性能調憂
(一)避免活躍性風險
1.死鎖與資源死鎖。(錯誤的加鎖順序)
(1)已經出現的死鎖java並不能解決。-------鎖順序死鎖。
(2)動態的鎖順序死鎖。對兩個參數加鎖,參數的順序不固定。
解決:對參數排序後再加鎖。對於兩者比較相同的參數,採起多加一個鎖的辦法。
(3)協做對象間發生的死鎖。一個加鎖的程序,調用了另外一個同步的方法。(至關於兩個鎖) 。 檢查死鎖比較難一些。
解決--開放調用。
(4)開放調用。調用方法時不須要持有鎖。(採用open call避免死鎖相似於封裝提供線程安全)分析開放調用的程序安全的多。
使同步代碼塊僅用於保護那些涉及共享狀態的操做,收縮代碼塊保護範圍還可提升伸縮性。 某些方法變爲非原子調用可接受,有些不行。丟掉原子性引起錯誤的,須要另外一種機制實現,添加監視狀態燈。
(5)資源死鎖。鏈接兩個數據庫時。 線程飢餓死鎖。
2.死鎖的避免與診斷
儘可能減小潛在的加鎖交互數量,將獲取鎖時須要遵循的協議寫入文檔。 細粒度鎖程序中用兩階段鎖策略檢查代碼中的死鎖。(找出多鎖集合;分析順序)
(1)支持定時的鎖。Lock.tryLock()(失敗後並不知道失敗的緣由---但至少能記錄,能夠平緩的重啓計算) ---能夠獲取失敗再重試。
(2)經過線程轉儲信息來分析死鎖。
線程轉儲包括各個運行中線程的棧追蹤信息,相似於發生異常的棧追蹤信息。(有哪些鎖=棧幀中的鎖=被阻塞線程等待的鎖)。生成轉儲前搜索循環圖找死鎖。
轉儲:linux/unix--- kill -3 或者 ctrl-\ windows:ctrl-Break.
顯式的Lock類不支持轉儲(5.0前,如今6.0上有信息可是仍是少)。
3。其餘活躍性危險。
(1)飢餓:沒法訪問它想要的資源。(優先級使用不當,持有沒法結束鎖的結構)儘可能不用優先級,增長平臺依賴。
(2)丟失信號:等待已通過去的信號。(如在wait以前不判斷,致使沒有notify喚醒。 用notifyall的地方用了notify,致使notify了不當的線程都沉睡了)
(3)糟糕的響應:開發框架把運行時間長或者io型任務放到後臺線程。下降後臺cpu密集型任務優先級;減小不良的鎖。
(4)活鎖:不會阻塞線程,也不繼續執行,線程不斷重複執行相同的操做,總會失敗。發生在處理事務消息的應用程序需,不成功處理就重試(總放到消息開頭,反覆調用)
錯誤的把不可恢復的錯誤做爲可修復的錯誤。
協做線程彼此相應改各自狀態,如十字路口讓路,解決:須要在重試機制中引入隨機性。
(二)性能與可伸縮性
性能與安全性和活躍性衝突。要永遠把安全性放到第一位(首先可運行,而後根據性能要求調優,考慮因素不是把性能提升到極限)。
1.對性能的思考。吞吐量、伸縮性與響應性。
多線程額外開銷:線程協調;上下文切換;線程的建立和銷燬;線程的調度等。 併發提升性能:有效利用現有資源;有新資源時利用好新資源。
(1)性能與可伸縮性。 衡量指標---多快;多少。兩方面徹底獨立,有時矛盾。(性能主要是對單線程算法調優)
可伸縮性:增長計算資源時,程序的吞吐量和處理能力能相應增長。(可伸縮性調優主要是並行化)
三層模型要比單一程序單元性能低(任務排隊-協調-數據複製)。單一系統會達處處理極限,會接受每一個工做單元更長時間/消耗更多資源換取應用程序增長資源時處理更高的負載。
服務器應用程序多少---可伸縮性;吞吐量;生產量。(交互式-延遲更重要)
(2)評估性能的權衡因素:避免不成熟優化-先正確,再提速。 決策--用某種成原本下降另外一種形式開銷,也會增長開銷換取安全性。(性能提高是併發錯誤的最大來源)。哪一種方法速度快會有錯覺(以測試爲基準不要猜想。perfbar工具能夠測試cpu忙碌程度信息)
2.Amdahl定律
串行和並行部分。
F是串行部分的佔比。全部併發程序都有串行部分(若是認爲沒有請檢查)。 如從隊列中取得數據,對結果進行彙總。
(1)各類框架隱藏的串行部分。如concurrentLinkedQueue/SynchronizedList(LinkedList) 兩者吞吐量差別來自於不一樣比例的串行部分。SynchronizedList用單個鎖,offer/move都持有;concurrentLinkedQueue用了更復雜的非阻塞隊列,使用原子引用更新連接指針。
(2)amdahl定律應用。算法考慮多處理器下的速度---對可伸縮性侷限有了解。
下降鎖粒度技術:鎖分解;鎖分段。
3線程引入的開銷.
(1)上下文切換。可運行線程大於cpu數量。(調度出線程,會致使上下文切換)。 調度--訪問Os/jvm共享數據結構,緩存缺失。(佔用了cpu週期--用於計算的減小) 。 鎖競爭阻塞會把它交換出去;越多的競爭越多的阻塞,越多的上下文切換,下降調度開銷,下降吞吐量。------無阻塞算法。
(2)內存同步。synchronized/volatile.(特殊指令--內存柵欄,抑制編譯器優化功能)。
有競爭同步和無競爭同步(volatile都是)。無競爭同步2--250個時鐘週期。jvm優化會去掉一些鎖。 jvm經過逸出分析,找出不會發布到堆的對象/鎖粒度優化。
某個線程同步會影響其餘線程性能(共享內存總線的通訊量)。
(3)阻塞:非競爭同步在jvm處理;競爭的同步須要操做系統接入,增長開銷。 自旋等待/os掛起線程。(這兩種方式取決於獲取鎖的時間與上下文切換開銷)。
4.較少鎖的競爭。(減小鎖競爭能夠提升性能/可伸縮性)。 可伸縮的最主要威脅--獨佔方式的資源鎖。
兩個因素影響競爭可能性:鎖的請求頻率以及鎖上的請求時間。--兩者的乘積越小越好。
三種方式下降鎖的競爭程度:減小鎖持有時間;下降鎖的請求頻率;使用帶有協調機制的獨佔鎖,容許更高的併發。
(1)縮小鎖範圍--快進快出。鎖無關代碼移除同步代碼塊。(實際把計算/阻塞操做從代碼塊移除時,才考慮代碼塊大小)。
(2)減少鎖的粒度(鎖分解)一個鎖之保護一個變量,提升伸縮性。(對競爭適中的鎖分解把這些鎖變爲非競爭的鎖,從而提升性能伸縮性)。
(3)鎖分段。一組獨立對象上的鎖進行分解,如concurrenthashmap.(包含16個鎖的數組)---支持16個寫入器。
劣勢:獲取多個鎖實現獨佔訪問更困難,開銷更高。(擴展映射範圍-值分到更大桶集合)
(4)避免熱點域。 每一個操做請求多個變量,粒度難下降。反覆計算的結果緩存起來,形成一些熱點域。(hashmap. size/empty. put/take時都更新一個狀態count,在concurrentmap中變成了熱點--看似優化。 避免熱點---concurrentmap將每一個段的數量計數,求的時候想加,不是全局的。)
(5)替代獨佔所方式。併發容器/讀寫鎖/不可變對象/原子變量。
讀寫鎖提升讀取的吞吐/原子變量下降熱點域的開銷。
(6)監測cpu利用率。
vmstat/mpstat. windows--perfmon.
cpu沒有充分利用的緣由:負載不足;io密集;外部限制;鎖競爭(轉儲查找)。
(7)不用對象池。對象循環利用。(下降了垃圾回收開銷,可是對高開銷意外的對象,仍然性能缺失)。對象分配線程本地的內存塊,能夠不用再堆結構上同步。協調對象池訪問,可能形成阻塞,成爲性能瓶頸。
5.比較map性能。map中只有一個鎖。concurrentmap對大多數讀操做不加鎖(寫加入了分段技術)。
6.減小上下文切換開銷。切換--請求產生日誌信息(日誌調度行爲--一種直接寫到io/另外一種後臺線程專門寫到日誌)。
經過將io操做移動到一個專門線程,提升處理效率。
(三)併發程序測試。 (幾個方面:吞吐量/響應性/可伸縮性) 安全性測試(採用測試不變形條件的形式)/活躍性測試。
1.正確性測試
(1) 跟串行同樣寫測試用例。
(2) 對阻塞操做的測試。(不多有工具建立線程和監視線程,要把失敗信息傳回主線程)。jsr166建立了測試基類(遵循規定等待全部線程結束才完成)。
該阻塞時阻塞就正常。(阻塞後再中斷它拋出Interruption)。
(3)安全性測試。(併發類-併發測試類同樣難設計)。找容易檢查的屬性。(生產者-消費者的檢查比較好的方法檢查放入隊列和從隊列取出的各個元素)。
----方法一:建立影子列表,判斷影子列表的一些東西。(這種方法會干擾線程調度)
----方法二:對順序敏感的校驗和函數檢查入列和出列(單-單);多生產者-多消費者用對順序不敏感的校驗和計算函數檢查入列和出列。(多線程不要共享校驗和變量,這樣能夠不用同步) 。同時不能讓編譯器能提早知道是什麼整數(會優化掉),用隨機數方式。
RNG生成xorshift. static int xorshift(int y){y^=(y<<6);y^=(y>>>21);y^=(y<<7);} N個線程放數N個線程取數(測試後合併起來)
爲了讓全部線程同時啓動以及同時獲得結果。(使用了開始柵欄和結束柵欄)。 開始閥門和結束閥門。
[測試線程數量要多於cpu數量]。 (測試框架要放棄指定時間內沒有完成的測試)
(4)資源管理的測試。(測試都側重與設計規範一致程度)。另外一方面測試是否沒有作不應作的事情(泄露;沒有關閉資源等)
使用堆檢查工具。
(5)使用回調。在對象生命週期的已知位置上,這些位置很是適合判斷不變性條件是否被破壞ThreadPoolExecutor調用runnable/ThreadFactory.
自定義線程和線程工廠。
2.性能測試。 性能測試是要測端到端性能/根據經驗調整限值(線程數量/緩存容量)
(1)putTakeTest增長計時功能。Barrierprimer . run() {if(!started) {started=true;starttime=t;} else endtime=t;} 提供gettime方法。
測試生產消費者在不一樣參數下的吞吐量/不一樣線程下伸縮性/如何選擇緩存大小。
(2)多種算法比較。緩存算法不高緣由:put/take都要競爭鎖,其餘實現方法競爭的位置少不少。
LinkedBlockingQueue的伸縮性高於ArrayBlockingQueue.(開始看奇怪,雖然會有更多的內存分配和gc開銷,由於優化後的鏈接隊列算法能加隊列的頭結點更新與尾結點更新分離開來。由於內存分配一般線程本地,算法能經過多執行內存分配操做來下降競爭,算法有更高的可伸縮性) ----直覺和提高性能需求是背道而馳的。
(3)響應性衡量。(更小的服務時間;更小的服務時間變更)。 公屏模式和非公平模式的差異明顯。
公平性開銷比較大主要是由於線程阻塞引發的。(沉睡/喚醒---兩次上下文交換)。
3.避免性能測試陷阱
(1)垃圾回收. 看起來很高,實際發生了gc.
(2)動態編譯.解釋執行與本地執行差距明顯
(3)對代碼路徑不真實採樣。(倒退並解釋執行)
(4)不真實的競爭程度。實際有一些線程本地計算,並不都是在鎖上競爭、
(5)無用代碼的消除,形成性能僞提高。
4.其餘測試方法。
(1)代碼審查.
(2)靜態分析工具。 findbugs.
不一致的同步;調用thread.run;未被釋放的鎖;空同步塊;雙重檢查加鎖;構造函數中啓動一個線程;通知錯誤;條件等待中的錯誤;Lock/conditon誤用;休眠或者等待時持有鎖;自旋循環;
(3)面向方面的測試技術。用spring的aop等。
(4)分析與檢測工具。 商業工具。----大多都支持線程。
4、java併發編程的一些高級主題
(一)顯式鎖
1.Lock/ReentrantLock.
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException;
void unlock();
Condition newCondition();
}
(1)lock提供無條件的、可輪詢的、定時的、可中斷的鎖獲取操做。 可見性要與同步塊同,可是在加鎖語義、調度算法、順序保證以及性能特性方面能夠不一樣。
ReentrantLock.這種更靈活的機制能提供搞好的活躍性和性能。比內置鎖複雜一些,必須在finally釋放鎖。
Lock lock = new ReentrantLock();
...
lock.lock();
try {
// update object state
// catch exceptions and restore invariants if necessary
} finally {
lock.unlock(); //沒在finally釋放是定時炸彈。
}
(2)輪詢鎖與定時鎖。 trylock 與 tryLock(long timeout, TimeUnit unit).沒成功就重試。
while (true) {
if (fromAcct.lock.tryLock()) {
try {
if (toAcct.lock.tryLock()) {
try {
if (fromAcct.getBalance().compareTo(amount)
< 0)
throw new InsufficientFundsException();
else {
fromAcct.debit(amount);
toAcct.credit(amount);
return true;
}
} finally {
toAcct.lock.unlock();
}
} } finally {
fromAcct.lock.unlock();
}
}
if (System.nanoTime() < stopTime)
return false;
NANOSECONDS.sleep(fixedDelay + rnd.nextLong() % randMod);
}
if (!lock.tryLock(nanosToLock, NANOSECONDS))
return false;
try {
return sendOnSharedLine(message);
} finally {
lock.unlock();
}
(2)可中斷的鎖獲取。lock.lockInterruptibly();
public boolean sendOnSharedLine(String message)
throws InterruptedException {
lock.lockInterruptibly();
try {
return cancellableSendOnSharedLine(message);
} finally {
lock.unlock();
}
}
private boolean cancellableSendOnSharedLine(String message)
throws InterruptedException { ... }
(3)非塊結構加鎖。 內置鎖都是基於代碼塊的。用相似分段技術下降鎖的粒度,爲鏈表節點使用一個獨立的鎖,使不一樣的線程能獨立的對鏈表的不一樣部分操做。(每一個節點的鎖保護連接指針及該節點存儲的數據。遍歷或者修改時,持有本節點的鎖,只有獲取了下一個節點的鎖時才釋放前一個節點的鎖。(連鎖式加鎖或者鎖耦合)。
2.性能因素考慮
java6.0之後差很少了;性能是一個不斷變化的指標。java5.0選取lock性能是個重要因素。
3.公平性
公平性和非公平性。 非公平性鎖--容許插隊。
公平性,永遠加到隊尾。非公平性--鎖被某個線程持有時,信發出線程纔會放入隊列。 公平性因爲掛起線程和恢復線程的開銷大大下降性能。
性能高的緣由:恢復一個被掛起的線程與該線程真正運行之間有嚴重的延遲。(延遲期間已經能處理完新來的請求)。
若是持有鎖的時間較長或者請求鎖的平均時間間隔長,應該用公平鎖。,插隊的性能提高不會體現。
4.synchronized與reentrantLock之間選擇。
內置鎖沒法知足需求的狀況下,ReentrantLock能夠做爲一種高級工具。高級功能時採用reentrantlock(定時/輪詢/中斷/公平隊列/非塊結構)
5.讀寫鎖。(其實內部是一個鎖,提供了一個互斥量) 一個調用tryaquireshared/一個調用tryacquire()
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
(1)readwritelock可選實現:釋放優先;讀線程插隊;重入性;降級;升級。
(2)ReentrantReadWriteLock.只能有惟一的全部者,並由得到該鎖的線程釋放。
(二)構建自定義的同步工具
1.狀態依賴的管理. 狀態依賴類---不知足某個條件的時候阻塞或者拋異常。
依賴狀態的操做能夠一直阻塞直到能夠繼續運行,比先失敗再實現起來要更爲方便。依賴管理加鎖模式有點特別:鎖在操做的執行過程當中被釋放與從新獲取。
構成前提條件的狀態變量必須由鎖來保護。
(1)拋失敗給調用者。給使用者增長負擔。 調用者處理,除了沉睡一段時間重試外,還能夠調用thread.yield讓出時間片。
(2)輪詢與休眠來實現阻塞。若是符合條件就睡一段時間。方便調用者處理。但沉睡時間須要在響應性和cpu使用率間平衡。
處理InterruptedException.,能夠實現取消。 若是有掛起和提醒方法簡化實現。
(3)條件隊列。wait/notify/notifyall() 這三個方法構成了內部條件隊列的api.
條件隊列名字:使得一組線程(集合)可以經過某種方式來等待特定的條件變成真。(傳統隊列是元素,這裏是等待條件的線程)。
object--wait會釋放鎖,可是須要發生特定事情喚醒。
用條件隊列優化了:cpu效率/上下文切換開銷/響應性。 (可是事先的功能與輪詢休眠是相同的) 支持定時的,能夠用定時版本的object.wait.
2.使用條件隊列
(1)條件謂詞。使得某個操做成爲依賴操做的前提條件。(須要將條件謂詞與等待操做都寫入文檔)。
條件等待的三元關係:加鎖/wait方法/一個條件謂詞。 條件謂詞中能夠包含多個狀態變量,條件謂詞只能由一個鎖來保護,鎖對象與條件隊列對象必須是同一個對象。wait被喚醒後在返回測試條件謂詞前必須再獲取鎖。
條件謂詞不爲真---必須等待。
(2)過早喚醒。notifyAll會把全部喚醒,大多數都是過早喚醒。wait方法還能夠僞裝返回不必定notify. 在從新獲取鎖的時間中可能有其餘線程獲取了這個鎖改變了標誌。
基於這個緣由,從wait中喚醒得到鎖後,須要再次測試條件謂詞。
void stateDependentMethod() throws InterruptedException {
// condition predicate must be guarded by lock
synchronized(lock) {
while (!conditionPredicate())
lock.wait();
// object is now in desired state
}
}
使用條件等待時:一般都有一個條件謂詞(狀態測試);在調用wait以前先測試,從wait中返回須要再次測試;在一個循環中用wait;確保使用與條件隊列相關鎖來保護條件謂詞相關的變量;調用wait/notify/notifyall時持有相關鎖;檢查條件謂詞後開始操做前不要釋放鎖。
(3)丟失的信號。等待一個已經爲真的條件。
出現緣由:開始等待前沒有檢查條件謂詞。(通常都是編碼錯誤)
(4)通知。notify/notifyall.每當等待一個條件時必定要經過某種方式發出通知。
只有知足兩個條件才用notify不用notifyall:全部等待線程類型相同(只有一個條件謂詞,wait後返回執行相同操做);單進單出(每次通知最多喚醒一個)。
優化:能夠在狀態變幻時才wait和notify.
(5)子類的安全問題。不提倡用狀態變幻時才wait和notify等優化,由於對實現有了依賴,子類可能會有安全問題。(等待和通知的協議要向子類公開)
禁止子類化。(若是子類破壞了notify,須要基類修復)。 不該該過分依賴notify. 用Notifyall子類更安全。
(6)封裝條件隊列。將條件隊列封裝,除了使用條件隊列類,不在其餘地方訪問它。(能夠單獨用不被公開的條件隊列對象,不用內置的)。 一般應該封裝起來。
(7)入口協議和出口協議。每一個依賴操做和修改依賴狀態的操做,都該定義一個入口協議和出口協議。入口協議是操做的條件謂詞;出口協議包括:檢查被該操做修改的全部狀態變量,並確認他們是否使某個其餘的條件謂詞爲真. 在AbstractQueuedSynchronizer中使用出口協議,(這個類不是同步類執行本身的通知,而是要求同步器返回一個值表示該類的操做已經解除一個或者多個等待線程的阻塞),這種明確的api調用需求使得難以忘記通知。
3.顯式的Condition對象
(1)lock能夠有多個Condition對象,有更豐富和靈活的功能。會繼承lock的公平性。
public interface Condition {
void await() throws InterruptedException;
boolean await(long time, TimeUnit unit)
throws InterruptedException;
long awaitNanos(long nanosTimeout) throws InterruptedException;
void awaitUninterruptibly();
boolean awaitUntil(Date deadline) throws InterruptedException;
void signal();
void signalAll();
}
(2)選擇用conditon,與選擇用lock是同樣的。
4.Synchronizer剖析。 semaphore/reenterantLock均可以作閥門--可經過/等待/取消/支持公平和非公平隊列操做。
他們都基於AbstractQueuedSynchronizer(AQS)實現,AQS是構建鎖和同步器的框架。除了上面兩個外還有CountDownLatch/ReentrantReadWriteLock/SynchronousQueue/FutureTask.
AQS解決了實現同步器時涉及的大量細節問題(如等待線程用fifo隊列的操做順序),不一樣的同步器定義了靈活的標準來經過仍是等待。減小實現;沒必要在處理多個位置上的競爭問題。
5.AbstractQueuedSynchronizer.
(1)基本的操做是獲取和釋放操做。 (鎖或者許可)。
一個類想成爲狀態依賴的類,那麼它必須擁有必定的狀態。AQS負責同步器類中的狀態,管理了一個整數狀態(經過getState/setState/COmpareAndSetState)等protected進行操做,整數能夠表示任意含義。
reenterantlock用它表示進入鎖次數;FutureTask表示狀態。
同步器不一樣--獲取操做能夠是獨佔(reenterantLock), 也能夠是非獨佔狀態(semaphore/countdownlatch).
----首先同步器判斷當前狀態是否容許得到操做,若是是線程容許執行/不容許(阻塞)----由同步器的語義定義的。
----其次更新同步器的狀態。獲取同步器的線程可能會對其餘線程獲取該同步器是否形成影響。
同步器支持獨佔的操做,須要實現一些保護方法----tryAcquire/tryRelease/isHeldExclusively.
支持共享操做,須要實現方法---tryAcquireShared/tryReleaseShared等方法。 (這寫方法供acquire/acquireshared/release/releaseshared調用)。
經過這些方法的返回值狀態告知獲取和釋放操做是否成功。tryAcquireShared返回負值,表示獲取操做失敗,返回0值表示同步器以獨佔方式獲取,正值以非獨佔方式獲取。 釋放操做使得全部阻塞線程回覆執行,須要tryRelease和tryReleased方法都爲true.
閉鎖(在打開狀態以前tryAcquireShared的獲取狀態都是失敗)。 同步類都不是經過擴展AQS實現的,都是委託給AQS的子類。
6.concurrent中的AQS。
(1)ReentrantLock. 除了實現獨佔操做的方法,還維護一個owner變量保存全部者線程。只有獲取鎖和釋放的時候才修改這個變量。檢查owner確保當前線程在Unlock前得到了鎖。用它肯定重入仍是競爭。
用CompareAndSet修改狀態。newCondition是Lock的內部類。
(2)Semaphore與CountDownLatch。保存當前許可的數量。 countdownlatch保存的是當前的計數值。
(3)FutureTask。Future.get(). AQS保存任務的狀態。(運行/完成/取消)。維護引用指向正在執行計算任務的線程,若是取消,線程中斷。
(4)ReentrantReadWriteLock。單個AQS子類同時管理讀取加鎖和寫入加鎖。用了一個16位狀態保存寫入鎖計數;另外一個16位狀態保存讀取鎖計數。讀取鎖上用共享的獲取方法與釋放方法,寫入鎖上用獨佔的。
AQS內部維護等待隊列,記錄了某線程請求的是獨佔訪問仍是共享訪問。
(三)原子變量和非阻塞同步機制
非鎖的方案方案設計和實現上都複雜的多,在可伸縮性和活躍性有巨大的優點,更細的粒度調度減小調度開銷。原子變量可作更好的volatile類型變量,更適合於作計數器、序列發生器和統計數據收集。
1.鎖的劣勢。鎖 細粒度的操做,在鎖上存在競爭時調度與工做的開銷比很高。
volatile變量-輕量級的同步,可是有侷限不能用於構建原子複合操做。在一個變量依賴其餘變量,或者當變量的新值依賴於舊值時,就不能用volatile變量。(不能用互斥體/計數器).
鎖的缺點:等待鎖不能作其餘事情,持有鎖狀況下被延遲執行/阻塞線程優先級高-持有的優先級低(優先級反轉)。
鎖對細粒度的操做(如遞增)是高代價的事情。
2.硬件對併發支持。鎖是悲觀機制。
樂觀方法---不發生干擾時完成更新,須要藉助衝突檢查機制。
(1)比較並交換.
public synchronized int compareAndSwap(int expectedValue,
int newValue) {
int oldValue = value;
if (oldValue == expectedValue)
value = newValue;
return oldValue;
}
public synchronized boolean compareAndSet(int expectedValue,
int newValue) {
return (expectedValue
== compareAndSwap(expectedValue, newValue));
} 根據true/false來進行重試。
(2)非阻塞的計數器。 simulateCas value (這個能夠用原子變量類替代)。競爭比較低的時候,性能遠遠超過了基於鎖的計數器。
cas的缺點:讓調用者處理競爭問題,鎖中自動處理競爭。cas性能隨着處理器數量變化很大。(非競爭的鎖在快速代碼路徑上的開銷大約是cas開銷兩倍)。
(3)JVM對CAS的支持。java 5.0之後才加入的。
3.原子變量類
相似於一種泛化的volatile變量。 公有12個原子變量類,分爲4組:標量類、更新器類、數組類以及複合變量類。最經常使用的是標量類--AtomicInteger/AtomicLong/AtomicBoolean/AtomicReference,全部這些都支持CAS.AtomicInteger/AtomicLong還支持算術運算。
原子數組類(INteger/Long/Reference)的元素能夠實現原子更新(爲數組的元素提供了volatile語義,普通數組不具有-volatile只在引用上volatile).
原子變量類擴展了Number沒有擴展包裝類。(實際上他們不能擴展,都是不變類型;而原子變量是可變類型)。原子變量沒有重定義hashcode/equals,每一個實例不一樣---不宜作散列容器的鍵值。
(1)更好的volatile。
(2)性能比較,鎖與原子變量。在高度競爭的狀況下,鎖的性能優於原子變量性能。(鎖阻塞會下降生產者線程)。真實的壓力下,原子變量性能更好。
ThreadLocal性能最好。
4.非阻塞算法。
鎖算法有活躍性過帳,非阻塞沒有。(一個線程失敗或掛起不會致使其餘線程也失敗或掛起,成爲非阻塞算法;若是算法的每一個步驟中都存在某個線程可以執行下去,無所算法。) CAS--無鎖/非阻塞。
非阻塞算法---實現棧/隊列/優先隊列/散列表等。
(1)非阻塞的棧。非阻塞更復雜。
關鍵在於:如何將原子修改的範圍縮小到單個變量上。
public void push(E item) {
Node<E> newHead = new Node<E>(item);
Node<E> oldHead;
do {
oldHead = top.get();
newHead.next = oldHead;
} while (!top.compareAndSet(oldHead, newHead));
}
(2) 非阻塞的鏈表。
更復雜,須要單獨維護兩個指針,頭指針和尾指針。 兩個指針位於尾部:當前最後一個元素的next指針以及尾節點。(緣由須要更新上一個節點的指針,而棧只須要更新top指向對象的指針便可)。
技巧---多步驟更新確保老是處於一致狀態。B到達發現A在更新,B等待A更新完成互不干擾(缺點,A失敗後會丟失一個操做)。設計算法---一個線程失敗讓另外一線程繼續執行。
技巧二:若是B到達時發現A在修改數據結構,數據結構中有足夠的信息,讓B能夠幫A完成更新操做。(多操做一次沒事)。
空隊列包含哨兵節點(sentinel)/啞節點(Dummy)節點。初始化首尾節點都應該指向啞節點。
@ThreadSafe
public class LinkedQueue <E> {
private static class Node <E> {
final E item;
final AtomicReference<Node<E>> next;
public Node(E item, Node<E> next) {
this.item = item;
this.next = new AtomicReference<Node<E>>(next);
}
}
private final Node<E> dummy = new Node<E>(null, null);
private final AtomicReference<Node<E>> head
= new AtomicReference<Node<E>>(dummy);
private final AtomicReference<Node<E>> tail
= new AtomicReference<Node<E>>(dummy);
public boolean put(E item) {
Node<E> newNode = new Node<E>(item, null);
while (true) {
Node<E> curTail = tail.get();
Node<E> tailNext = curTail.next.get();
if (curTail == tail.get()) {
if (tailNext != null) {
// Queue in intermediate state, advance tail
tail.compareAndSet(curTail, tailNext);
} else {
// In quiescent state, try inserting new node
if (curTail.next.compareAndSet(null, newNode)) {
// Insertion succeeded, try advancing tail
tail.compareAndSet(curTail, newNode);
return true;
} }} } }
}
(3)原子的域更新器。
上面說了linkedqueue的算法,實際中略有區別。ConcurrentLinkedQueue中沒有用原子引用表示Node,而是用普通的volatile引用。
private class Node<E> {
private final E item;
private volatile Node<E> next;
public Node(E item) {
this.item = item;
}
}
private static AtomicReferenceFieldUpdater<Node, Node> nextUpdater
= AtomicReferenceFieldUpdater.newUpdater(Node.class,Node.class,"next"); 這個next必須是volatile字段,第一個字段是第三個字段所在的類,第二個字段是更新對象next所屬的類。 這個是一個基於反射的視圖,能從已有的volatile域上用cas.(更新器沒有構造函數,只有newUpdater工場方法,制定類和域的名字)。
域更新器類與某個特定的實例關聯在一塊兒,能夠更新目標雷任意實例的域;原子性保證性比普通元子類弱一些(沒法保證底層的域不被修改)。
用nextUpdater的compareAndSet方法更新Node的next,方法繁瑣爲了提高性能。(分配頻繁,生命週期短暫的對象,如連接節點,若是能去掉每一個Node的AtomicReference建立過程會下降插入開銷)。 極少狀況採用域更新器。
(4)ABA問題。
比較並交換---在判斷與替換的中間,變更過一次。
解決辦法:不更新某個引用的值,而是更新兩個值,一個引用和一個版本號。 AtomicStampedReference(以及AtomicMarkableReference)支持兩個變量上執行原子條件更新。(對象引用布爾值的二元組)
(四)java內存模型
1.什麼是jmm,爲何須要他。
(1)串行結果不變,全部操做都容許。 因此又了重排序技術;全局寄存器。JMM規定了JVM遵循的最小保證,規定了變量寫入什麼時候對其餘線程可見。
(2)平臺的內存模型。每一個處理器都有本身的緩存,按期與主線程協調(容許處理器看到不一樣值)
確保處理器瞭解其餘處理器作的操做,開銷很大;特殊指令-內存柵欄/柵欄,須要共享時指令實現存儲協調保證。 樂觀的模型--串行一致性(現代處理器都不提供這種一致性)
(3)重排序
JMM使得不一樣線程的執行順序不一樣。 同步會限制編譯器/運行時/硬件對內存操做的重排序方式,在實施重排序時不破壞jmm可見性保證。
(4)java內存模型簡介--經過各類操做定義的(變量讀寫,監視器加鎖解鎖,線程的啓動合併操做。)JMM爲全部操做定義了偏序關係Happens-Before.
兩個操做缺少Happens-Before關係,就能夠重排序。
多個線程讀取至少一個線程寫入,操做之間沒有依照Happens-Before來排序,就會有數據競爭問題。正確同步沒有數據競爭。
Happens-before規則:
程序順序規則。(程序中操做A在操做B以前,線程中A操做在B操做前完成)
監視器鎖規則。-----全序關係
volatile規則。 全序
線程啓動規則
線程結束規則
中斷規則
終結器規則
傳遞性。
在不一樣的鎖上同步就不存在happens-before的關係。
(5)藉助同步。能夠藉助happens順序規則與其餘某個順序規則結合起來,對爲被鎖保護變量訪問操做進行排序。(這項技術對語句順序敏感,容易出錯),最大限度提高類的性能時才用。
FutureTask使用了AbstractQueuedSynchronizer.
類庫中提供的happens-before:
--一個元素放入線程安全容器操做,在另外一個線程從該容器獲取操做以前執行;
---在countdownlatch上的倒數操做將在閉鎖上await方法返回以前執行。
---釋放semaphoe許可的操做在該semaphore上得到一個許可以前執行。
---Future表示的任務全部操做都在從future.get返回以前完成
--Executor提交一個runnable或者Callable操做在任務開始以前執行。
---線程到達CylicBarrier或Exchanger的操做在其餘到達該柵欄或交換點的線程被釋放以前執行。
2.發佈
(1)不安全的發佈。缺乏happens-before會發生重排序。(解釋了爲何沒有充分同步另外一個線程會看到部分構造對象)
錯誤的延遲初始化;在構造函數中啓動線程。
(2)安全的發佈。Happens-before比安全發佈提供了更強可見性和順序發佈。(happens-before是內存訪問級別上的-併發彙編,安全發佈更接近程序設計)
(3)安全的初始化模式。
synchronized. 初始化器以特殊方式處理靜態化域,有安全保證。(能夠在靜態塊中) =-----這項技術和jvm延遲加載結合起來,一種延遲初始化技術。
(4)雙重檢查加鎖。
public class DoubleCheckedLocking {
private static Resource resource;
public static Resource getInstance() {
if (resource == null) {
synchronized (DoubleCheckedLocking.class) {
if (resource == null)
resource = new Resource();
}
}
return resource;
}
} 問題在於返回時沒有加鎖,看到的是構造的未徹底的對象,看到失效值。並且雙重檢查加鎖並無提升多少性能。
3.初始化過程的安全性。
不可變對象永遠是安全的。(初始化安全性確保,全部線程看到的由構造函數爲對象給final域設置的正確值,無論何種方式發佈),並且能夠經過final域到達的全部對象對其餘線程可見。
final域對象初始化安全性能夠防止對對象的初始化引用被從新排序到構造構成以前。(final域寫入操做,都會被凍結)(final即便不安全的延遲初始化,也能正確發佈)。
final只在開始時保證可見性,構造完成後可能改變的值,須要用同步保證可見性。