java 并发编程总结
這邊文章的主要內容是基于“java并發編程藝術”這本書,中間加入了一些自己的理解。這篇文章包括并發編程涉及到的幾乎所有基礎知識。主要是幫助長期從事業務邏輯開發的java程序員梳理一下java并發開發基礎。
CPU原理簡介
| 內存屏障(memory barriers) | 是一組處理器指令,用于實現對內存操作的順序限制 |
| 內存緩沖行(cache line) | CPU高速緩存中可以分配的最小存儲單元。處理器填寫緩存行時會加載整個緩存行,現在CPU需要執行幾百次CPU指令。在64位操作系統中,一個內存緩沖行是8個字節 |
| 原子操作(atomic operation) | 不可中斷的一個或者一系列操作 |
| 緩存行填充(cache line fill) | 當處理器識別到從內存中讀取操作數是可緩存的,處理器讀取整個高速緩存行到適當的緩存(L1,L2,L3的或所有) |
| 緩存命中(cache hit) | 如果進行高速緩存行填充操作的內存位置仍然是下次處理器訪問的地址時,處理器從緩存行讀取操作數據,而不是內存讀取 |
| 寫命中(write hit) | 當處理器將操作數寫回到一個內存緩存的區域時,它首先會檢查這個緩存的內存地址是否在緩存行中,如果存在一個有效的緩存行,則處理器會將這個操作數寫回到緩存,而不是寫回到內存,這個稱為寫命中 |
| 寫缺失(write misses the cache) | 一個有效的緩存行被寫入到不存在的內存區域 |
| 比較和交換(Compare And Swap) | CAS操作需要輸入兩個數值,一個舊值(期望操作前的值)和一個新值,在操作期間比較久之有沒有發生變化,如果沒有發生變化,才交換成新值,否則不交換 |
| CPU流水線(CPU pipeline) | 在CPU中由5-6個不同功能的電路單元組成一條指令處理流水線,一條流水線對應一個指令的執行 |
緩存的存儲器層次結構
15261103888142.jpgCPU緩存可以分為一級緩存,二級緩存,部分高端CPU還具有三級緩存,每一級緩存中所儲存的全部數據都是下一級緩存的一部分,這三種緩存的技術難度和制造成本是相對遞減的,所以其容量也是相對遞增的。
基于緩存的存儲器層次結構行之有效,是因為較慢的存儲設備比較快的存儲設備更便宜,還因為程序往往展示出局部性:
時間局部性:被引用過一次的存儲器的位置很可能在不遠的將來被再次引用。
空間局部性:如果一個存儲器位置被引用了一次,那么程序很可能在不遠的將來引用附近的一個存儲器位置。
CPU如何實現原子操作
1)使用總線鎖保證原子性,當一個處理器在總線上輸出LOCK#信號,那么該處理器可以獨占共享內存。
2)使用緩存鎖保證原子性,同一時刻我們只需保證對某個內存地址的操作是原子性即可,總線鎖定的開銷比較大。通過緩存一致性保障當某個被多處理器緩存的變量被修改,所有的緩存處理器重新從共享內存加載新值。
處理器之間的緩存一致性協議:每個處理器通過嗅探在總線上傳播的數據來檢查自己緩存的值是不是過期了,當處理器發現自己緩存行對應內存地址被修改,就會將當前處理器的緩存行設置成無效狀態,當處理器對這個數據進行修改操作的時候,會重新從系統內存中把數據讀到操作器緩存里。
vilatile關鍵字原理
對vilatile關鍵字修飾的變量,當發生修改時,生成的匯編代碼會多出一個lock前綴的指令。
1)lock前綴指令會引起處理器緩存回寫到內存;
2)一個處理器的緩存回寫到內存會導致其他處理器的緩存無效。(之后的所有操作,各個處理器看到的值是最新的,也就是valatile的可見性)
前面說過,處理器是通過嗅探在總線來檢查混存的值是不是過期,根據處理器之間的緩存一致性協議,vilatile變量的修改是滿足原子的。vilatile相對synchronized修飾一個變量,實現原子性會輕量很多。
可見性 != 線程安全
可見性指的是單個變量;線程安全指的是變量的一系列操作;兩者是不同維度的概念,可以理解為可見性是線程安全的前提。
即便i變量是volatile關鍵字修飾的,但是i++不是線程安全的。
i++編譯后是多條指令,指令如下:
0: aload_0
1: dup
2: getfield #2; //Field i:I
5: iconst_1
6: iadd
7: putfield #2; //Field i:I
8: return
可見性只保證了7這一步的原子性。如果另一個線程2也做i++,當線程1、2都執行到第7步,這個時候,并行變成串行,線程1執行第7步,線程2等待線程1執行完第7步才能往下執行。但是并發情況下線程2得到的值也是1,線程1、2各做了一次i++,應該得到2,但是輸出是1。解決這個問題可以采用volatile配合CAS來保證多操作的一致性。
鎖
同步的實現當然是采用鎖了,java中使用鎖的兩個基本工具是 synchronized 和 Lock。Lock通過顯示定義同步鎖對象來實現同步,在這種機制下,同步鎖由Lock對象充當。Lock提供了比synchronized方法和synchronized代碼塊更廣泛的鎖定操作,Lock允許實現更靈活的結構,可以具有差別很大的屬性,并且支持多個相關的Condition對象。
Synchronized實現同步的基礎:Java中的每一個對象都可以作為鎖,Synchronized具體體現為以下四種形式:
1)對于普通對象,鎖是當前實例對象;
2)對于普通同步方法,鎖是當前實例對象;
3)對于靜態同步方法,鎖是當前類的class對象;
4)對于同步方法塊,鎖是Synchronized括號里配置的對象。
Synchronized在JVM里的實現原理:JVM基于進入和退出Monitor對象來實現對象同步和代碼塊同步,其中對象同步是通過獲取對象的monitor(也就是對象頭里面的鎖信息)來決定哪個線程持可以操作這個對象;代碼塊的同步是使用monitorenter指令和monitorexit指令實現的,在代碼編譯后,在同步代碼塊開始處添加monitorenter指令,結束和異常處添加monitorexit指令,方法同步類似。
java對象頭結構
如果對象是數組類型,則虛擬機用3個字寬(1個字寬,在32位虛擬機即4字節,64位虛擬機8字節),非數組,則用2個字寬。
| Mark Word | 存儲對象的hashCode或鎖信息 |
| Class Metadata Address | 存儲到對象類型數據的指針 |
其中Mark Word里默認存儲對象的HashCode、分代年齡和鎖標記位。32位的結構如下
對象頭.pngMonitor Record
Monitor Record是線程私有的數據結構,每一個線程都有一個可用monitor record列表,同時還有一個全局的可用列表。每一個被鎖住的對象都會和一個monitor record關聯(對象頭的MarkWord中的LockWord指向monitor record的起始地址),同時monitor record中有一個Owner字段存放擁有該鎖的線程的唯一標識,表示該鎖被這個線程占用。Monitor Record的內部結構字段如下:
Owner:初始時為NULL表示當前沒有任何線程擁有該monitor record,當線程成功擁有該鎖后保存線程唯一標識,當鎖被釋放時又設置為NULL;
EntryQ:關聯一個系統互斥鎖(semaphore),阻塞所有試圖鎖住monitor record失敗的線程。
RcThis:表示blocked或waiting在該monitor record上的所有線程的個數。
Nest:用來實現重入鎖的計數。
HashCode:保存從對象頭拷貝過來的HashCode值(可能還包含GC age)。
Candidate:用來避免不必要的阻塞或等待線程喚醒,因為每一次只有一個線程能夠成功擁有鎖,如果每次前一個釋放鎖的線程喚醒所有正在阻塞或等待的線程,會引起不必要的上下文切換(從阻塞到就緒然后因為競爭鎖失敗又被阻塞)從而導致性能嚴重下降。Candidate只有兩種可能的值。0表示沒有需要喚醒的線程1表示要喚醒一個繼任線程來競爭鎖。
偏向鎖
Java偏向鎖(Biased Locking)是Java6引入的一項多線程優化。 偏向鎖,顧名思義,它會偏向于第一個訪問鎖的線程,如果在運行過程中,同步鎖只有一個線程訪問,不存在多線程爭用的情況,則線程是不需要觸發同步的,這種情況下,就會給線程加一個偏向鎖。 如果在運行過程中,遇到了其他線程搶占鎖,則持有偏向鎖的線程會被掛起,JVM會消除它身上的偏向鎖,將鎖恢復到標準的輕量級鎖。偏向鎖的撤銷,需要等待全局安全點(在這個時間點上沒有字節碼正在執行),它會首先暫停擁有偏向鎖的線程,判斷鎖對象是否處于被鎖定狀態,撤銷偏向鎖后恢復到未鎖定(標志位為“01”)或輕量級鎖(標志位為“00”)的狀態。
輕量級鎖
輕量級鎖是由偏向所升級來的,偏向鎖運行在一個線程進入同步塊的情況下,當第二個線程加入鎖爭用的時候,偏向鎖就會升級為輕量級鎖; 輕量級鎖的加鎖過程:
(1)當對象處于無鎖狀態時(RecordWord值為HashCode,狀態位為001),線程首先從自己的可用moniter record列表中取得一個空閑的moniter record,初始Nest和Owner值分別被預先設置為1和該線程自己的標識,一旦monitor record準備好然后我們通過CAS原子指令安裝該monitor record的起始地址到對象頭的LockWord字段,如果存在其他線程競爭鎖的情況而調用CAS失敗,則只需要簡單的回到monitorenter重新開始獲取鎖的過程即可。
(2)對象已經被膨脹同時Owner中保存的線程標識為獲取鎖的線程自己,這就是重入(reentrant)鎖的情況,只需要簡單的將Nest加1即可。不需要任何原子操作,效率非常高。
(3)對象已膨脹但Owner的值為NULL,當一個鎖上存在阻塞或等待的線程同時鎖的前一個擁有者剛釋放鎖時會出現這種狀態,此時多個線程通過CAS原子指令在多線程競爭狀態下試圖將Owner設置為自己的標識來獲得鎖,競爭失敗的線程在則會進入到第四種情況(4)的執行路徑。
(4)對象處于膨脹狀態同時Owner不為NULL(被鎖住),在調用操作系統的重量級的互斥鎖之前先自旋一定的次數,當達到一定的次數時如果仍然沒有成功獲得鎖,則開始準備進入阻塞狀態,首先將rfThis的值原子性的加1,由于在加1的過程中可能會被其他線程破壞Object和monitor record之間的關聯,所以在原子性加1后需要再進行一次比較以確保LockWord的值沒有被改變,當發現被改變后則要重新monitorenter過程。同時再一次觀察Owner是否為NULL,如果是則調用CAS參與競爭鎖,鎖競爭失敗則進入到阻塞狀態。
多線程下java如何實現原子操作
通過鎖和CAS的方式實現多指令的“原子”操作,以計數為例:
private void safeCount(){for(;;){int count = atomicCount.get();boolean suc = atomicCount.get.compareAndSet(count,++count);} }使用CAS方式實現原子操作的三個問題
1)ABA問題,1A->2B->3A,JDK中提供AtomicStampedReference來解決,是通過首先檢查前引用是否等于預期引用,再檢查當前值是否等于預期值來實現的。
2)循環時間開銷比較大
3)只能保障一個變量的原子操作,多個變量的原子操作通過鎖來實現。
JVM實現鎖的方式都使用了循環CAS。
java內存模型
線程之間的通信和同步
線程之間的通信可以分為隱式通信和顯式通信。隱式通信是指在共享內存的并發模型里,通過寫-讀內存中的公共狀態來實現隱式通信。顯式通信是指在信息傳遞的并發模型里,線程之間通過socket發送消息進行通信。
同步是指程序中用于控制不同線程間操作發生的相對順序的機制。在共享內存并發模型里,同步是顯式進行的,程序員必須顯式指定某個方法或某段代碼需要在線程之間互斥執行。在消息傳遞的并發模型里,由于消息的發送必須在消息的接收之前,因此同步是隱式進行的。
如果編寫多線程程序的java程序員不理解隱式進行的線程之間通信,很可能遇到各種奇怪的內存可見性問題。
從源代碼到最終執行的指令序列經過以下幾步的重排序。
15263131591096.jpg編譯器在不影響 程序執行的結果下,可能對代碼執行的先后順序進行調整。
在執行程序時,為了提供性能,處理器和編譯器常常會對指令進行重排序,但是不能隨意重排序,不是你想怎么排序就怎么排序,它需要滿足以下兩個條件:
1. 在單線程環境下不能改變程序運行的結果;
2. 存在數據依賴關系的不允許重排序
A、B、C三個操作存在如下關系:A、B不存在數據依賴關系,A和C、B和C存在數據依賴關系,因此在進行重排序的時候,A、B可以隨意排序,但是必須位于C的前面,執行順序可以是A –> B –> C或者B –> A –> C。
在多線程情況下,重排序可能導致處理器執行內存操作的順序和內存實際的操作執行順序不一致。比如下面寫緩存的存在,導致處理器對執行的操作順序進行了重排序。處理器與內存的交互圖如下
15263104499020.jpg
并發場景下,處理器A執行
a=1; x=b;處理器B執行
b=2; y=a;可能得到a=b=0的結果
從內存操作的實際發生順序來看,直到A3將寫緩存區的值刷新到主內存,A1操作才算真正執行。處理器執行的順序是A1->A2,但是內存操作實際發生的順序是A2->A1,這個就是內存系統的重排序。
對于未同步或未正確同步的多線程程序,JMM只提供最小的安全性:線程執行時讀取到的值,要么是之前某個線程寫入的值,要么是默認值(0,Null,false),JMM保證線程操作的值不會是無中生有冒出來的。
為了保證并發系統的順序一致性,java編譯器在生成指令序列的適當位置會插入內存屏障指令來禁止特定類型的處理器排序,有以下4類內存屏障指令,我們熟悉各種關鍵字,如volatile、鎖、final等 都是通過屏障指令來保證內存語義的:
| LoadLoad Barriers | Load1;LoadLoad;Load2 | 確保Load1數據的裝載先于Load2及所有后續裝載指令 |
| StoreStore Barriers | Store1;StoreStore;Store2 | 確保Store1數據對其他處理器可見(刷新到內存)先于Store2及后續所有存儲指令的存儲 |
| LoadStore Barriers | Load1;LoadLoad;Store2 | 確保Load1數據的裝載先于Store2及所有后續存儲指令刷新到內存 |
| StoreLoad Barriers | Store1;StoreStore;Load2 | 確保Store1數據對其他處理器可見(刷新到內存)先于Load2及后續裝載指令 |
volatile
在每一個volatile寫操作的前面插入一個StoreStore屏障
在每一個volatile寫操作的后面插入一個StoreLoad屏障
在每一個volatile讀操作的后面插入一個LoadLoad屏障
在每一個volatile讀操作的后面插入一個LoadStore屏障
JSR-133還嚴格限制編譯器和處理器對volatile變量與普通變量的重排序確保volatile的寫-讀 和鎖的釋放-獲取具有相同的內存語義。
雙重檢查鎖定和延遲初始化(單例的寫法)
寫法一:
public static synchronized Instance getInstance(){ if (instance == null){ instance = new Instance(); } return instance; } }這種做法的問題是很明顯的,每一次讀取instance都需要同步,可能會對性能產生較大的影響。
寫法二:
這種方案看似解決了上面兩種方案都存在的問題,但是也是有問題的。
問題根源
這一條語句在實際執行中,可能會被拆分程三條語句,如下:
memory = allocate(); //1 createInstance(memory); //2 instance = memory; //3根據重排序規則,后兩條語句不存在數據依賴,因此是可以進行重排序的。重排序之后,就意味著,instance域在被賦值了之后,指向的對象可能尚未初始化完成,而instance域是一個靜態域,可以被其他線程讀取到,那么其他線程就可以讀取到尚未初始化完成的instance域。
基于volatile的解決方案
要解決這個辦法,只需要禁止語句2和語句3進行重排序即可,因此可以使用volatile來修改instance就能做到了。
private volatile static Instance instance;
因為Volatile語義會禁止編譯器將volatile寫之前的操作重排序到volatile之后。
寫法三:
public class InstanceFactory {private static class InstanceHolder {public static Instance instance = new Instance();}public static Instance getInstance() {return InstanceHolder.instance ; //這里將導致InstanceHolder類被初始化} }Java語言規范規定,對于每一個類或者接口C ,都有一個唯一的初始化鎖LC與之對應,從C到LC的映射,由JVM實現。
每個線程在讀取一個類的信息時,如果此類尚未初始化,則嘗試獲取LC去初始化,如果獲取失敗則等待其他線程釋放LC。
如果能獲取到LC,則要判斷類的初始化狀態,如果是未初始化,則要進行初始化。如果是正在初始化,
則要等待其他線程初始化完成,如果是已經初始化,則直接使用此類對象。
線程
線程的狀態
| NEW | 初始狀態,線程被構建,但是還沒有調用start()方法 |
| RUNNABLE | 運行狀態,Java線程將操作系統中的就緒和運行兩種狀態籠統稱為“運行中” |
| BLOCKED | 阻塞狀態,表示線程阻塞于鎖 |
| WATING | 等待狀態,表示線程進入等待狀態,進入該狀態表示當前線程需要等待其他線程做出一些特定動作(通知或中斷) |
| TIME_WATING | 超時等待狀態,該狀態不同于WATING,它是可以在指定的時間自行返回的 |
| TERMINATED | 終止狀態,表示當前線程已經執行完畢 |
Java線程狀態變遷
15279229266495.jpg
線程之間的通信方式
1)volatile和synchronized關鍵字
volatile修飾的變量可以被多個線程共用;synchronzed保證線程對變量的可見性和排他性
2)等待/通知機制
從WaitNotify中可以提煉出等待/通知的經典范式,該范式分為兩部分:等待方(消費者)和通知方(生產者)。
等待方遵循如下的原則:
a.獲取對象的鎖
b.如果條件不滿足,那么調用對象的wait()方法,被通知后仍要檢查條件
c.條件滿足則執行對應的邏輯。
對應的偽代碼如下
通知方遵循如下原則:
a.獲取對象的鎖
b.改變條件
c.通知所有的等待方線程
對應的偽代碼如下:
3)管道輸入/輸出流
PipeWriter out = new PipeWriter(); PipeReader in = new PipeReader(); out.connect(in);out、in分別在兩個線程中進行讀寫
4)Thread.join()的使用
如果一個線程A執行了thread.join()語句,其含義:當前線程A等到thread線程終止之后才從thread.join()返回。
5)ThreadLocal的使用
ThreadLocal即本地線程變量,是一個以ThreadLocal對象為key,任意對象為value的存儲結構。這個結構被附帶在線程上,也就是說一個線程可以根據一個ThreadLocal對象查詢到綁定在這個線程上的值。
Lock
Lock接口提供了和Synchronized關鍵字類似的同步功能,只是在使用時需要顯示地獲取和釋放鎖。雖然它缺少了Synchronized隱式獲取釋放鎖的便捷性,但是卻擁有了鎖獲取和釋放的可操作性、可中斷的獲取鎖以及超時獲取鎖等多種synchronized關鍵字所不具備的同步特性。
Lock的API
| void lock() | 獲取鎖,調用該方法獲取鎖,當獲取成功后,從該方法返回,否則阻塞在這個方法中 |
| void lockInterruptibly() throws InterruptedException | 可中斷地獲取鎖,在獲取過程中可以相應中斷 |
| boolean tryLock() | 嘗試非阻塞獲取鎖,調用該方法立刻返回,如果能夠獲取則返回true,否則返回false |
| boolean tryLock(long,unit) throws InterruptedException | 帶超時時間的獲取鎖 |
| void unlock() | 釋放鎖 |
| Codition newCondition() | 獲取等待通知組件,該組件和當前的鎖綁定,當前線程只有獲得了鎖,才能調用該組件的wait()方法,而調用后,當前線程將釋放鎖 |
Lock接口的實現基本都是通過聚合一個同步器的子類來完成訪問控制的,隊列同步器中維護了一個FIFO的雙向鏈表來管理線程之間的同步,當前線程調用lock.lock()獲取同步狀態失敗時,同步器會將當前線程以及等待狀態等信息構造成一個節點并將其加入同步隊列,同時會阻塞當前線程,當同步狀態釋放時,會把首節點中的線程喚醒,使其再次嘗試獲取同步狀態。
隊列同步器(AbstractQueueSynchronizer)
子類通過繼承同步器并實現它的抽象方法來管理同步狀態,同步器可以支持獨占式獲取同步狀態,也可以支持共享式地獲取同步狀態,這樣就可以實現不同類型的組件(ReentrantLock、ReentrantReadWriteLock 和 CountDownLatch等)
同步器可重寫的方法
| protected boolean tryAcquire(int arg) | 獨占式獲取同步器狀態,實現該方法需要查詢當前狀態并判斷同步狀態是否符合預期,然后再進行CAS設置同步狀態 |
| protected boolean tryRelease(int arg) | 獨占式釋放同步狀態,等待獲取同步狀態的線程將有機會獲取同步狀態 |
| protected int tryAcquireShare(int arg) | 共享式獲取同步狀態,返回大于等于0的值表示獲取成功,反之獲取失敗 |
| protected boolean tryReleaseShare(int arg) | 共享式釋放同步狀態 |
| protected boolean isHeldExclusively() | 當前同步器是否在獨占模式下被線程占用,一般該方法表示是否被當前線程所獨占 |
隊列同步器內部中維護了一個FIFO的雙向鏈表來管理線程之間的同步,每一個線程被封裝成一個節點Node,Node的屬性如下:
int waitStatus:CANCAELLED(值為1),等待超時或者被中斷的狀態;SIGNAL(值為-1),等待信號狀態;CONDITION(值為-2),節點線程在等待Condition,如果其他線程對Condition調用了sinal()方法后,該節點獲取同步狀態;PROPAGATE(值為-3),表示下一次共享式同步狀態獲取會無條件地被傳播下去;INITIAL(值為0),初始狀態
Node prev:前驅節點
Node next:后繼節點
Node nextWaiter:等待隊列的后繼節點。如果當前節點是共享的,那么這個字段將是一個SHARED常量。該字段用于處理等待狀態為CONDITION的線程同步。
Thread thread:獲取同步狀態的線程
獨占式同步狀態獲取與釋放
通過調用同步器的acquire(int arg)方法可以獲取同步狀態,該方法對中斷不敏感。源碼如下:
public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();}上述代碼主要完成了同步狀態獲取、節點構造、加入同步器以及在同步隊列中自旋等待的相關工作。
private Node addWaiter(Node mode) {Node node = new Node(Thread.currentThread(), mode);// Try the fast path of enq; backup to full enq on failureNode pred = tail;if (pred != null) {node.prev = pred;if (compareAndSetTail(pred, node)) {pred.next = node;return node;}}enq(node);return node;}private Node enq(final Node node) {for (;;) {Node t = tail;if (t == null) { // Must initializeif (compareAndSetHead(new Node()))tail = head;} else {node.prev = t;if (compareAndSetTail(t, node)) {t.next = node;return t;}}}}enq方法是在第一次添加尾節點失敗時通過自旋死循環的方式來確保節點加到隊列尾部。
final boolean acquireQueued(final Node node, int arg) {boolean failed = true;try {boolean interrupted = false;for (;;) {final Node p = node.predecessor();if (p == head && tryAcquire(arg)) {setHead(node);p.next = null; // help GCfailed = false;return interrupted;}if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)cancelAcquire(node);}}acquireQueued方法中,當前線程在“死循環”中嘗試獲取同步狀態,而只有前驅節點是頭節點才能嘗試獲取同步狀態。
獨占式同步狀態獲取流程
15280341253622.jpg
共享式同步狀態獲取與釋放
共享式獲取與獨占式獲取最主要的區別在于同一時刻能否有過個線程同時獲取到同步狀態。一個文件的寫往往是獨占式訪問,但是讀可以是共享式訪問。
通過調用同步器的acquireShared(int arg)方法可以共享式地獲取同步狀態,該方法代碼如下所示:
在acquireShared(int arg)方法中,同步器調用tryAcquireShared(int arg)方法嘗試獲取同步狀態,tryAcquireShared(int arg)返回大于等于0時,表示能夠獲取到同步狀態。在doAcquireShared(int arg)方法的自旋中,如果當前節點的前驅節點為頭節點時,嘗試獲取同步狀態,如果返回值大于等于0,表示獲取同步狀態成功,并從自旋中退出。setHeadAndPropagate是設置頭節點,然后調用releaseShared()方法釋放后續處于SIGNAL狀態的線程節點。
重入鎖(ReentrantLock)
重入鎖是基于獨占式同步來實現的,在此基礎上增加了請求線程是不是獲得鎖的線程判斷,來支持線程在獲得到鎖之后能夠在此獲取該鎖而不會被鎖阻塞。每次獲得鎖,狀態都要累加,如果鎖被獲取了n此,那么前(n-1)次tryRelease(int release)方法必須返回fasle,只有同步狀態完全釋放了,才能返回true。
鎖的獲取存在公平和非公平兩種實現。如果一個鎖是公平的,那么鎖的獲取順序就應該符合請求的絕對時間順序,即FIFO。所有鎖的實現都是基于同步隊列實現的額,ReentrantLock也不例外,通過重寫tryAcquire(int arg)方法來實現公平和非公平。現在看一下公平鎖和非公平鎖的區別:
唯一的區別是公平鎖比非公平鎖在獲取鎖是多一個hasQueuedPredecessors()邏輯,即加入了同步隊列中當前節點是否有前驅節點的判斷,如果該方法返回true,則表示有線程比當前線程更早地請求獲取鎖,因此需要等待前驅線程獲取并釋放之后才能獲取鎖。
讀寫鎖
現實應用場景下,對某個資源的讀遠遠大于寫,這個時候使用獨占鎖來同等對待讀寫線程,性能遠沒有使用共享鎖來的高,在讀線程占用資源時讓其他讀線程也訪問資源可以減少線程之間的競爭。
讀寫狀態的設計
在ReentrantLock中,同步狀態代表了一個線程獲取鎖的次數。讀寫鎖同樣適用同步狀態來維護多個讀線程和一個寫線程的狀態。
如果在一個整型變量上維護多種狀態,就一定需要“按位切割使用”這個變量,讀寫鎖將變量分成兩個部分,高16位表示讀,低16位表示寫。
寫鎖的獲取與釋放
寫鎖是一個支持重進入的排他鎖。如果當前線程已經獲取了寫鎖,則增加寫狀態。如果當前線程在獲取寫鎖時,讀鎖已經被獲取(讀狀態不為0)或者該線程不是已經獲取寫鎖的線程,則當前線程進入等待狀態。ReentrantReadWriteLock的tryAcquire代碼如下:
protected final boolean tryAcquire(int acquires) {Thread current = Thread.currentThread();int c = getState();int w = exclusiveCount(c);if (c != 0) {// 存在讀鎖或者當前獲取線程不是已經獲取寫鎖的線程 if (w == 0 || current != getExclusiveOwnerThread())return false;if (w + exclusiveCount(acquires) > MAX_COUNT)throw new Error("Maximum lock count exceeded");// Reentrant acquiresetState(c + acquires);return true;}if (writerShouldBlock() ||!compareAndSetState(c, c + acquires))return false;setExclusiveOwnerThread(current);return true;}讀鎖的獲取與釋放
讀鎖時一個支持重進入的共享鎖,它能夠被多個線程同時獲取,在沒有其他寫線程訪問(寫狀態為0)時,讀鎖總會被成功地獲取,而所做的也只是(線程安全的)增加讀狀態。ReentrantReadWriteLock的tryAcquireShared方法代碼如下:
protected final int tryAcquireShared(int unused) {Thread current = Thread.currentThread();int c = getState();if (exclusiveCount(c) != 0 &&getExclusiveOwnerThread() != current)return -1;int r = sharedCount(c);if (!readerShouldBlock() &&r < MAX_COUNT &&compareAndSetState(c, c + SHARED_UNIT)) {if (r == 0) {firstReader = current;firstReaderHoldCount = 1;} else if (firstReader == current) {firstReaderHoldCount++;} else {HoldCounter rh = cachedHoldCounter;if (rh == null || rh.tid != getThreadId(current))cachedHoldCounter = rh = readHolds.get();else if (rh.count == 0)readHolds.set(rh);rh.count++;}return 1;}return fullTryAcquireShared(current);}LockSupport工具
LockSupport提供的阻塞和喚醒方法如下:
| void park() | 阻塞當前線程,如果調用unpark(Thread thread)方法或當前線程被中斷,才能從park()方法返回 |
| void parkNanos(long nanos) | 阻塞當前線程,最長不超過nanos納秒,返回條件在park()基礎上增加了超時返回 |
| void parkUntil(long deadline) | 阻塞當前線程,知道deadline時間(從1970年開始到deadline的毫秒數) |
| void unpark(Thread thread) | 喚醒處于阻塞的線程thread |
Condition接口
Condition的使用如下:
Lock lock = new ReentrantLock(); Condition condition = lock.newCondition();Condition接口的使用類似于普通java對象的監視器方法,普通java對象的監視器方法主要包括wait()、wait(long timeout)、notify、notifyAll()方法,Condition對應的方法為await()、await(long timeout)、signal()、signalAll().
Object的監視器方法與Condition接口的主要區別如下:
| 前置條件 | 獲取對象的鎖 | 調用Lock.lock()獲取鎖 然后調用Lock.newCondition()獲取Condition對象 |
| 等待隊列個數 | 一個 | 多個 |
| 當前線程釋放鎖并進入等待狀態,在等待狀態中不響應中斷 | 不支持 | 支持 |
相對于同步器的隊列是雙向隊列,condition的等待隊列是一個單向隊列。每個condition一個隊列。
ConditionObject的await()、signal()方法源碼如下:
調用await方法,說明該線程已經獲取到了鎖,該方法會將當前線程構造成節點加入到condition的等待隊列。
signal方法會喚起等待的頭線程節點。
java并發容器、框架、工具類
ConcurrentHashMap
jdk1.8之前實現原理是通過鎖分段技術提高并發訪問率。hashMap的內部是一個Entry數組,如果在這個數組上面上鎖,并發常見下線程等待會非常多。通過分段技術,ConcurrentHashMap維護一個兩級數組,第一級為最大65535個Segment,每個Segment對象內部再維護一個HashEntry數組。因為Segment最多65535個,也就是hashCode的最后15位決定分到哪個Segment,為了使分到Segment地數據更加分散,ConcurrentHashMap對hashCode再次散列。
jdk1.8之后,ConcurrentHashMap放棄了Segment概念,采用table數組某個元素作為鎖,從而實現了對每一行(鏈表)數據進行加鎖,進一步減少并發沖突的概率。將原先table數組+單向鏈表的數據結構,變更為table數組+單向鏈表+紅黑樹(當鏈表個數超過8時,鏈表轉為紅黑樹,查詢復雜度從O(N)降為O(lnN))的結構。
Fork/Join框架
Fork就是把一個大任務切分成若干個子任務并行執行,Join就是合并這些子任務的執行結果,最后得到這個大任務的結果。
ForkJoin框架的幾個核心類和接口
ForkJoinTask類,實現Future接口的抽象類,需要實現compute方法(類似Thread的run方法,完成相應的計算邏輯)可以當Thread來理解使用。調用fork方法就加入到任務池中,調用join方法得到任務類的執行結果。
ForkJoinPool類,執行任務的池子,可以當ThreadPool來理解使用
CountDownLatch
CountDownLatch允許一個或多個線程等待其他線程完成操作。CountDownLatch接受一個正整數作為構造函數,當我們調用countDown方法時,N就會減1。CountDownLatch的await方法會阻塞當前線程,加入到AQS的同步隊列,當N變成0時,就會喚起AQS中等待的等待線程。
CyclicBarrier
CyclicBarrier的字面意思是可循環的屏障。它讓一組線程到達一個屏障(也可以叫同步點)時被阻塞,直到最后一個線程到達屏障時,屏障才會開門,所有被屏障攔截的線程才會繼續運行。構造函數也接受一個正整數,表示要同步的線程數。
Semaphore
信號量Semaphore是用來控制同時訪問特定資源的線程數量,它通過協調各個線程以保證合理的使用公共資源。構造函數也是一個正整數表示最大并發訪問的線程數量。通過acquire方法獲取信號,接著使用資源執行操作,完成后通過release方法釋放信號量
原子操作類
AtomicBoolean
AtomicInteger
AtomicLong
以上提供CAS方法,如addAndGet、compareAndSet等
AtomicIntergerArray
AtomicLongArray
AtomicReferenceArray
以上提供CAS原子的方式更新數組中的值,如addAndGet(int i,int delta)等
AtomicReference:原子更新引用類型
AtomicReferenceFieldUpdater:原子更新引用類型里的字段
AtomicMarkableReference:原子更新帶有標記位的引用類型
AtomicIntergerFiledUpdater:原子更新整型的字段
AtomicLongFiledUpdater:原子更新Long類型字段
AtomicStampedReference:原子更新帶有版本號的引用類型,可以解決CAS進行原子更新時可能出現的ABA問題。
Java的線程池
線程池創建的構造函數如下
ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler) ;參數說明:
corePoolSize(線程池的基本大小):當提交一個任務到線程池時,線程池會創建一個線程來執行任務,即使其他空閑的基本線程能夠執行新任務也會創建線程,等到需要執行的任務數大于線程池基本大小時就不再創建。如果調用了線程池的prestartAllCoreThreads()方法,線程池會提前創建并啟動所有基本線程。maximumPoolSize(線程池最大數量):線程池允許創建的最大線程數量。如果隊列滿了,并且已創建的線程數小于最大線程數,則線程池會再創建新的線程執行任務。值得注意的是,如果使用了無界隊列這個參數就沒什么效果。
keepAliveTime(工作線程活動保持時間):線程池的工作線程空閑后,保持存活的時間。
unit:時間單位
workQueue(任務隊列):用于保存等待執行的任務的阻塞隊列。可以選擇以下幾種隊列:ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、PriorityBlockingQueue。
threadFactory:用于設置創建線程的工廠,可以通過線程給每個創建出來的線程設置更有意義的名字。
handler(飽和策略):當隊列和線程池都滿了,說明線程池處于飽和狀態,那么必須采取一種策略丟棄任務。jdk1.5中提供以下4中策略:AbortPolicy(直接跑出異常)、CallerRunsPolicy(只用調用者所在線程運行任務)、DiscardOldestPolicy(丟棄隊列里最近的一個任務,并執行當前任務)、DiscardPolicy(不處理,丟棄掉)。
線程池處理的主要流程:
1)線程池判斷核心線程池里的線程是否都在執行任務,如果不是,則創建一個新的工作線程來執行任務,如果核心線程里的線程都在執行任務,則進入下個流程。
2)線程池判斷工作隊列是否已經滿,如果工作隊列沒有慢,則將新提交的任務存儲在這個工作隊列里面,如果工作隊列滿了,則進入下個流程。
3)線程池判斷線程池的線程(最大線程數)是否都處于工作狀態,如果沒有,則創建一個新的工作線程來執行,如果已經滿了,則將誒飽和策略來處理這個任務。
工作線程:線程池創建時,會將線程封裝成工作線程Worker,Worker在執行完任務后,還會循環獲取工作隊列里的任務來執行。
Executor框架
Executor是用戶級的調度框架,成員關系圖如下:
15285947707039.jpg
其中ThreadPoolExecutor可以使用工廠類Executors來創建,有以下三種類型:
1)FixedThreadPool 可重用固定線程數的線程池。
corePoolSize和maximumPoolSize都被設置成nThreads。keepAliveTime為0,代表多余的空閑線程會被立即終止。隊列使用的是鏈表類型,是無界的。
2)SingleThreadPool 單線程的線程池。corePoolSize和maximumPoolSize都為1。keepAliveTime為0,代表多余的空閑線程會被立即終止。隊列使用的是鏈表類型,是無界的。
3)CachedThreadPool 是大小無界的線程池。corePool被設置為0,即corePool為空,maximumPoolSize被設置為Integer.MAX_VALUE,即maximumPool是無界的。這里把keepAliveTime設置為60S,意味著CachedThreadPool中的空閑線程等待新任務的最長時間為60秒,超過60秒會被終止。
ScheduledThreadPoolExecutor功能和Timer類似,但ScheduledThreadPoolExecutor功能更強大靈活。Timer對應的是單個后臺線程,ScheduledThreadPoolExecutor對應的是一個線程池。
ScheduledThreadPoolExecutor中的延遲隊列DelayQueue也是一個無界隊列。DelayQueue封裝了一個PriorityQueue,這個PriorityQueue會對隊列中的ScheduledFutureTask進行排序。
總結
以上是生活随笔為你收集整理的java 并发编程总结的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: JavaScriptjQuery.doc
- 下一篇: Java学习笔记二十六:Java多态中的