1. 程式人生 > >互聯網架構多線程並發編程高級教程(下)

互聯網架構多線程並發編程高級教程(下)

rri service 累加 機制 jms exceptio 模式 設置 擁有

基礎篇幅:
線程基礎知識、並發安全性、JDK鎖相關知識、線程間的通訊機制、
JDK提供的原子類、並發容器、線程池相關知識點

高級篇幅:
ReentrantLock源碼分析、對比兩者源碼,更加深入理解讀寫鎖,JAVA內存模型、先行發生原則、指令重排序


環境說明:
idea、java8、maven


第四章--鎖

01 鎖的分類

自旋鎖: 線程狀態及上下文切換消耗系統資源,當訪問共享資源的時間短,頻繁上下文切換不值得。jvm實現,使線程在沒獲得鎖的時候,不被掛起,轉而執行空循環,循環幾次之後,如果還沒能獲得鎖,則被掛起

阻塞鎖:阻塞鎖改變了線程的運行狀態,讓線程進入阻塞狀態進行等待,當獲得相應的信號(喚醒或者時間)時,才可以進入線程的準備就緒狀態,轉為就緒狀態的所有線程,通過競爭,進入運行狀態

重入鎖:支持線程再次進入的鎖,就跟我們有房間鑰匙,可以多次進入房間類似

讀寫鎖: 兩把鎖,讀鎖跟寫鎖,寫寫互斥、讀寫互斥、讀讀共享

互斥鎖: 上廁所,進門之後就把門關了,不讓其他人進來

悲觀鎖: 總是假設最壞的情況,每次去拿數據的時候都認為別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖

樂觀鎖:每次去拿數據的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可以使用版本號等機制。

公平鎖:大家都老老實實排隊,對大家而言都很公平

非公平鎖:一部分人排著隊,但是新來的可能插隊

偏向鎖:偏向鎖使用了一種等到競爭出現才釋放鎖的機制,所以當其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程才會釋放鎖

獨占鎖:獨占鎖模式下,每次只能有一個線程能持有鎖

共享鎖:允許多個線程同時獲取鎖,並發訪問共享資源


02 深入理解Lock接口

Lock的使用
lock與synchronized的區別
lock
獲取鎖與釋放鎖的過程,都需要程序員手動的控制
Lock用的是樂觀鎖方式。所謂樂觀鎖就是,每次不加鎖而是假設沒有沖突而去完成某項操作,如果因為沖突失敗就重試,直到成功為止。樂觀鎖實現的機制就 是CAS操作
synchronized托管給jvm執行
原始采用的是CPU悲觀鎖機制,即線程獲得的是獨占鎖。獨占鎖意味著其他線程只能依靠阻塞來等待線程釋放鎖。

實現了lock接口的鎖
各個方法的簡介


03 實現屬於自己的鎖

實現lock接口
使用wait notify
具體見視頻


04 AbstractQueuedSynchronizer淺析

AbstractQueuedSynchronizer -- 為實現依賴於先進先出 (FIFO) 等待隊列的阻塞鎖和相關同步器(信號量、事件,等等)提供一個框架。
此類的設計目標是成為依靠單個原子 int 值來表示狀態的大多數同步器的一個有用基礎。
子類必須定義更改此狀態的受保護方法,並定義哪種狀態對於此對象意味著被獲取或被釋放。
假定這些條件之後,此類中的其他方法就可以實現所有排隊和阻塞機制。子類可以維護其他狀態字段,但只是為了獲得同步而只追蹤使用 getState()、setState(int) 和 compareAndSetState(int, int) 方法來操作以原子方式更新的 int 值。
應該將子類定義為非公共內部幫助器類,可用它們來實現其封閉類的同步屬性。類 AbstractQueuedSynchronizer 沒有實現任何同步接口。而是定義了諸如 acquireInterruptibly(int) 之類的一些方法,在適當的時候可以通過具體的鎖和相關同步器來調用它們,以實現其公共方法。

此類支持默認的獨占 模式和共享 模式之一,或者二者都支持。處於獨占模式下時,其他線程試圖獲取該鎖將無法取得成功。在共享模式下,多個線程獲取某個鎖可能(但不是一定)會獲得成功。此類並不“了解”這些不同,除了機械地意識到當在共享模式下成功獲取某一鎖時,下一個等待線程(如果存在)也必須確定自己是否可以成功獲取該鎖。處於不同模式下的等待線程可以共享相同的 FIFO 隊列。通常,實現子類只支持其中一種模式,但兩種模式都可以在(例如)ReadWriteLock 中發揮作用。只支持獨占模式或者只支持共享模式的子類不必定義支持未使用模式的方法。

此類通過支持獨占模式的子類定義了一個嵌套的 AbstractQueuedSynchronizer.ConditionObject 類,可以將這個類用作 Condition 實現。isHeldExclusively() 方法將報告同步對於當前線程是否是獨占的;使用當前 getState() 值調用 release(int) 方法則可以完全釋放此對象;如果給定保存的狀態值,那麽 acquire(int) 方法可以將此對象最終恢復為它以前獲取的狀態。沒有別的 AbstractQueuedSynchronizer 方法創建這樣的條件,因此,如果無法滿足此約束,則不要使用它。AbstractQueuedSynchronizer.ConditionObject 的行為當然取決於其同步器實現的語義。

此類為內部隊列提供了檢查、檢測和監視方法,還為 condition 對象提供了類似方法。可以根據需要使用用於其同步機制的 AbstractQueuedSynchronizer 將這些方法導出到類中。

此類的序列化只存儲維護狀態的基礎原子整數,因此已序列化的對象擁有空的線程隊列。需要可序列化的典型子類將定義一個 readObject 方法,該方法在反序列化時將此對象恢復到某個已知初始狀態。

tryAcquire(int)
tryRelease(int)
tryAcquireShared(int)
tryReleaseShared(int)
isHeldExclusively()
Acquire:
while (!tryAcquire(arg)) {
enqueue thread if it is not already queued;
possibly block current thread;
}

Release:
if ((arg))
unblock the first queued thread;

05 深入剖析ReentrantLock源碼之非公平鎖的實現

如何閱讀源碼?
一段簡單的代碼
看構造
看類之間的關系,形成關系圖
看使用到的方法,並逐步理解,邊看代碼邊看註釋
debug


06 深入剖析ReentrantLock源碼之公平鎖的實現

公平鎖與非公平鎖的區別
公平鎖:顧名思義--公平,大家老老實實排隊
非公平鎖:只要有機會,就先嘗試搶占資源
公平鎖與非公平鎖其實有點像在公廁上廁所。公平鎖遵守排隊的規則,只要前面有人在排隊,那麽剛進來的就老老實實排隊。而非公平鎖就有點流氓,只要當前茅坑沒人,它就占了那個茅坑,不管後面的人排了多久。

源碼解析
詳見視頻

非公平鎖的弊端
可能導致後面排隊等待的線程等不到相應的cpu資源,從而引起線程饑餓

07 掌控線程執行順序之多線程debug

詳見視頻


08 讀寫鎖特性及ReentrantReadWriteLock的使用

特性:寫寫互斥、讀寫互斥、讀讀共享
鎖降級:寫線程獲取寫入鎖後可以獲取讀取鎖,然後釋放寫入鎖,這樣就從寫入鎖變成了讀取鎖,從而實現鎖降級的特性。


09 源碼探秘之AQS如何用單一int值表示讀寫兩種狀態

int 是32位,將其拆分成兩個無符號short
高位表示讀鎖 低位表示寫鎖
0000000000000000 0000000000000000

兩種鎖的最大次數均為65535也即是2的16次方減去1

讀鎖: 每次都從當前的狀態加上65536
0000000000000000 0000000000000000
?0000000000000001 0000000000000000?
-----------------------------------
0000000000000001 0000000000000000?
0000000000000001 0000000000000000?
-----------------------------------
0000000000000010 0000000000000000?

獲取讀鎖個數,將state整個無符號右移16位就可得出讀鎖的個數
0000000000000001

寫鎖:每次都直接加1
0000000000000000 0000000000000000
0000000000000000 0000000000000001
-----------------------------------
0000000000000000 0000000000000001

獲取寫鎖的個數
0000000000000000 0000000000000001
?0000000000000000 1111111111111111?
-----------------------------------
0000000000000000 0000000000000001


10 深入剖析ReentrantReadWriteLock之讀鎖源碼實現

詳見視頻

11 深入剖析ReentrantReadWriteLock之寫鎖源碼實現

詳見視頻
0000000000000000 0000000000000001
?0000000000000000 1111111111111111?
-----------------------------------
0000000000000000 0000000000000001

12 鎖降級詳解

鎖降級:寫線程獲取寫入鎖後可以獲取讀取鎖,然後釋放寫入鎖,這樣就從寫入鎖變成了讀取鎖,從而實現鎖降級的特性。

註意點:鎖降級之後,寫鎖並不會直接降級成讀鎖,不會隨著讀鎖的釋放而釋放,因此需要顯式地釋放寫鎖

是否有鎖升級?
在ReentrantReadWriteLock裏面,不存在鎖升級這一說法

鎖降級的應用場景
用於對數據比較敏感,需要在對數據修改之後,獲取到修改後的值,並進行接下來的其他操作


13 StampedLock原理及使用

1.8之前,鎖已經那麽多了,為什麽還要有StampedLock?
一般應用,都是讀多寫少,ReentrantReadWriteLock 因讀寫互斥,故讀時阻塞寫,因而性能上上不去。可能會使寫線程饑餓

StampedLock的特點
所有獲取鎖的方法,都返回一個郵戳(Stamp),Stamp為0表示獲取失敗,其余都表示成功;
所有釋放鎖的方法,都需要一個郵戳(Stamp),這個Stamp必須是和成功獲取鎖時得到的Stamp一致;
StampedLock是不可重入的;(如果一個線程已經持有了寫鎖,再去獲取寫鎖的話就會造成死鎖)
支持鎖升級跟鎖降級
可以樂觀讀也可以悲觀讀
使用有限次自旋,增加鎖獲得的幾率,避免上下文切換帶來的開銷
樂觀讀不阻塞寫操作,悲觀讀,阻塞寫得操作

StampedLock的優點
相比於ReentrantReadWriteLock,吞吐量大幅提升

StampedLock的缺點
api相對復雜,容易用錯
內部實現相比於ReentrantReadWriteLock復雜得多

StampedLock的原理
每次獲取鎖的時候,都會返回一個郵戳(stamp),相當於mysql裏的version字段
釋放鎖的時候,再根據之前的獲得的郵戳,去進行鎖釋放

使用stampedLock註意點
如果使用樂觀讀,一定要判斷返回的郵戳是否是一開始獲得到的,如果不是,要去獲取悲觀讀鎖,再次去讀取

第五章--線程間的通信

1 wait、notify、notifyAll
何時使用
在多線程環境下,有時候一個線程的執行,依賴於另外一個線程的某種狀態的改變,這個時候,我們就可以使用wait與notify或者notifyAll

wait跟sleep的區別
wait會釋放持有的鎖,而sleep不會,sleep只是讓線程在指定的時間內,不去搶占cpu的資源

註意點
wait notify必須放在同步代碼塊中, 且必須擁有當前對象的鎖,即不能取得A對象的鎖,而調用B對象的wait
哪個對象wait,就得調哪個對象的notify

notify跟notifyAll的區別
nofity隨機喚醒一個等待的線程
notifyAll喚醒所有在該對象上等待的線程


2 等待通知經典模型之生產者消費者

生產者消費者模型一般包括:生產者、消費者、中間商
詳見視頻


3 使用管道流進行通信

以內存為媒介,用於線程之間的數據傳輸。
主要有面向字節:【PipedOutputStream、PipedInputStream】、面向字符【PipedReader、PipedWriter】


4 Thread.join通信及其源碼淺析

使用場景:線程A執行到一半,需要一個數據,這個數據需要線程B去執行修改,只有B修改完成之後,A才能繼續操作
線程A的run方法裏面,調用線程B的join方法,這個時候,線程A會等待線程B運行完成之後,再接著運行


5 ThreadLocal的使用

線程變量,是一個以ThreadLocal對象為鍵、任意對象為值的存儲結構。為每個線程單獨存放一份變量副本,也就是說一個線程可以根據一個ThreadLocal對象查詢到綁定在這個線程上的一個值。
只要線程處於活動狀態並且ThreadLocal實例可訪問,那麽每個線程都擁有對其本地線程副本的隱式引用變量一個線程消失後,它的所有副本線程局部實例受垃圾回收(除非其他存在對這些副本的引用)

一般用的比較多的是
1、ThreadLocal.get: 獲取ThreadLocal中當前線程共享變量的值。
2、ThreadLocal.set: 設置ThreadLocal中當前線程共享變量的值。
3、ThreadLocal.remove: 移除ThreadLocal中當前線程共享變量的值。
4、ThreadLocal.initialValue: ThreadLocal沒有被當前線程賦值時或當前線程剛調用remove方法後調用get方法,返回此方法值。


6 Condition的使用

可以在一個鎖裏面,存在多種等待條件
主要的方法
await
signal
signalAll

第六章--原子類


1 什麽是原子類

一度認為原子是不可分割的最小單位,故原子類可以認為其操作都是不可分割

為什麽要有原子類?

對多線程訪問同一個變量,我們需要加鎖,而鎖是比較消耗性能的,JDk1.5之後,
新增的原子操作類提供了一種用法簡單、性能高效、線程安全地更新一個變量的方式,
這些類同樣位於JUC包下的atomic包下,發展到JDk1.8,該包下共有17個類,
囊括了原子更新基本類型、原子更新數組、原子更新屬性、原子更新引用

1.8新增的原子類
DoubleAccumulator、DoubleAdder、LongAccumulator、LongAdder、Striped64


2 原子更新基本類型

發展至JDk1.8,基本類型原子類有以下幾個:
AtomicBoolean、AtomicInteger、AtomicLong、DoubleAccumulator、DoubleAdder、
LongAccumulator、LongAdder
大致可以歸為3類
AtomicBoolean、AtomicInteger、AtomicLong 元老級的原子更新,方法幾乎一模一樣
DoubleAdder、LongAdder 對Double、Long的原子更新性能進行優化提升
DoubleAccumulator、LongAccumulator 支持自定義運算


3 原子更新數組類型

AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray


4 原子地更新屬性

原子地更新某個類裏的某個字段時,就需要使用原子更新字段類,Atomic包提供了以下4個類進行原子字段更新
AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicStampedReference、AtomicReferenceFieldUpdater

使用上述類的時候,必須遵循以下原則
字段必須是volatile類型的,在線程之間共享變量時保證立即可見
字段的描述類型是與調用者與操作對象字段的關系一致。
也就是說調用者能夠直接操作對象字段,那麽就可以反射進行原子操作。
對於父類的字段,子類是不能直接操作的,盡管子類可以訪問父類的字段。
只能是實例變量,不能是類變量,也就是說不能加static關鍵字。
只能是可修改變量,不能使final變量,因為final的語義就是不可修改。
對於AtomicIntegerFieldUpdater和AtomicLongFieldUpdater只能修改int/long類型的字段,不能修改其包裝類型(Integer/Long)。
如果要修改包裝類型就需要使用AtomicReferenceFieldUpdater。


5 原子更新引用

AtomicReference:用於對引用的原子更新
AtomicMarkableReference:帶版本戳的原子引用類型,版本戳為boolean類型。
AtomicStampedReference:帶版本戳的原子引用類型,版本戳為int類型。


第七章--容器

1 同步容器與並發容器

同步容器
Vector、HashTable -- JDK提供的同步容器類
Collections.synchronizedXXX 本質是對相應的容器進行包裝

同步容器類的缺點
在單獨使用裏面的方法的時候,可以保證線程安全,但是,復合操作需要額外加鎖來保證線程安全
使用Iterator叠代容器或使用使用for-each遍歷容器,在叠代過程中修改容器會拋出ConcurrentModificationException異常。想要避免出現ConcurrentModificationException,就必須在叠代過程持有容器的鎖。但是若容器較大,則叠代的時間也會較長。那麽需要訪問該容器的其他線程將會長時間等待。從而會極大降低性能。
若不希望在叠代期間對容器加鎖,可以使用"克隆"容器的方式。使用線程封閉,由於其他線程不會對容器進行修改,可以避免ConcurrentModificationException。但是在創建副本的時候,存在較大性能開銷。
toString,hashCode,equalse,containsAll,removeAll,retainAll等方法都會隱式的Iterate,也即可能拋出ConcurrentModificationException。

並發容器
CopyOnWrite、Concurrent、BlockingQueue
根據具體場景進行設計,盡量避免使用鎖,提高容器的並發訪問性。
ConcurrentBlockingQueue:基於queue實現的FIFO的隊列。隊列為空,取操作會被阻塞
ConcurrentLinkedQueue,隊列為空,取得時候就直接返回空

LinkedBlockingQueue的使用及其源碼探秘
在並發編程中,LinkedBlockingQueue使用的非常頻繁。因其可以作為生產者消費者的中間商

add 實際上調用的是offer,區別是在隊列滿的時候,add會報異常
offer 對列如果滿了,直接入隊失敗
put("111"); 在隊列滿的時候,會進入阻塞的狀態


remove(); 直接調用poll,唯一的區別即使remove會拋出異常,而poll在隊列為空的時候直接返回null
poll(); 在隊列為空的時候直接返回null
take(); 在隊列為空的時候,會進入等待的狀態

第八章--並發工具類

CountDownLatch
await(),進入等待的狀態
countDown(),計數器減一
應用場景:啟動三個線程計算,需要對結果進行累加。

CyclicBarrier--柵欄
允許一組線程相互等待達到一個公共的障礙點,之後再繼續執行

跟countDownLatch的區別
CountDownLatch一般用於某個線程等待若幹個其他線程執行完任務之後,它才執行;不可重復使用
CyclicBarrier一般用於一組線程互相等待至某個狀態,然後這一組線程再同時執行;可重用的

Semaphore--信號量
控制並發數量
使用場景:接口限流

Exchanger
用於交換數據

它提供一個同步點,在這個同步點兩個線程可以交換彼此的數據。這兩個線程通過exchange方法交換數據, 如果第一個線程先執行exchange方法,它會一直等待第二個線程也執行exchange,當兩個線程都到達同步點時,這兩個線程就可以交換數據,將本線程生產出來的數據傳遞給對方。因此使用Exchanger的重點是成對的線程使用exchange()方法,當有一對線程達到了同步點,就會進行交換數據。因此該工具類的線程對象是【成對】的。

第九章—線程池及Executor框架

1 為什麽要使用線程池?

諸如 Web 服務器、數據庫服務器、文件服務器或郵件服務器之類的許多服務器應用程序都面向處理來自某些遠程來源的大量短小的任務。請求以某種方式到達服務器,這種方式可能是通過網絡協議(例如 HTTP、FTP )、通過 JMS隊列或者可能通過輪詢數據庫。 不管請求如何到達,服務器應用程序中經常出現的情況是:單個任務處理的時間很短而請求的數目卻是巨大的。每當一個請求到達就創建一個新線程,然後在新線程中為請求服務,但是頻繁的創建線程,銷毀線程所帶來的系統開銷其實是非常大的。

線程池為線程生命周期開銷問題和資源不足問題提供了解決方案。通過對多個任務重用線程,線程創建的開銷被分攤到了多個任務上。其好處是,因為在請求到達時線程已經存在,所以無意中也消除了線程創建所帶來的延遲。這樣,就可以立即為請求服務,使應用程序響應更快。而且,通過適當地調整線程池中的線程數目,也就是當請求的數目超過某個閾值時,就強制其它任何新到的請求一直等待,直到獲得一個線程來處理為止,從而可以防止資源不足。

風險與機遇
用線程池構建的應用程序容易遭受任何其它多線程應用程序容易遭受的所有並發風險,
諸如同步錯誤和死鎖,它還容易遭受特定於線程池的少數其它風險,諸如與池有關的死鎖、資源不足和線程泄漏。


2 創建線程池及其使用

詳見視頻


3 Future與Callable、FutureTask

Callable與Runable功能相似,Callable的call有返回值,可以返回給客戶端,而Runable沒有返回值,一般情況下,Callable與FutureTask一起使用,或者通過線程池的submit方法返回相應的Future

Future就是對於具體的Runnable或者Callable任務的執行結果進行取消、查詢是否完成、獲取結果、設置結果操作。get方法會阻塞,直到任務返回結果

FutureTask則是一個RunnableFuture,而RunnableFuture實現了Runnbale又實現了Futrue這兩個接口


4 線程池的核心組成部分及其運行機制

corePoolSize:核心線程池大小 cSize
maximumPoolSize:線程池最大容量 mSize
keepAliveTime:當線程數量大於核心時,多余的空閑線程在終止之前等待新任務的最大時間。
unit:時間單位
workQueue:工作隊列 nWorks
ThreadFactory:線程工廠
handler:拒絕策略

運行機制
通過new創建線程池時,除非調用prestartAllCoreThreads方法初始化核心線程,否則此時線程池中有0個線程,即使工作隊列中存在多個任務,同樣不會執行

任務數X
x <= cSize 只啟動x個線程

x >= cSize && x < nWorks + cSize 會啟動 <= cSize 個線程 其他的任務就放到工作隊列裏

x > cSize && x > nWorks + cSize
x-(nWorks) <= mSize 會啟動x-(nWorks)個線程
x-(nWorks) > mSize 會啟動mSize個線程來執行任務,其余的執行相應的拒絕策略


5 線程池拒絕策略

AbortPolicy:該策略直接拋出異常,阻止系統正常工作
CallerRunsPolicy:只要線程池沒有關閉,該策略直接在調用者線程中,執行當前被丟棄的任務(叫老板幫你幹活)
DiscardPolicy:直接啥事都不幹,直接把任務丟棄
DiscardOldestPolicy:丟棄最老的一個請求(任務隊列裏面的第一個),再嘗試提交任務


6 Executor框架

通過相應的方法,能創建出6種線程池
ExecutorService executorService = Executors.newCachedThreadPool();
ExecutorService executorService1 = Executors.newFixedThreadPool(2);
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
ExecutorService executorService2 = Executors.newWorkStealingPool();
ExecutorService executorService3 = Executors.newSingleThreadExecutor();
ScheduledExecutorService scheduledExecutorService1 = Executors.newSingleThreadScheduledExecutor();

上面的方法最終都創建了ThreadPoolExecutor
newCachedThreadPool:創建一個可以根據需要創建新線程的線程池,如果有空閑線程,優先使用空閑的線程
newFixedThreadPool:創建一個固定大小的線程池,在任何時候,最多只有N個線程在處理任務
newScheduledThreadPool:能延遲執行、定時執行的線程池
newWorkStealingPool:工作竊取,使用多個隊列來減少競爭
newSingleThreadExecutor:單一線程的線程次,只會使用唯一一個線程來執行任務,即使提交再多的任務,也都是會放到等待隊列裏進行等待
newSingleThreadScheduledExecutor:單線程能延遲執行、定時執行的線程池


7 線程池的使用建議

盡量避免使用Executor框架創建線程池
newFixedThreadPool newSingleThreadExecutor
允許的請求隊列長度為 Integer.MAX_VALUE,可能會堆積大量的請求,從而導致 OOM。

newCachedThreadPool newScheduledThreadPool
允許的創建線程數量為 Integer.MAX_VALUE,可能會創建大量的線程,從而導致 OOM

為什麽第二個例子,在限定了堆的內存之後,還會把整個電腦的內存撐爆
創建線程時用的內存並不是我們制定jvm堆內存,而是系統的剩余內存。(電腦內存-系統其它程序占用的內存-已預留的jvm內存)

創建線程池時,核心線程數不要過大

相應的邏輯,發生異常時要處理

submit 如果發生異常,不會立即拋出,而是在get的時候,再拋出異常
execute 直接拋出異常


第十章--jvm與並發

1 jvm內存模型

硬件內存模型
處理器--》高速緩存--》緩存一致性協議--》主存
java內存模型
線程《--》工作內存《--》save和load 《---》主存

java內存間的交互操作
(1)lock(鎖定):作用於主內存的變量,把一個變量標記為一條線程獨占狀態
(2)unlock(解鎖):作用於主內存的變量,把一個處於鎖定狀態的變量釋放出來,釋放後的變量才可以被其他線程鎖定
(3)read(讀取):作用於主內存的變量,把一個變量值從主內存傳輸到線程的工作內存中,以便隨後的load動作使用
(4)load(載入):作用於工作內存的變量,它把read操作從主內存中得到的變量值放入工作內存的變量副本中
(5)use(使用):作用於工作內存的變量,把工作內存中的一個變量值傳遞給執行引擎
(6)assign(賦值):作用於工作內存的變量,它把一個從執行引擎接收到的值賦給工作內存的變量
(7)store(存儲):作用於工作內存的變量,把工作內存中的一個變量的值傳送到主內存中,以便隨後的write的操作
(8)write(寫入):作用於主內存的變量,它把store操作從工作內存中的一個變量的值傳送到主內存的變量中

上面8中操作必須滿足以下規則
1、不允許read和load、store和write操作之一單獨出現,即不允許一個變量從主內存讀取了但工作內存不接受,或者從工作內存發起回寫了但主內存不接受的情況出現。
2、不允許一個線程丟棄它的最近的assign操作,即變量在工作內存中改變了之後必須把該變化同步回主內存。
3、不允許一個線程無原因地(沒有發生過任何assign操作)把數據從線程的工作內存同步回主內存。
4、一個新的變量只能在主內存中“誕生”,不允許在工作內存中直接使用一個未被初始化(load或assign)的變量,換句話說,就是對一個變量實施use、store操作之前,必須先執行過了assign和load操作。
5、一個變量在同一時刻只允許一條線程對其進行lock操作,但lock操作可以被同一條線程重復執行多次,多次執行lock後,只有執行相同次數的unlock操作,變量才會被解鎖。
6、如果對一個變量執行lock操作,那將會清空工作內存中此變量的值,在執行引擎使用這個變量前,需要重新執行load或assign操作初始化變量的值。
7、如果一個變量事先沒有被lock操作鎖定,那就不允許對它執行unlock操作,也不允許去unlock一個被其他線程鎖定住的變量。
8、對一個變量執行unlock操作之前,必須先把此變量同步回主內存中(執行store、write操作)。


2 先行發生原則 happens-before

判斷數據是有有競爭、線程是否安全的主要依據
1. 程序次序規則:同一個線程內,按照代碼出現的順序,前面的代碼先行於後面的代碼,準確的說是控制流順序,因為要考慮到分支和循環結構。

2. 管程鎖定規則:一個unlock操作先行發生於後面(時間上)對同一個鎖的lock操作。

3. volatile變量規則:對一個volatile變量的寫操作先行發生於後面(時間上)對這個變量的讀操作。

4. 線程啟動規則:Thread的start( )方法先行發生於這個線程的每一個操作。

5. 線程終止規則:線程的所有操作都先行於此線程的終止檢測。可以通過Thread.join( )方法結束、Thread.isAlive( )的返回值等手段檢測線程的終止。

6. 線程中斷規則:對線程interrupt( )方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生,可以通過Thread.interrupt( )方法檢測線程是否中斷

7. 對象終結規則:一個對象的初始化完成先行於發生它的finalize()方法的開始。

8. 傳遞性:如果操作A先行於操作B,操作B先行於操作C,那麽操作A先行於操作C。

為什麽要有該原則?
無論jvm或者cpu,都希望程序運行的更快。如果兩個操作不在上面羅列出來的規則裏面,那麽久可以對他們進行任意的重排序。

時間先後順序與先行發生的順序之間基本沒有太大的關系。


3 指令重排序

什麽是指令重排序?
重排序是指編譯器和處理器為了優化程序性能而對指令序列進行重新排序的一種手段。
數據依賴性
編譯器和處理器在重排序時,會遵守數據依賴性,編譯器和處理器不會改變存在數據依賴關系的兩個操作的執行順序。(僅針對單個處理器中執行的指令序列和單個線程中執行的操作,不同處理器之間和不同線程之間的數據依賴性不被編譯器和處理器考慮。)

兩操作訪問同一個變量,其兩個操作中有至少一個寫操作,此時就存在依賴性
寫後讀 a=0 b=a
讀後寫 a=b b=1
寫後寫 a=1 a=2

a=1,b=1
寫後讀 a=0 b=a 正確b=0 錯誤b=1
as-if-serial原則
不管怎麽重排序(編譯器和處理器為了提高並行度),(單線程)程序的執行結果不能被改變。

x=0,y=1
x=1, y=0
x=1, y=1
x=0, y=0

第十一章—實戰

1 數據同步接口--需求分析

業務場景:
一般系統,多數會與第三方系統的數據打交道,而第三方的生產庫,並不允許我們直接操作。在企業裏面,一般都是通過中間表進行同步,即第三方系統將生產數據放入一張與其生產環境隔離的另一個獨立的庫中的獨立的表,再根據接口協議,增加相應的字段。而我方需要讀取該中間表中的數據,並對數據進行同步操作。此時就需要編寫相應的程序進行數據同步。

數據同步一般分兩種情況
全量同步:每天定時將當天的生產數據全部同步過來(優點:實現簡單 缺點:數據同步不及時)
增量同步:每新增一條,便將該數據同步過來(優點:數據近實時同步 缺點:實現相對困難)

我方需要做的事情:
讀取中間表的數據,並同步到業務系統中(可能需要調用我方相應的業務邏輯)
模型抽離
生產者消費者模型
生產者:讀取中間表的數據
消費者:消費生產者生產的數據

接口協議的制定
1.取我方業務所需要的字段
2.需要有字段記錄數據什麽時候進入中間表
3.增加相應的數據標誌位,用於標誌數據的同步狀態
4.記錄數據的同步時間

技術選型:
mybatis、單一生產者多消費者、多線程並發操作

2 中間表設計
詳見視屏
3 基礎環境搭建
詳見視屏

4 生產者代碼實現

1:分批讀取中間表(10I),並將讀取到的數據狀態修改為10D(處理中)
2:將相應的數據交付給消費者進行消費
1:把生產完的數據,直接放到隊列裏,由消費者去進行消費
2:把消費者放到隊列裏面,生產完數據,直接從隊列裏拿出消費者進行消費

並發編程及原理視頻教程,基礎理論+實戰,由淺入深,層層深入,剖析並發編程原理
更多課程資料可以查看https://xdclass.net/#/coursecatalogue?video_id=17
贈送大量的學習資料以及幹貨

技術分享圖片

互聯網架構多線程並發編程高級教程(下)