1. 程式人生 > 其它 >多執行緒與高併發筆記

多執行緒與高併發筆記

1. 建立執行緒的四種方式
實現Runnable 重寫run方法
繼承Thread 重寫run方法
執行緒池建立 Executors.newCachedThreadPool()
實現Callable介面
2. Thread執行緒操作方法
當前執行緒睡眠指定mills毫秒

Thread.sleep([mills])
當前執行緒優雅讓出執行權

Thread.yield()
例如Thread t1, t2,在t2的run方法中呼叫t1.join(),執行緒t2將等待t1完成後執行

join
3. Thread狀態
狀態 使用場景
NEW Thread被建立之後,未start之前
RUNNABLE 在呼叫start()方法之後,這也是執行緒進入執行狀態的唯一一種方式。
具體分為ready跟running,當執行緒被掛起或者呼叫Thread.yield()的時候為ready
WAITING 當一個執行緒執行了Object.wait()的時候,它一定在等待另一個執行緒執行Object.notify()或者Object.notifyAll()。
或者一個執行緒thread,其在主執行緒中被執行了thread.join()的時候,主執行緒即會等待該執行緒執行完成。當一個執行緒執行了LockSupport.park()的時候,其在等待執行LockSupport.unpark(thread)。當該執行緒處於這種等待的時候,其狀態即為WAITING。需要關注的是,這邊的等待是沒有時間限制的,當發現有這種狀態的執行緒的時候,若其長時間處於這種狀態,也需要關注下程式內部有無邏輯異常。
TIMED_WAITING
這個狀態和WAITING狀態的區別就是,這個狀態的等待是有一定時效的
Thread.sleep(long)
Object.wait(long)
Thread.join(long)
LockSupport.parkNanos()
LockSupport.parkUntil()
BLOCKED 在進入synchronized關鍵字修飾的方法或程式碼塊(獲取鎖)時的狀態
TERMINATED 執行緒執行結束之後的狀態。
執行緒一旦終止了,就不能復生。
在一個終止的執行緒上呼叫start()方法,會丟擲java.lang.IllegalThreadStateException異常

4. synchronized
鎖住的是物件而不是程式碼
this 等價於 當前類.class
鎖定方法,非鎖定方法同時進行
鎖在執行過程中發生異常會自動釋放鎖
synchronized獲得的鎖是可重入的
鎖升級 偏向鎖-自旋鎖-重量級鎖
synchronized(object)不能用String常量/Integer,Long等基本資料型別
鎖定物件的時候要保證物件不能被重寫,最好加final定義
4. volatile
保證執行緒可見性
禁止指令重排序
volatile並不能保證多個執行緒修改的一致性,要保持一致性還是需要synchronized關鍵字
volatile 引用型別(包括陣列)只能保證引用本身的可見性,不能保證內部欄位的可見性
volatile關 鍵字只能用於變數而不可以修飾方法以及程式碼塊
5. synchronized與AtomicLong以及LongAdder的效率對比
Synchronized 是需要加鎖的,效率偏低;
AtomicLong 不需要申請鎖,使用CAS機制;
LongAdder 使用分段鎖,所以效率好,在併發數量特別高的時候,LongAdder最合適

6. ConcurrentHashMap的分段鎖原理
分段鎖就是將資料分段上鎖,把鎖進一步細粒度化,有助於提升併發效率。
HashTable容器在競爭激烈的併發環境下表現出效率低下的原因是所有訪問HashTable的執行緒都必須競爭同一把鎖,假如容器裡有多把鎖,每一把鎖用於鎖容器其中一部分資料,那麼當多執行緒訪問容器裡不同資料段的資料時,執行緒間就不會存在鎖競爭,從而可以有效提高併發訪問效率,這就是ConcurrentHashMap所使用的鎖分段技術。首先將資料分成一段一段地儲存,然後給每一段資料配一把鎖,當一個執行緒佔用鎖訪問其中一個段資料的時候,其他段的資料也能被其他執行緒訪問。

7. ReentrantLock
ReentrantLock可以替代synchronized
但是ReentrantLock必須手動開啟鎖/關閉鎖,synchronized遇到異常會自動釋放鎖,ReentrantLock需要手動關閉,一般都是放在finally中關閉
定義鎖 Lock lock = new ReentrantLock();
開啟 lock.lock();
關閉 lock.unlock();
使用Reentrantlock可以進行“嘗試鎖定”tryLock,這樣無法鎖定,或者在指定時間內無法鎖定,執行緒可以決定是否繼續等待。
使用tryLock進行嘗試鎖定,不管鎖定與否,方法都將繼續執行
可以根據tryLock的返回值來判定是否鎖定
也可以指定tryLock的時間,由於tryLock(time)丟擲異常,所以要注意unclock的處理,必須放到finally中,如果tryLock未鎖定,則不需要unlock
使用ReentrantLock還可以呼叫lockInterruptibly方法,可以對執行緒interrupt方法做出響應,在一個執行緒等待鎖的過程中,可以被打斷
new ReentrantLock(true) 表示公平鎖,不帶引數預設為false,非公平鎖

8. CountDownLatch
countDownLatch這個類可以使一個執行緒等待其他執行緒各自執行完畢後再執行。
是通過一個計數器來實現的,計數器的初始值是執行緒的數量。當呼叫countDown()方法後,每當一個執行緒執行完畢後,計數器的值就-1,當計數器的值為0時,表示所有執行緒都執行完畢,然後在閉鎖上等待的執行緒就可以恢復工作了。

執行緒中呼叫countDown()方法開始計數;
在呼叫await()方法的執行緒中,當計數器為0是後續才會繼續執行,否則一直等待;
也可以使用latch.await(timeout, unit)在等待timeout時間後如果計數器不為0,執行緒仍將繼續。
countDown()之後的程式碼不受計數器控制
與join區別,使用join的執行緒將被阻塞,使用countDown的執行緒不受影響,只有呼叫await的時候才會阻塞

8. CyclicBarrier
作用就是會讓指定數量的(數量由建構函式指定)所有執行緒都等待完成後才會繼續下一步行動。
建構函式:
public CyclicBarrier(int parties)
public CyclicBarrier(int parties, Runnable barrierAction)
parties 是執行緒的個數;
barrierAction為最後一個到達執行緒要做的任務

所有執行緒會等待全部執行緒到達柵欄之後才會繼續執行,並且最後到達的執行緒會完成 Runnable 的任務。

實現原理:在CyclicBarrier的內部定義了一個Lock物件,每當一個執行緒呼叫await方法時,將攔截的執行緒數減1,然後判斷剩餘攔截數是否為初始值parties,如果不是,進入Lock物件的條件佇列等待。如果是,執行barrierAction物件的Runnable方法,然後將鎖的條件佇列中的所有執行緒放入鎖等待佇列中,這些執行緒會依次的獲取鎖、釋放鎖。

9. Phaser
可重複使用的同步屏障,功能類似於CyclicBarrier和CountDownLatch,但支援更靈活的使用。

Phaser使我們能夠建立在邏輯執行緒需要才去執行下一步的障礙等。

我們可以協調多個執行階段,為每個程式階段重用Phaser例項。每個階段可以有不同數量的執行緒等待前進到另一個階段。我們稍後會看一個使用階段的示例。

要參與協調,執行緒需要使用Phaser例項 register() 本身。請注意:這隻會增加註冊方的數量,我們無法檢查當前執行緒是否已註冊 - 我們必須將實現子類化以支援此操作。

執行緒通過呼叫 arriAndAwaitAdvance() 來阻止它到達屏障,這是一種阻塞方法。當數量到達等於註冊的數量時,程式的執行將繼續,並且數量將增加。我們可以通過呼叫getPhase()方法獲取當前數量。

10. ReadWriteLock
ReadWriteLock的具體實現是ReentrantReadWriteLock

ReadWriteLock允許分別建立讀鎖跟寫鎖

ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
Lock readLock = readWriteLock.readLock();
Lock writeLock = readWriteLock.writeLock();
1
2
3
使用ReadWriteLock時,適用條件是同一個資料,有大量執行緒讀取,但僅有少數執行緒修改。
ReadWriteLock可以保證:

只允許一個執行緒寫入(其他執行緒既不能寫入也不能讀取);
沒有寫入時,多個執行緒允許同時讀(提高效能)
讀寫分離鎖可以有效地幫助減少鎖競爭,以提高系統性能,讀寫鎖讀讀之間不互斥,讀寫,寫寫都是互斥的

11. Semaphore
Semaphore 是一個計數訊號量,必須由獲取它的執行緒釋放。常用於限制可以訪問某些資源的執行緒數量,例如通過 Semaphore 限流。

對於Semaphore來說,它要保證的是資源的互斥而不是資源的同步,在同一時刻是無法保證同步的,但是卻可以保證資源的互斥。只是限制了訪問某些資源的執行緒數,其實並沒有實現同步。

常用方法:
1、acquire(int permits)

從此訊號量獲取給定數目的許可,在提供這些許可前一直將執行緒阻塞,或者執行緒已被中斷。就好比是一個學生佔兩個視窗。這同時也對應了相應的release方法。

2、release(int permits)

釋放給定數目的許可,將其返回到訊號量。這個是對應於上面的方法,一個學生佔幾個視窗完事之後還要釋放多少

3、availablePermits()

返回此訊號量中當前可用的許可數。也就是返回當前還有多少個視窗可用。

4、reducePermits(int reduction)

根據指定的縮減量減小可用許可的數目。

5、hasQueuedThreads()

查詢是否有執行緒正在等待獲取資源。

6、getQueueLength()

返回正在等待獲取的執行緒的估計數目。該值僅是估計的數字。

7、tryAcquire(int permits, long timeout, TimeUnit unit)

如果在給定的等待時間內此訊號量有可用的所有許可,並且當前執行緒未被中斷,則從此訊號量獲取給定數目的許可。

8、acquireUninterruptibly(int permits)

從此訊號量獲取給定數目的許可,在提供這些許可前一直將執行緒阻塞。

12. Exchanger
用於兩個工作執行緒之間交換資料的封裝工具類,簡單說就是一個執行緒在完成一定的事務後想與另一個執行緒交換資料,則第一個先拿出資料的執行緒會一直等待第二個執行緒,直到第二個執行緒拿著資料到來時才能彼此交換對應資料。其定義為 Exchanger 泛型型別,其中 V 表示可交換的資料型別,對外提供的介面很簡單,具體如下:

Exchanger():無參構造方法。

V exchange(V v):等待另一個執行緒到達此交換點(除非當前執行緒被中斷),然後將給定的物件傳送給該執行緒,並接收該執行緒的物件。

V exchange(V v, long timeout, TimeUnit unit):等待另一個執行緒到達此交換點(除非當前執行緒被中斷或超出了指定的等待時間),然後將給定的物件傳送給該執行緒,並接收該執行緒的物件。

13. LockSupport
LockSupport 是一個非常方便實用的執行緒阻塞工具,他可以在任意位置讓執行緒阻塞。

LockSupport 的靜態方法 park()可以阻塞當前執行緒,類似的還有 parkNanos(),parkUntil()等,他們實現了一個限時的等待。

方法 描述
void park(): 阻塞當前執行緒,如果呼叫unpark方法或者當前執行緒被中斷,從能從park()方法中返回
void park(Object blocker) 功能同方法1,入參增加一個Object物件,用來記錄導致執行緒阻塞的阻塞物件,方便進行問題排查;
void parkNanos(long nanos) 阻塞當前執行緒,最長不超過nanos納秒,增加了超時返回的特性;
void parkNanos(Object blocker, long nanos) 功能同方法3,入參增加一個Object物件,用來記錄導致執行緒阻塞的阻塞物件,方便進行問題排查;
void parkUntil(long deadline) 阻塞當前執行緒,直到deadline;
void parkUntil(Object blocker, long deadline) 功能同方法5,入參增加一個Object物件,用來記錄導致執行緒阻塞的阻塞物件,方便進行問題排查;
同樣的,有阻塞的方法,當然有喚醒的方法,什麼呢?unpark(Thread) 方法。該方法可以將指定執行緒喚醒。

需要注意的是:park 方法和 unpark 方法執行順序不是那麼的嚴格。比如我們在 Thread 類中提到的 suspend 方法 和resume 方法,如果順序錯誤,將導致永遠無法喚醒,但 park 方法和 unpark 方法則不會,因為 LockSupport 使用了類似訊號量的機制。他為每一個執行緒準備了一個許可(預設不可用),如果許可能用,那麼 park 函式會立即返回,並且消費這個許可(也就是將許可變為不可用),如果許可不可用,將會阻塞。而 unpark 方法則使得一個許可變為可用

14. AQS
AQS 為 AbstractQueuedSynchronizer 的簡稱

AQS是JDK下提供的一套用於實現基於FIFO等待佇列的阻塞鎖和相關的同步器的一個同步框架。
這個抽象類被設計為作為一些可用原子int值來表示狀態的同步器的基類。
AQS管理一個關於狀態資訊的單一整數,該整數可以表現任何狀態。
#比如
Semaphore 用它來表現剩餘的許可數,
ReentrantLock 用它來表現擁有它的執行緒已經請求了多少次鎖;
FutureTask 用它來表現任務的狀態(尚未開始、執行、完成和取消)

使用須知
Usage
To use this class as the basis of a synchronizer, redefine the following methods, as applicable, by inspecting and/or modifying the synchronization state using {@link #getState}, {@link #setState} and/or {@link #compareAndSetState}:

{@link #tryAcquire}
{@link #tryRelease}
{@link #tryAcquireShared}
{@link #tryReleaseShared}>
{@link #isHeldExclusively}
以上方法不需要全部實現,根據獲取的鎖的種類可以選擇實現不同的方法:
支援獨佔(排他)獲取鎖的同步器應該實現tryAcquire、 tryRelease、isHeldExclusively;
支援共享獲取鎖的同步器應該實現tryAcquireShared、tryReleaseShared、isHeldExclusively。

AQS淺析
AQS的實現主要在於維護一個"volatile int state"(代表共享資源)和
一個FIFO執行緒等待佇列(多執行緒爭用資源被阻塞時會進入此佇列)。
佇列中的每個節點是對執行緒的一個封裝,包含執行緒基本資訊,狀態,等待的資源型別等。

#state的訪問方式有三種:
getState()
setState()
compareAndSetState()

#AQS定義兩種資源共享方式
Exclusive(獨佔,只有一個執行緒能執行,如ReentrantLock)
Share(共享,多個執行緒可同時執行,如Semaphore/CountDownLatch)
不同的自定義同步器爭用共享資源的方式也不同。
自定義同步器在實現時只需要實現共享資源state的獲取與釋放方式即可,
至於具體執行緒等待佇列的維護(如獲取資源失敗入隊/喚醒出隊等),AQS已經在頂層實現好了。
自定義同步器實現時主要實現以下幾種方法:

isHeldExclusively():該執行緒是否正在獨佔資源。只有用到condition才需要去實現它。

tryAcquire(int):獨佔方式。嘗試獲取資源,成功則返回true,失敗則返回false。

tryRelease(int):獨佔方式。嘗試釋放資源,成功則返回true,失敗則返回false。
tryAcquireShared(int):共享方式。嘗試獲取資源。負數表示失敗;0表示成功,但沒有剩餘可用資源;正數表示成功,且有剩餘資源。

tryReleaseShared(int):共享方式。嘗試釋放資源,如果釋放後允許喚醒後續等待結點返回true,否則返回false。

以ReentrantLock為例

state初始化為0,表示未鎖定狀態。

A執行緒lock()時,會呼叫tryAcquire()獨佔該鎖並將state+1。

此後,其他執行緒再tryAcquire()時就會失敗,直到A執行緒unlock()到state=0(即釋放鎖)為止,其它執行緒才有機會獲取該鎖。

當然,釋放鎖之前,A執行緒自己是可以重複獲取此鎖的(state會累加),這就是可重入的概念。

但要注意,獲取多少次就要釋放多麼次,這樣才能保證state是能回到零態的。

以CountDownLatch以例

任務分為N個子執行緒去執行,state也初始化為N(注意N要與執行緒個數一致)。

這N個子執行緒是並行執行的,每個子執行緒執行完後countDown()一次,state會CAS減1。

等到所有子執行緒都執行完後(即state=0),會unpark()主呼叫執行緒,然後主呼叫執行緒就會從await()函式返回,繼續後餘動作。

一般來說,自定義同步器要麼是獨佔方法,要麼是共享方式,

他們也只需實現tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一種即可。

但AQS也支援自定義同步器同時實現獨佔和共享兩種方式,如"ReentrantReadWriteLock"。

15. 鎖基本概念
公平鎖/非公平鎖
可重入鎖
獨享鎖/共享鎖
互斥鎖/讀寫鎖
樂觀鎖/悲觀鎖
分段鎖
偏向鎖/輕量級鎖/重量級鎖
自旋鎖
公平鎖/非公平鎖
公平鎖是指多個執行緒按照申請鎖的順序來獲取鎖。

非公平鎖是指多個執行緒獲取鎖的順序並不是按照申請鎖的順序,
有可能後申請的執行緒比先申請的執行緒優先獲取鎖;
有可能會造成優先順序反轉或者飢餓現象。

對於Java ReentrantLock而言,通過建構函式指定該鎖是否是公平鎖,預設是非公平鎖。

非公平鎖的優點在於吞吐量比公平鎖大。

對於Synchronized而言,也是一種非公平鎖。
由於其並不像ReentrantLock是通過AQS的來實現執行緒排程,
所以並沒有任何辦法使其變成公平鎖。

可重入鎖
可重入鎖又名遞迴鎖,是指在同一個執行緒在外層方法獲取鎖的時候,在進入內層方法會自動獲取鎖。

ReentrantLock, Synchronized都是可重入鎖。

可重入鎖的一個好處是可一定程度避免死鎖

獨享(排他)鎖/共享鎖
獨享鎖是指該鎖一次只能被一個執行緒所持有。

共享鎖是指該鎖可被多個執行緒所持有。

對於ReentrantLock而言,其是獨享鎖。

但是對於Lock的另一個實現類ReadWriteLock,其讀鎖是共享鎖,其寫鎖是獨享鎖。
讀鎖的共享鎖可保證併發讀是非常高效的,讀寫,寫讀 ,寫寫的過程是互斥的。
獨享鎖與共享鎖也是通過AQS來實現的,通過實現不同的方法,來實現獨享或者共享。

對於Synchronized而言,當然是獨享鎖。

互斥鎖/讀寫鎖
上面講的獨享鎖/共享鎖就是一種廣義的說法,互斥鎖/讀寫鎖就是具體的實現。

互斥鎖在Java中的具體實現就是ReentrantLock

讀寫鎖在Java中的具體實現就是ReadWriteLock

樂觀鎖/悲觀鎖
樂觀鎖與悲觀鎖不是指具體的什麼型別的鎖,而是指看待併發同步的角度。

悲觀鎖 (Synchronized 和 ReentrantLock)

認為對於同一個資料的併發操作,一定是會發生修改的,哪怕沒有修改,也會認為修改。

因此對於同一個資料的併發操作,悲觀鎖採取加鎖的形式。
悲觀的認為,不加鎖的併發操作一定會出問題。

樂觀鎖 (java.util.concurrent.atomic包)

認為對於同一個資料的併發操作,是不會發生修改的。
在更新資料的時候,會採用嘗試更新,不斷重新的方式更新資料。
樂觀的認為,不加鎖的併發操作是沒有事情的。

悲觀鎖適合寫操作非常多的場景,樂觀鎖適合讀操作非常多的場景,

不加鎖會帶來大量的效能提升。

悲觀鎖在Java中的使用,就是利用各種鎖。

樂觀鎖在Java中的使用,是無鎖程式設計,常常採用的是CAS演算法。
典型的例子就是原子類,通過CAS自旋實現原子操作的更新。

分段鎖
分段鎖其實是一種鎖的設計,並不是具體的一種鎖,ConcurrentHashMap併發的實現就是通過分段鎖的形式來實現高效的併發操作。

ConcurrentHashMap中的分段鎖稱為Segment,
它類似於HashMap(JDK7與JDK8中HashMap的實現)的結構,
即內部擁有一個Entry陣列,陣列中的每個元素又是一個連結串列;
同時又是一個ReentrantLock(Segment繼承了ReentrantLock)。
當需要put元素的時候,並不是對整個hashmap進行加鎖,
而是先通過hashcode來知道他要放在那一個分段中,然後對這個分段進行加鎖,
所以當多執行緒put的時候,只要不是放在一個分段中,就實現了真正的並行的插入。
但是,在統計size的時候,可就是獲取hashmap全域性資訊的時候,就需要獲取所有的分段鎖才能統計。

分段鎖的設計目的是細化鎖的粒度,當操作不需要更新整個陣列的時候,
就僅僅針對陣列中的一項進行加鎖操作。

偏向鎖/輕量級鎖/重量級鎖
這三種鎖是指鎖的狀態,並且是針對Synchronized。
在Java 5通過引入鎖升級的機制來實現高效Synchronized。
這三種鎖的狀態是通過物件監視器在物件頭中的欄位來表明的。

偏向鎖

是指一段同步程式碼一直被一個執行緒所訪問,那麼該執行緒會自動獲取鎖。降低獲取鎖的代價。

輕量級鎖

是指當鎖是偏向鎖的時候,被另一個執行緒所訪問,偏向鎖就會升級為輕量級鎖,
其他執行緒會通過自旋的形式嘗試獲取鎖,不會阻塞,提高效能。

重量級鎖

是指當鎖為輕量級鎖的時候,另一個執行緒雖然是自旋,但自旋不會一直持續下去,
當自旋一定次數的時候,還沒有獲取到鎖,就會進入阻塞,該鎖膨脹為重量級鎖。
重量級鎖會讓其他申請的執行緒進入阻塞,效能降低。

自旋鎖
在Java中,自旋鎖是指嘗試獲取鎖的執行緒不會立即阻塞,而是採用迴圈的方式去嘗試獲取鎖,
這樣的好處是減少執行緒上下文切換的消耗,缺點是迴圈會消耗CPU。
典型的自旋鎖實現的例子,可以參考自旋鎖的實現.