《Java并发编程实践-第一部分》-读书笔记
大家好,我是烤鴨:
《Java并發(fā)編程實戰(zhàn)-第一部分》-讀書筆記。
第一章:介紹
1.1 并發(fā)歷史:
多個程序在各自的進程中執(zhí)行,由系統(tǒng)分配資源,如:內(nèi)存、文件句柄、安全證書。進程間通信方式:Socket、信號處理(Signal Handlers)、共享內(nèi)存(Shared Memory)、信號量(Semaphores)和文件。
促進因素:
- 資源利用:等待的時候,其他程序運行會提高效率。
- 公平:多個用戶或程序可能對系統(tǒng)資源有平等的優(yōu)先級別。
- 方便:多個程序各自執(zhí)行單獨的任務(wù)比一個程序執(zhí)行多個任務(wù)方便調(diào)度和協(xié)調(diào)。
早期分時共享系統(tǒng),每一個進程都是一個虛擬的馮諾依曼機:擁有內(nèi)存空間、存儲指令和數(shù)據(jù),根據(jù)機器語言來順序執(zhí)行指令,通過操作系統(tǒng)的 I/O 實現(xiàn)與外部世界交互。對于每一條指令的執(zhí)行,都有對 “下一條指令” 的明確定義,并根據(jù)程序中的指令集來進行流程控制。
線程允許程序控制流(control flow)的多重分支同時存在于一個進程。共享進程范圍內(nèi)的資源,比如內(nèi)存和文件句柄,但是每個線程有自己的程序計數(shù)器(program counter)、棧(stack)和本地變量。
1.2 線程優(yōu)點:
適當多線程降低開發(fā)和維護開銷,提高性能。
- 多核處理器:程序調(diào)度基本單元是線程,一個單線程應(yīng)用程序一次只能運行在一個處理器上。
- 模型簡化:一個復(fù)雜、異步的流程可以被分解為一系列更簡單的同步流程,每一個在相互獨立的線程中運行,只有在特定同步點才彼此交互。比如servlets或者RMI。
- 異步事件的簡單處理:歷史上,操作系統(tǒng)把一個進程能夠創(chuàng)建的線程限制在比較少的數(shù)量(幾百個)。因此,操作系統(tǒng)為多元化的I/O開發(fā)了一些高效機制,比如unix的select和poll系統(tǒng)調(diào)用,java類庫對非阻塞I/O提供了一組包(java.nio)。
- 用戶界面的更加響應(yīng)性:AWT 和 Swing 框架
1.3 線程風險:
- 安全危險:多線程操作共享變量,有可能指令重排序,會導(dǎo)致發(fā)生混亂。
- 活躍度風險:單線程沒問題,多線程可能出現(xiàn) 死鎖、饑餓、活鎖 (https://blog.csdn.net/qq_22054285/article/details/87911464)。
- 性能風險:上下文切換可能引起巨大的系統(tǒng)開銷。
1.4 線程無處不在:
比如 AWT和Swing、Timer、Servlets
第二章 線程安全
編寫線程安全的代碼,本質(zhì)上是管理對狀態(tài)的訪問,而且通常都是共享、可變的狀態(tài)。
狀態(tài):對象的狀態(tài)就是數(shù)據(jù)存儲在狀態(tài)變量,比如實例域或靜態(tài)域。共享指多線程訪問、可變指生命周期內(nèi)可以改變。線程安全的性質(zhì),取決于程序中如何使用對象,而不是對象完成了什么。(沒讀懂)
無論何時,只要有多于一個線程訪問給定的狀態(tài)變量,而且其中某個線程會寫入該變量,此時必須使用同步來協(xié)調(diào)線程對該變量的訪問。
Java中首要的同步機制是 synchronized ,提供了獨占鎖。volatile 用來聲明變量可見性,但不能保證原子性。
消除多線程訪問同一個變量的隱患:不跨線程共享變量、使狀態(tài)變量不可變、在任何狀態(tài)訪問變量使用同步。
設(shè)計時考慮線程安全,比后期修復(fù)容易。設(shè)計線程安全的類,封裝、不可變性以及明確的不變約束會提供幫助。
2.1 線程安全性:
類與它的規(guī)約保持一致。良好的規(guī)約定義了用于強制對象狀態(tài)的不變約束以及描述操作影響的后驗條件。
定義:當多個線程訪問一個類時,如果不用考慮這些線程在運行時環(huán)境下的調(diào)度和交替執(zhí)行,并且不需要額外的同步及在調(diào)用方代碼不必做其他的協(xié)調(diào),這個類行為仍然是正確的,那么這個類是線程安全的。
對于線程安全類的實例進行順序或并發(fā)的一系列操作,都不會導(dǎo)致實例處于無效狀態(tài)。
-
示例:一個無狀態(tài)(stateless)的servlet
@ThreadSafe public class StatelessFactorizer implements Servlet{public void Service(ServletRequest req,ServletResponse resp){BigInteger i = extractFormRequest(req);BigInteger[] factors = factor(i);//...} }StatelessFactorizer 和 大多數(shù) Servlet 一樣,是無狀態(tài)的:不包含域也沒有引用其他類的域。一次特定計算的瞬時狀態(tài),會唯一地存在本地變量中,這些變量存儲在線程的棧中,只有執(zhí)行線程才能訪問。
無狀態(tài)對象永遠是線程安全的。
2.2 原子性
比如下面的代碼,單線程時運行良好。但是多線程會有問題,++count不是原子操作。
自增操作是"讀-改-寫"操作的實例。
-
競爭條件:線程交替執(zhí)行,會產(chǎn)生競爭,正確的答案依賴"幸運"的時序。最常見的競爭是"檢查再運行"(check-then-act)。比如單例模式,如果沒有double check synchronized,多線程場景就可能出現(xiàn)不止一個對象。
-
示例:惰性初始化中的競爭條件,延遲對象的初始化,直到程序真正用到它,同時確保只初始化一次。
(單例模式推薦使用的是double check synchronized) -
復(fù)合操作:**操作A、B,當其他線程執(zhí)行B時,要么B全部執(zhí)行完成,要么一點沒有執(zhí)行。A、B互為原子操作。**比如前面的自增操作,如果是原子操作的話就沒有問題了。可以考慮Java內(nèi)置的原子性機制-鎖。
@ThreadSafe public class StatelessFactorizer implements Servlet{private final AtomicLong count = new AtomicLong(0);public void Service(ServletRequest req,ServletResponse resp){BigInteger i = extractFormRequest(req);BigInteger[] factors = factor(i);count.incrementAndGet();//...} }juc 包下包含了原子變量類,實現(xiàn)數(shù)字和對象引用的原子狀態(tài)轉(zhuǎn)換。把long類型的計數(shù)器替換為 AtomicLong 類型的。
2.3 鎖
比如下面這段代碼,雖然變量本身是線程安全的,但是多個set操作不是原子性的,所以結(jié)果是有問題的。
為了保護狀態(tài)的一致性,要在單一的原子操作中更新相互關(guān)聯(lián)的狀態(tài)變量。
-
內(nèi)部鎖:synchronized 塊。
@ThreadSafe public class SynchronizedFactorizer implements Servlet{private BigInteger lastNumber;private BigInteger[] lastFactors;public synchronized void Service(ServletRequest req,ServletResponse resp){BigInteger i = extractFormRequest(req);if(i.equals(lastNumber)){//...}else{BigInteger[] factors = factor(i);lastNumber = i;lastFactors = factors;}} }
每個Java對象都可以隱式扮演一個用于同步的鎖角色,這些內(nèi)置鎖被稱為內(nèi)部鎖(intrinsic locks)或監(jiān)視器鎖(monitor locks)。執(zhí)行線程進入 synchronized 塊之前會自動獲得鎖,無論是正常退出,還是拋出異常,都會在失去 synchronized 塊 的控制時釋放鎖。
內(nèi)部鎖在Java中扮演了互斥鎖,只有至多一個線程可以擁有鎖。 -
重進入 (Reentrancy):當一個線程請求其他線程已經(jīng)占有的鎖時,請求線程將被阻塞。然而內(nèi)部鎖時可重進入的,重進入意味著基于“每線程”,而不是“每調(diào)用”。(這句話沒看明白,重入說的就是持有鎖的線程再次持有鎖,也就是多線程場景下的單線程場景,每持有鎖一次就重入一次,計數(shù)遞增)重進入的實現(xiàn)通過為每個鎖關(guān)聯(lián)一個請求計數(shù)和一個占有它的線程。沒鎖的時候,計數(shù)為0,相同的線程每請求一次鎖,計數(shù)就會遞增。
2.4 用鎖來保護狀態(tài)
鎖使得線程能夠串行地訪問它所保護的代碼路徑,可以用鎖創(chuàng)建相關(guān)的協(xié)議,保證對共享狀態(tài)的獨占訪問。比如 ++count 的遞增或 惰性初始化,操作共享狀態(tài)的復(fù)合操作必須是原子的。
對于每個可被多個線程訪問的可變狀態(tài)變量,如果所有訪問它的線程在執(zhí)行時都占有同一個鎖,這種情況下,我們稱這個變量由這個鎖保護。
對象的內(nèi)部鎖和它的狀態(tài)之前沒有內(nèi)在關(guān)系。每個共享的可變變量都需要由唯一一個確定的鎖保護,而維護者應(yīng)該清楚這個鎖。
不是所有數(shù)據(jù)都需要鎖保護—只有那些被多個線程訪問的可變數(shù)據(jù)。
對于每一個涉及多個變量的不變約束,需要同一個鎖保護其所有的變量。
2.5 活躍度與性能
上面的代碼 SynchronizedFactorizer 類,在 Service方法加 synchronized,每次只能有一個線程執(zhí)行它,違背了 Servlet 框架的初衷。多個請求排隊等待并依次被處理。我們把這種Web應(yīng)用的運行方式描述為弱并發(fā)(poor concurrency)的一種表現(xiàn):限制并發(fā)調(diào)用數(shù)量的,并非可用的處理器資源,而恰恰是應(yīng)用程序自身的結(jié)構(gòu)。
可以通過縮小 synchronized 塊的范圍來提升并發(fā)性。
通過簡單性與性能之間是相互牽制的。實現(xiàn)一個同步策略時,不要過早為了性能而犧牲簡單性(這是對安全性潛在的威脅)。
有些耗時的計算或操作,比如網(wǎng)絡(luò)或控制臺 I/O,難以快速完成。這些操作期間不要占有鎖。
第三章 共享對象
synchronized 不僅保證原子性,還保證 內(nèi)存可見性。
3.1 可見性:讀、寫操作發(fā)生在不同線程,“重排序”可能會影響讀線程看到的結(jié)果。
在沒有同步的情況下,編譯器、處理器,運行時安排操作的執(zhí)行順序可能完全出人意料。在沒有進行適當同步的多線程程序中,嘗試推斷那些“必然”發(fā)生在內(nèi)存中的動作時,你總是會判斷錯誤。
- 過期數(shù)據(jù):類似數(shù)據(jù)庫隔離級別的讀未提交,也就是讀到了更新前的數(shù)據(jù)。
- 非原子的64位操作:對于非 volatile的long和double變量,JVM將64位的讀或?qū)憚澐譃閮蓚€32位的操作。如果讀、寫在不同的線程,這種情況讀取一個非 volatile類型的long就可能出現(xiàn)一個值的高32位和另一個值的低32位。
- 鎖和可見性:鎖不僅僅是關(guān)于同步與互斥的,也是關(guān)于內(nèi)存可見的。為了保證所有線程都能看到共享的、可變變量的最新值,讀取和寫入線程必須使用公共的鎖進行同步。
- Volatile 變量:同步的弱形式:確保對一個變量的更新以可預(yù)見的方式告知其他線程。當一個域聲明volatile 后,編譯器會監(jiān)視這個變量,并且對它的操作不會與其他的內(nèi)存操作一起被重排序。
只有當 volatile變量能夠簡化實現(xiàn)和同步粗略的驗證時,才使用它們。當驗證正確性必須推斷可見性問題時,應(yīng)該避免使用volatile變量。正確使用volatile變量的方式:用于確保它們引用的對象狀態(tài)的可見性,或者用于標識重要的生命周期事件(比如初始化或關(guān)閉)的發(fā)生。
讀取 volatile 變量比 讀取非 volatile 變量的開銷略高。
加鎖可以保證可見性和原子性;volatile 變量只能保證可見性。
3.2 發(fā)布和逸出:
- 安全構(gòu)建的實踐:不要讓this引用在構(gòu)造期間逸出。在構(gòu)造函數(shù)中注冊監(jiān)聽器或啟動線程,使用私有構(gòu)造函數(shù)和一個公共的工廠方法。
3.3 線程封閉:
類似JDBC連接池的Connection對象,線程總是從池中獲得一個Connection對象,并且用它處理單一請求,最后歸還。這種連接管理模式隱式地將Connection對象限制在處于請求處理期間的線程中。
-
Ad-hoc 線程限制(未經(jīng)設(shè)計的線程封閉行為): 比如使用volatile關(guān)鍵字,外加讀寫在同一線程內(nèi)。
-
棧限制:棧限制中,只能通過本地變量才能觸及對象。
-
ThreadLocal:線程與持有數(shù)值的對象關(guān)聯(lián),提供get和set方法。
3.4不可變性:
不可變對象永遠是線程安全的。
-
final:將所有的域聲明為final型,除非它們是可變的。
-
使用volatile 發(fā)布不可變對象:使用可變的容器對象,必須加鎖以確保原子性。使用不可變對象,不必擔心其他線程修改對象狀態(tài),如果更新變量,會創(chuàng)建新的容器對象,不過在此之前任何線程都還和原先的容器打交道,仍然看到它處于一致的狀態(tài)。(對應(yīng)下面的代碼 OneValueCache 和 VolatileCachedFactorizer)
@Immutable class OneValueCache{private final BigInteger lastNumber;private final BigInteger[] lastFactors;public OneValueCache(BigInteger i,BigInteger[] factors){lastNumber = i;lastFactors = Arrays.copyOf(factors,factors.length);}public BigInteger[] getFactors(BigInteger i) {if(lastNumber == null || !lastNumber.equals(i)){return null;}else{return Arrays.copyOf(lastFactors, lastFactors.length);}} }
3.5 安全發(fā)布:
@ThreadSafe public class VolatileCachedFactorizer implements Servlet{private volatile OneValueCache cache = new OneValueCache(null, null);public void service(ServletRequest req, ServletResponse resp){BigInteger i = extractFromRequest(req);BigInteger[] factors = cache.getFactors(i);if (factors == null){facotrs = factors(i);cache = new OneValueCache(i, factors);}//} }-
不正確發(fā)布:當好對象變壞時,代碼如下:
public class Holder {private int n;public Holder(int n){this.n = n;}public void assertSanity(){if(n != n){throw new AssertionError("This statement is false.");}} }發(fā)布線程以外的任何線程都可以看到Holder域的過期值,因而看到的是一個null引用或者舊值。(反之線程同理),這種寫法本身是不安全的。
-
不可變對象與初始化安全性:即使發(fā)布對象引用時沒有使用同步,不可變對象仍然可以被安全地訪問。
不可變對象可以在沒有額外同步的情況下,安全地用于任意線程;甚至發(fā)布它們也不需要同步。
-
安全發(fā)布的模式:
發(fā)布對象的安全可見性。- 通過靜態(tài)初始化器初始化對象的引用;
- 將它的引用存儲到volatile域或AtomicReference;
- 將它的引用存儲到正確創(chuàng)建的對象的final域中;
- 或者將它的引用存儲到由鎖正確保護的域中。
線程安全容器。
? HashTable、SynchronizedMap、ConcurrentMap、Vector.CopyOnWriteArrayList、CopyOnWriteArraySet、syncronized-List、BlockingQueue或者ConcurrentListQueue
-
高效不可變對象(Effectively immutable objects):任何線程都可以在沒有額外的同步下安全地使用一個安全發(fā)布的高效不可變對象。比如正在維護一個Map存儲每位用戶的最近登錄時間:
public Map<String,Date> lastLogin = Collections.synchronizedMap(new HashMap<String, Date>)());訪問Date值時不需要額外的同步。
-
可變對象:
不可變對象可以通過任意機制發(fā)布;
高效不可變對象必須要安全發(fā)布;
可變對象必須要安全發(fā)布,同時必須要線程安全或者被鎖保護。 -
安全地共享對象:
共享策略:
-
線程限制:一個線程限制的對象,通過限制在線程中,而被線程獨占,且只能被占有它的線程修改。
-
共享只讀(shared read-only):一個共享的只讀對象,在沒有額外同步的情況下,可以被多個線程并發(fā)訪問,但是任何線程都不能修改它。共享只讀對象包括可變對河與高效不可變對象。
-
共享線程安全(shared thread-safe) :一個線程安全的對象在內(nèi)部進行同步,所以其他線程無須額外同步,就可以通過公共接口隨意訪問它。
-
被守護的(Guarded) :一個被守護的對象只能通過特定的鎖來訪問。被守護的對象包括那些被線程安全對象封裝的對象,和已知被待定的鎖保護起來的已發(fā)布的對象。
-
第四章 組合對象
4.1 設(shè)計線程安全的類:
確定對象狀態(tài)由哪些變量構(gòu)成;
確定限制狀態(tài)變量的不變約束;
制定一個管理并發(fā)訪問對象狀態(tài)的策略。
同步策略定義對象如何協(xié)調(diào)對其狀態(tài)訪問,并且不會違反它的不變約束或后驗條件。
-
收集同步需求:對象與變量擁有一個狀態(tài)空間,即它們可能處于的狀態(tài)范圍。狀態(tài)空間越小,越容易判斷它們。盡量使用final類型的域,可以簡化對對象可能狀態(tài)進行分析。
比如 Long的區(qū)間是 Long.MIN_VALUE 到 Long.MAX_VALUE,后驗條件會指出某種狀態(tài)轉(zhuǎn)換是非法的。比如當前狀態(tài)17,下一個合法狀態(tài)是18。
不理解對象的不變約束和后驗條件,就不能保證線程安全性。要約束狀態(tài)變量的有效值或者狀態(tài)轉(zhuǎn)換,就需要原子性與封裝性。 -
狀態(tài)依賴的操作
和后驗條件對應(yīng)的是先驗條件,比如移除隊列中的元素,隊列必須是“非空”狀態(tài)。 -
狀態(tài)所有權(quán)
創(chuàng)建的對象歸屬誰所有,所有權(quán)意味著控制權(quán),一旦將引用發(fā)布到一個可變對象上,就不再擁有獨占的控制權(quán),充其量只可能有"共享控制權(quán)"。容器類通常表現(xiàn)出一種"所有權(quán)分離"的形式。以 servlet中的 ServletContext 為例。 ServletContext 為 Servlet 提供了類似于 Map的對象容器服務(wù)。ServletContext可以調(diào)用 setAttribute 和 getAttribute,由于被多線程訪問,ServletContext 必須是線程安全的,而 setAttribute 和 getAttribute 不必是同步的。
4.2 實例限制
將數(shù)據(jù)封裝在對象內(nèi)部,把對數(shù)據(jù)的訪問限制在對象的方法上,更易確保線程在訪問數(shù)據(jù)時總能獲得正確的鎖。比如 私有的類變量、本地變量或者線程內(nèi)部的變量。
比如 PersonSet中的 mySet 只能通過 addPerson 和 containsPerson 訪問,而這兩個方法都加了鎖。
使用線程安全的類,分析安全性時更容易。
- 監(jiān)視器模式
Java monitor pattern,遵循Java監(jiān)視器模式的對象封裝了所有的可變狀態(tài),并由對象自己的內(nèi)部鎖保護。私有鎖對象可以封裝鎖,客戶代碼無法得到它。共有鎖允許客戶代碼涉足它的同步策略,不正確地使用可能引起活躍度問題。要驗證是否正確使用,需要檢查整個程序,而不是單個類。比如內(nèi)部方法加 synchronized 。
4.3 委托線程安全
無狀態(tài)的類中加入一個 AtomicLong類型的屬性,組合對象安全,因為線程安全性委托給了 AtomicLong。比如使用 ConcurrentHashMap。
-
非狀態(tài)依賴變量
使用 CopyOnWriteArrayList 存儲每個監(jiān)聽器清單。
-
委托無法勝任
public class NumberRange{// 不變約束: lower <= upperprivate final AtomicInteger lower = new AtomicInteger(0);private final AtomicInteger upper = new AtomicInteger(0);public void setLower(int i){// 警告 —— 不安全的 "檢查再運行"if (i > uppper.get()){throw new IllegalArgumentException("can't set lower to ...");}lower.set(i);}public void setUpper(int i){// 警告 —— 不安全的 "檢查再運行"if (i < lower.get()){throw new IllegalArgumentException("can't set upper to ...");}upper.set(i);}public boolean isInRange(int i){return (i >= lower.get() && i <= upper.get());} }NumberRange 不是線程安全的,"檢查再運行"沒有保證原子性。
底層的AtomicInteger是線程安全的,但是組合類不是,因為狀態(tài)變量lower和upper不是彼此獨立的。
如果一個類由多個彼此獨立的線程安全的狀態(tài)變量組成,并且類的操作不包含任何無效狀態(tài)轉(zhuǎn)換時,可以將線程安全委托給這些變量。
-
發(fā)布底層的狀態(tài)變量
如果一個狀態(tài)變量是線程安全的,沒有任何的不變約束限制它的值,并且沒有任何狀態(tài)轉(zhuǎn)換限制它的操作,可以被安全發(fā)布。
比如發(fā)布一個 public 的AtomicLong變量,沒有上述多余的判斷,就是安全的。
4.4 向已有的線程安全類添加功能
“缺少就加入”,比如list的!contains,再add,由于操作非原子性,可能同一個元素會執(zhí)行多次add。比如使用Vector可以解決,或者改寫add方法,增加synchronized。
-
4.4.1 客戶端加鎖
public classs ListHelper<E>{public List<E> list = Collections.synchronizedList(new ArrayList<E>());// ...public synchronized boolean putIfAbsent(E x) {boolean absent = !list.contains(x);if (absent){list.add(x);}return absent;} }上面這個例子對list并不是線程安全的,雖然在方法上加了synchronized。因為鎖的對象不對(客戶端加鎖和外部加鎖所用的不是一個鎖),操作的是list(多線程下對list的操作不是原子性的)。修改如下:
public classs ListHelper<E>{public List<E> list = Collections.synchronizedList(new ArrayList<E>());// ...public boolean putIfAbsent(E x) {synchronized (list) {boolean absent = !list.contains(x);if (absent){list.add(x);}return absent;}} } -
4.4.2 組合
客戶端沒法直接使用list,只能通過 ImprovedList 操作list的方法。客戶端并無關(guān)心底層的list是否安全,由ImprovedList的鎖來實現(xiàn)保證就行。
public classs ImprovedList<T> implements List<T>{public final List<T> list;public ImprovedList(List<T> list){this.list = list;}public synchronized boolean putIfAbsent(E x) {boolean absent = !list.contains(x);if (absent){list.add(x);}return absent;} } -
4.5 同步策略的文檔化
為類的用戶編寫類線程安全性擔保的問答;為類的維護者編寫類的同步策略文檔。
-
4.5.1 含糊不清的文檔
比如 Servlet 規(guī)范沒有建議任何用來協(xié)調(diào)對這些共享屬性并發(fā)訪問的機制。所以容器代替Web Application所存儲的這些對象應(yīng)該是線程安全的或者是搞笑不可變的。(容器不可能知道你的鎖協(xié)議,需要自己保證這些對象是線程安全的。)
再比如 DataSource的JDBC Connection,如果一個獲得了JDBC Connection 的及活動跨越了多個線程,那么必須確保利用同步正確地保護到 Connection的訪問。
-
第五章 構(gòu)建塊
在實踐中,委托是創(chuàng)建線程安全類最有效的策略之一:只需要用已有的線程安全類來管理所有狀態(tài)即可。
5.1 同步容器
同步包裝類,Collections.synchronizedXxx 工廠方法創(chuàng)建。
5.1.1 同步容器之中出現(xiàn)的問題
同步容器都是線程安全的。
public static Object getLast(Vector list) {int lastIndex = list.size() - 1;return list.get(lastIndex); } public static void deleteLast(Vector list) {int lastIndex = list.size() - 1;list.remove(lastIndex); }不同線程調(diào)用 size和get/remove時可能出現(xiàn) ArrayIndexOutOfBoundsException,對list加鎖可以避免這個問題,但會增加性能開銷。
public static Object getLast(Vector list) {sychroized(list) {int lastIndex = list.size() - 1;return list.get(lastIndex);} } public static void deleteLast(Vector list) {sychroized(list) {int lastIndex = list.size() - 1;list.remove(lastIndex); }}5.1.2 迭代器和ConcurrentModificationException
無論單線程還是多線程,操作容器,都可能出現(xiàn)ConcurrentModificationException,這是迭代器 Iterator 對修改檢查拋出的異常。
對容器加鎖可以避免這個問題,但會影響性能,替代方法是復(fù)制容器,線程隔離操作安全,會有明顯的性能開銷。
5.1.3 隱藏迭代器
容器本身作為一個元素,或者作為另一容器的key時,containsAll、removeAll、retainAll方法以及把容器作為參數(shù)的構(gòu)造函數(shù),都會對容器進行迭代。這些對迭代的間接調(diào)用,都可能引起 ConcurrentModificationException。
5.2 并發(fā)容器
JUC的并發(fā)容器和隊列。
5.2.1 ConcurrentHashMap
5.2.2 Map附加的原子操作
5.2.2 CopyOnWriteArrayList
寫入時復(fù)制,為了保證數(shù)組內(nèi)容的可見性(不會考慮后續(xù)的修改)
查詢頻率遠高于修改頻率時,適合使用 CopyOnWriteArrayList
5.3 阻塞隊列和生產(chǎn)者-消費者模式
生產(chǎn)者-消費者模式可以把生產(chǎn)者和消費者的代碼解耦合,但還是通過共享工作隊列耦合在一起。
設(shè)計初期就使用阻塞隊列建立對資源的管理。
LinkedBlockingQueue和ArrayBlockingQueue是FIFO隊列,PriorityBlockingQueue是一個按優(yōu)先級排序的隊列(可以使用Comparator進行排序)。
5.3.3 雙端隊列和竊取工作
雙端隊列和竊取工作(work stealing)模式。一個生產(chǎn)消費者設(shè)計中,所有的消費者共享一個工作隊列;在竊取工作的設(shè)計中,每一個消費者都有一個自己的雙端隊列。如果一個消費完,可以偷取其他消費者的雙端隊列中的末尾任務(wù)。
5.4 阻斷和可中斷的方法
關(guān)于 InterruptedException的兩種處理方式:
傳遞 InterruptedException,拋出給調(diào)用者。
恢復(fù)中斷。捕獲 InterruptedException ,并且在當前線程中調(diào)用通過 interrupt 中斷中恢復(fù)。
恢復(fù)中斷狀態(tài),避免掩蓋中斷:
public class TaskRunnable implements Runnable{BlockingQueue<Task> queue;//...public void run(){try{processTask(queue.take());} catch (InterruptedException e){//恢復(fù)中斷狀態(tài)Thread.currentThread().interrupt();}} }5.5 Synchronizer
Synchronizer 是一個對象,包含 信號量(semaphore)、關(guān)卡(barrier)以及閉鎖(latch)。
5.5.1 閉鎖
閉鎖(latch)是一種Synchronizer,可以延遲線程的進度直到線程到達終止狀態(tài)。閉鎖可以確保特定活動直到其他活動完成后才發(fā)生。
- 資源初始化,使用二元閉鎖可以保證 先后加載。
- 服務(wù)依賴,使用二元閉鎖可以保證 服務(wù)的先后啟動。
CountDownLatch 是一個靈活的閉鎖實現(xiàn)。countDown 方法對計數(shù)器做減操作,表示一個事件已經(jīng)發(fā)生了,而 await 方法等待計數(shù)器打到零,此時所有需要等待的事件都已發(fā)生。如果計數(shù)器入口值為非零,await會一直阻塞到計數(shù)器為零,或者等待線程中斷以及超時。(await一定要設(shè)置超時時間)
5.5.2 FutureTask
Future.get() 可以立刻得到返回結(jié)果。通常可以把多個FutureTask放到一個list,循環(huán) get()
5.5.3 信號量
計數(shù)信號量(Counting semaphore)用來控制能夠同時訪問特定資源的活動的數(shù)量,或者同時執(zhí)行某一給定操作的數(shù)量。
一個Semaphore管理一個有效的許可集(permit),活動獲取許可,使用之后釋放。如果沒有可用的許可,acquire會阻塞。
release方法向信號量返回一個許可。信號量的退化形式:二元信號量(互斥)。
比如可以用信號量維護連接池,池為空的時候阻塞,反之解除。
5.5.4 關(guān)卡
barrier 類似閉鎖,能夠阻塞線程,關(guān)卡和閉鎖關(guān)鍵不同在于,所有線程必須同時達到關(guān)卡點。閉鎖等待的是事件,關(guān)卡等待的是其他線程。
5.6 為計算結(jié)果建立高效、可伸縮的告訴緩存
用HashMap和ConcurrentHashMap(同步安全)做內(nèi)存存儲。
總結(jié):
可變狀態(tài)越少,保證線程安全就越容易。
盡量將域聲明為final類型,除非它們的需要是可變的。
不可變對象天數(shù)是線程安全的。
封裝使管理復(fù)雜度變的更可行。
用鎖來守護每個可變變量。
對同一不變約束中的所有變量都使用相同的鎖。
在運行復(fù)合操作期間持有鎖。
在非同步的多線程情況下,訪問可變變量的程序是存在隱患的。
在設(shè)計之過程就考慮線程安全,或者在文檔中明確說明非線程安全。
文檔化同步策略。
總結(jié)
以上是生活随笔為你收集整理的《Java并发编程实践-第一部分》-读书笔记的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 负载均衡配置与使用
- 下一篇: springboot使用mongodb