Java多線程


1. 多線程

image.png
image.png
  1. 新建狀態: 一個新產生的線程從新狀態開始了它的生命周期。它保持這個狀態直到程序 start 這個線程。
  2. 運行狀態:當一個新狀態的線程被 start 以后,線程就變成可運行狀態,一個線程在此狀態下被認為是開始執行其任務
  3. 就緒狀態:當一個線程等待另外一個線程執行一個任務的時候,該線程就進入就緒狀態。當另一個線程給就緒狀態的線程發送信號時,該線程才重新切換到運行狀態。
  4. 休眠狀態: 由於一個線程的時間片用完了,該線程從運行狀態進入休眠狀態。當時間間隔到期或者等待的時間發生了,該狀態的線程切換到運行狀態。
  5. 終止狀態: 一個運行狀態的線程完成任務或者其他終止條件發生,該線程就切換到終止狀態。

2. 僵死進程

計算機的計算模型大部分是基於空間和時間來考慮的。僵死進程唯一占用的空間是pid空間,這個空間如果不能合理的應用就會造成浪費,之所以保留這個空間,是為了讓父進程感知子進程已經終止這個行為。時間方面,這個感知過程是一個異步的過程。

3. 創建線程的方式

繼承 Thread 類
實現 Runnable 接口
使用 Executor 框架

法一:繼承Thread類

1.1定義一個類繼承Thread

1.2重寫run方法

1.3創建對象

1.4調用start方法開啟線程

線程對象調用run()方法和start()方法區別?

調用run方法不開啟線程,僅是對象調用方法。

調用start方法開啟線程,並讓jvm調用run方法在開啟的線程中執行。

run()方法用來執行線程體中具體的內容

start()方法用來啟動線程對象,使其進入就緒狀態

法二:實現Runnable接口

2.1定義一個類實現Runnable接口。

2.2覆蓋run()方法。

2.3創建子類對象。

2.4創建Thread類對象,將實現Runnable接口的子類對象作為參數傳遞給Thread類對象的構造函數。

2.5調用start方法開啟線程。

Runnable優點:避免了繼承Thread類的單繼承局限性,更加符合面向對象

3. 線程安全類

何為線程安全的類

  在線程安全性的定義中,最核心的概念就是 正確性。當多個線程訪問某個類時,不管運行時環境采用何種調度方式或者這些線程將如何交替執行,並且在主調代碼中不需要任何額外的同步或協同,這個類都能表現出正確的行為,那么這個類就是線程安全的。

線程安全類

在集合框架中,有些類是線程安全的,這些都是jdk1.1中的出現的。在jdk1.2之后,就出現許許多多非線程安全的類。 下面是這些線程安全的同步的類:

vector:就比arraylist多了個同步化機制(線程安全),因為效率較低,現在已經不太建議使用。在web應用中,特別是前台頁面,往往效率(頁面響應速度)是優先考慮的。

statck:堆棧類,先進后出

hashtable:就比hashmap多了個線程安全

enumeration:枚舉,相當於迭代器

除了這些之外,其他的都是非線程安全的類和接口。

線程安全的類其方法是同步的,每次只能一個訪問。是重量級對象,效率較低。

4. 如何確保線程安全

在Java中可以有很多方法來保證線程安全,諸如:

通過加鎖(Lock/Synchronized)保證對臨界資源的同步互斥訪問;

使用volatile關鍵字,輕量級同步機制,但不保證原子性;

使用不變類 和 線程安全類(原子類,並發容器,同步容器等)。

5. 什么是死鎖

兩個線程或兩個以上線程都在等待對方執行完畢才能繼續往下執行的時候就發生了死鎖。結果就是這些線程都陷入了無限的等待中

6. wait()與 sleep()的區別

sleep()來自 Thread 類,wait()來自 Object 類;

調用 sleep()方法,線程不會釋放對象鎖。而調用 wait 方法線程會釋放對象鎖;

sleep()睡眠后不出讓系統資源,wait 讓其他線程可以占用 CPU;

sleep(milliseconds)需要指定一個睡眠時間,時間一到會自動喚醒。而 wait()需要配合 notify()

或者 notifyAll()使用。

7. 為什么wait(), notify()和notifyAll()必須在同步方法或者同步塊中被調用

 wait/notify機制是依賴於Java中Synchronized同步機制的,其目的在於確保等待線程從Wait()返回時能夠感知通知線程對共享變量所作出的修改。如果不在同步范圍內使用,就會拋出java.lang.IllegalMonitorStateException的異常。

8. 什么是 ThreadLocal?ThreadLocal 和 Synchonized 的區別?

線程局部變量。是局限於線程內部的變量,屬於線程自身所有,不在多個線程間共享。Java提供 ThreadLocal 類來支持線程局部變量,是一種實現線程安全的方式。

synchronized 是利用鎖的機制,使變量或代碼塊在某一時該只能被一個線程訪問。而 ThreadLocal 為每一個線程都提供了變量的副本,使得每個線程在某一時間訪問到的並不是同一個對象,這樣就隔離了多個線程對數據的數據共享。

9. ThreadLocal中的內存泄露問題(OOM):

如果ThreadLocal被設置為null后,並且沒有任何強引用指向它,根據垃圾回收的可達性分析算法,ThreadLocal將被回收。這樣的話,ThreadLocalMap中就會含有key為null的Entry,而且ThreadLocalMap是在Thread中的,只要線程遲遲不結束,這些無法訪問到的value就會形成內存泄露。為了解決這個問題,ThreadLocalMap中的getEntry()、set()和remove()函數都會清理key為null的Entry,以下面的getEntry()函數為例。

要注意的是ThreadLocalMap的key是一個弱引用。在這里我們分析一下強引用key和弱引用key的差別

強引用key:ThreadLocal被設置為null,由於ThreadLocalMap持有ThreadLocal的強引用,如果不手動刪除,那么ThreadLocal將不會回收,產生內存泄漏。

弱引用key:ThreadLocal被設置為null,由於ThreadLocalMap持有ThreadLocal的弱引用,即便不手動刪除,ThreadLocal仍會被回收,ThreadLocalMap在之后調用set()、getEntry()和remove()函數時會清除所有key為null的Entry。

ThreadLocalMap僅僅含有這些被動措施來補救內存泄露問題,如果在之后沒有調用ThreadLocalMap的set()、getEntry()和remove()函數的話,那么仍然會存在內存泄漏問題。在使用線程池的情況下,如果不及時進行清理,內存泄漏問題事小,甚至還會產生程序邏輯上的問題。所以,為了安全地使用ThreadLocal,必須要像每次使用完鎖就解鎖一樣,在每次使用完ThreadLocal后都要調用remove()來清理無用的Entry。

10. 多線程常見問題

上下文切換

多線程並不一定是要在多核處理器才支持的,就算是單核也是可以支持多線程的。 CPU 通過給每個線程分配一定的時間片,由於時間非常短通常是幾十毫秒,所以 CPU 可以不停的切換線程執行任務從而達到了多線程的效果。

但是由於在線程切換的時候需要保存本次執行的信息,在該線程被 CPU 剝奪時間片后又再次運行恢復上次所保存的信息的過程就稱為上下文切換。

上下文切換是非常耗效率的。

通常有以下解決方案:

  • 采用無鎖編程,比如將數據按照 Hash(id) 進行取模分段,每個線程處理各自分段的數據,從而避免使用鎖。

  • 采用 CAS(compare and swap) 算法,如 Atomic 包就是采用 CAS 算法。

  • 合理的創建線程,避免創建了一些線程但其中大部分都是處於 waiting 狀態,因為每當從 waiting 狀態切換到 running 狀態都是一次上下文切換。

死鎖

死鎖的場景一般是:線程 A 和線程 B 都在互相等待對方釋放鎖,或者是其中某個線程在釋放鎖的時候出現異常如死循環之類的。這時就會導致系統不可用。

常用的解決方案如下:

  • 盡量一個線程只獲取一個鎖。

  • 一個線程只占用一個資源。

  • 嘗試使用定時鎖,至少能保證鎖最終會被釋放。

資源限制

當在帶寬有限的情況下一個線程下載某個資源需要 1M/S,當開 10 個線程時速度並不會乘 10 倍,反而還會增加時間,畢竟上下文切換比較耗時。如果是受限於資源的話可以采用集群來處理任務,不同的機器來處理不同的數據,就類似於開始提到的無鎖編程

11. synchronized 關鍵字原理

1)synchronized 關鍵字是解決並發問題常用解決方案,有以下三種使用方式:

  • 同步普通方法,鎖的是當前對象。

  • 同步靜態方法,鎖的是當前 Class 對象。

  • 同步塊,鎖的是 () 中的對象。

實現原理: JVM 是通過進入、退出對象監視器( Monitor )來實現對方法、同步塊的同步的

2)鎖優化

synchronized 很多都稱之為重量鎖,JDK1.6 中對 synchronized 進行了各種優化,為了能減少獲取和釋放鎖帶來的消耗引入了偏向鎖和輕量鎖。

輕量鎖

當代碼進入同步塊時,如果同步對象為無鎖狀態時,當前線程會在棧幀中創建一個鎖記錄(Lock Record)區域,同時將鎖對象的對象頭中 Mark Word 拷貝到鎖記錄中,再嘗試使用 CAS 將 Mark Word 更新為指向鎖記錄的指針。

如果更新成功,當前線程就獲得了鎖。

如果更新失敗 JVM 會先檢查鎖對象的 Mark Word 是否指向當前線程的鎖記錄。

如果是則說明當前線程擁有鎖對象的鎖,可以直接進入同步塊。

不是則說明有其他線程搶占了鎖,如果存在多個線程同時競爭一把鎖,輕量鎖就會膨脹為重量鎖。

解鎖

輕量鎖的解鎖過程也是利用 CAS 來實現的,會嘗試鎖記錄替換回鎖對象的 Mark Word 。如果替換成功則說明整個同步操作完成,失敗則說明有其他線程嘗試獲取鎖,這時就會喚醒被掛起的線程(此時已經膨脹為重量鎖)

輕量鎖能提升性能的原因是:

認為大多數鎖在整個同步周期都不存在競爭,所以使用 CAS 比使用互斥開銷更少。但如果鎖競爭激烈,輕量鎖就不但有互斥的開銷,還有 CAS 的開銷,甚至比重量鎖更慢。

偏向鎖

為了進一步的降低獲取鎖的代價,JDK1.6 之后還引入了偏向鎖。

偏向鎖的特征是:鎖不存在多線程競爭,並且應由一個線程多次獲得鎖。

當線程訪問同步塊時,會使用 CAS 將線程 ID 更新到鎖對象的 Mark Word 中,如果更新成功則獲得偏向鎖,並且之后每次進入這個對象鎖相關的同步塊時都不需要再次獲取鎖了。

釋放鎖

當有另外一個線程獲取這個鎖時,持有偏向鎖的線程就會釋放鎖,釋放時會等待全局安全點(這一時刻沒有字節碼運行),接着會暫停擁有偏向鎖的線程,根據鎖對象目前是否被鎖來判定將對象頭中的 Mark Word 設置為無鎖或者是輕量鎖狀態。

偏向鎖可以提高帶有同步卻沒有競爭的程序性能,但如果程序中大多數鎖都存在競爭時,那偏向鎖就起不到太大作用。可以使用 -XX:-UseBiasedLocking 來關閉偏向鎖,並默認進入輕量鎖。

|

12.ReentrantLock 和Synchronized的區別

1、ReentrantLock 擁有Synchronized相同的並發性和內存語義,此外還多了 鎖投票,定時鎖等候和中斷鎖等候
線程A和B都要獲取對象O的鎖定,假設A獲取了對象O鎖,B將等待A釋放對O的鎖定,
如果使用 synchronized ,如果A不釋放,B將一直等下去,不能被中斷
如果 使用ReentrantLock,如果A不釋放,可以使B在等待了足夠長的時間以后,中斷等待,而干別的事情
ReentrantLock獲取鎖定與三種方式:
a) lock(), 如果獲取了鎖立即返回,如果別的線程持有鎖,當前線程則一直處於休眠狀態,直到獲取鎖
b) tryLock(), 如果獲取了鎖立即返回true,如果別的線程正持有鎖,立即返回false;
c)tryLock(long timeout,TimeUnit unit), 如果獲取了鎖定立即返回true,如果別的線程正持有鎖,會等待參數給定的時間,在等待的過程中,如果獲取了鎖定,就返回true,如果等待超時,返回false;
d) lockInterruptibly:如果獲取了鎖定立即返回,如果沒有獲取鎖定,當前線程處於休眠狀態,直到或者鎖定,或者當前線程被別的線程中斷

2、synchronized是在JVM層面上實現的,不但可以通過一些監控工具監控synchronized的鎖定,而且在代碼執行時出現異常,JVM會自動釋放鎖定,但是使用Lock則不行,lock是通過代碼實現的,要保證鎖定一定會被釋放,就必須將unLock()放到finally{}中

3、在資源競爭不是很激烈的情況下,Synchronized的性能要優於ReetrantLock,但是在資源競爭很激烈的情況下,Synchronized的性能會下降幾十倍,但是ReetrantLock的性能能維持常態;

13.Java 多線程三大核心

1)原子性
Java 的原子性就和數據庫事務的原子性差不多,一個操作中要么全部執行成功或者失敗。
2)可見性
現代計算機中,由於 CPU 直接從主內存中讀取數據的效率不高,所以都會對應的 CPU 高速緩存,先將主內存中的數據讀取到緩存中,線程修改數據之后首先更新到緩存,之后才會更新到主內存。如果此時還沒有將數據更新到主內存其他的線程此時來讀取就是修改之前的數據。

image.jpeg
image.jpeg

如上圖所示。

volatile 關鍵字就是用於保證內存可見性,當線程A更新了 volatile 修飾的變量時,它會立即刷新到主線程,並且將其余緩存中該變量的值清空,導致其余線程只能去主內存讀取最新值。

使用 volatile 關鍵詞修飾的變量每次讀取都會得到最新的數據,不管哪個線程對這個變量的修改都會立即刷新到主內存。

synchronized和加鎖也能能保證可見性,實現原理就是在釋放鎖之前其余線程是訪問不到這個共享變量的。但是和 volatile 相比開銷較大。

3)順序性

int a = 100 ; //1int b = 200 ; //2int c = a + b ; //3

正常情況下的執行順序應該是 1>>2>>3。但是有時 JVM 為了提高整體的效率會進行指令重排導致執行的順序可能是 2>>1>>3。但是 JVM 也不能是什么都進行重排,是在保證最終結果和代碼順序執行結果一致的情況下才可能進行重排。

重排在單線程中不會出現問題,但在多線程中會出現數據不一致的問題。

Java 中可以使用 volatile 來保證順序性,synchronized 和 lock 也可以來保證有序性,和保證原子性的方式一樣,通過同一段時間只能一個線程訪問來實現的。
除了通過 volatile 關鍵字顯式的保證順序之外, JVM 還通過 happen-before 原則來隱式的保證順序性。
其中有一條就是適用於 volatile 關鍵字的,針對於 volatile 關鍵字的寫操作肯定是在讀操作之前,也就是說讀取的值肯定是最新的。

總結

volatile 關鍵字只能保證可見性,順序性,不能保證原子性。

14 .volatile關鍵字在Java中有什么作用

  volatile的特殊規則保證了新值能立即同步到主內存,以及每次使用前立即從主內存刷新,即保證了內存的可見性,除此之外還能 禁止指令重排序。此外,synchronized關鍵字也可以保證內存可見性。
  指令重排序問題在並發環境下會導致線程安全問題,volatile關鍵字通過禁止指令重排序來避免這一問題。而對於Synchronized關鍵字,其所控制范圍內的程序在執行時獨占的,指令重排序問題不會對其產生任何影響,因此無論如何,其都可以保證最終的正確性。

15. volatile關鍵字和synchronized關鍵字的區別

(1)、volatile只能作用於變量,使用范圍較小。synchronized可以用在變量、方法、類、同步代碼塊等,使用范圍比較廣。
(2)、volatile只能保證可見性和有序性,不能保證原子性。而可見性、有序性、子性synchronized都可以包證。
(3)、volatile不會造成線程阻塞。synchronized可能會造成線程阻塞。

16. 對於鎖的一些認知

同一進程

重入鎖

使用 ReentrantLock 獲取鎖的時候會判斷當前線程是否為獲取鎖的線程,如果是則將同步的狀態 +1 ,釋放鎖的時候則將狀態 -1。只有將同步狀態的次數置為 0 的時候才會最終釋放鎖。

讀寫鎖

使用 ReentrantReadWriteLock ,同時維護一對鎖:讀鎖和寫鎖。當寫線程訪問時則其他所有鎖都將阻塞,讀線程訪問時則不會。通過讀寫鎖的分離可以很大程度的提高並發量和吞吐量。

不同進程

分布式鎖:

基於數據庫

可以創建一張表,將其中的某個字段設置為唯一索引,當多個請求過來的時候只有新建記錄成功的請求才算獲取到鎖,當使用完畢刪除這條記錄的時候即釋放鎖。

存在的問題:

數據庫單點問題,掛了怎么辦?
不是重入鎖,同一進程無法在釋放鎖之前再次獲得鎖,因為數據庫中已經存在了一條記錄了。
鎖是非阻塞的,一旦 insert 失敗則會立即返回,並不會進入阻塞隊列只能下一次再次獲取。
鎖沒有失效時間,如果那個進程解鎖失敗那就沒有請求可以再次獲取鎖了。
解決方案:
數據庫切換為主從,不存在單點。
在表中加入一個同步狀態字段,每次獲取鎖的是加 1 ,釋放鎖的時候-1,當狀態為 0 的時候就刪除這條記錄,即釋放鎖。
非阻塞的情況可以用 while 循環來實現,循環的時候記錄時間,達到 X 秒記為超時,break。
可以開啟一個定時任務每隔一段時間掃描找出多少 X 秒都沒有被刪除的記錄,主動刪除這條記錄。
基於 Redis
使用 setNX(key) setEX(timeout) 命令,只有在該 key 不存在的時候創建這個 key,就相當於獲取了鎖。由於有超時時間,所以過了規定時間會自動刪除,這樣也可以避免死鎖。

17. 線程池

簡單來說使用線程池有以下幾個目的:

  • 線程是稀缺資源,不能頻繁的創建。

  • 解耦作用;線程的創建於執行完全分開,方便維護。

  • 應當將其放入一個池子中,可以給其他任務進行復用。

18. 線程池原理

談到線程池就會想到池化技術,其中最核心的思想就是把寶貴的資源放到一個池子中;每次使用都從里面獲取,用完之后又放回池子供其他人使用,有點吃大鍋飯的意思。

那在 Java 中又是如何實現的呢?

在 JDK 1.5 之后推出了相關的 api,常見的創建線程池方式有以下幾種:

  • Executors.newCachedThreadPool():無限線程池。

  • Executors.newFixedThreadPool(nThreads):創建固定大小的線程池。

  • Executors.newSingleThreadExecutor():創建單個線程的線程池。

這幾個核心參數的作用:

  • corePoolSize 為線程池的基本大小。

  • maximumPoolSize 為線程池最大線程大小。

  • keepAliveTime 和 unit 則是線程空閑后的存活時間。

  • workQueue 用於存放任務的阻塞隊列。

  • handler 當隊列和最大線程池都滿了之后的飽和策略。

了解了這幾個參數再來看看實際的運用。

這里借助《聊聊並發》的一張圖來描述這個流程:

image.jpeg
image.jpeg

線程池的飽和策略

  當阻塞隊列滿了,且沒有空閑的工作線程,如果繼續提交任務,必須采取一種策略處理該任務,線程池提供了4種策略:

AbortPolicy:直接拋出異常,默認策略;

CallerRunsPolicy:用調用者所在的線程來執行任務;

DiscardOldestPolicy:丟棄阻塞隊列中最老的任務,並執行當前任務;

DiscardPolicy:直接丟棄任務;

當然也可以根據應用場景實現RejectedExecutionHandler接口,自定義飽和策略,如記錄日志或持久化存儲不能處理的任務。

線程池調優

設置最大線程數,防止線程資源耗盡;

使用有界隊列,從而增加系統的穩定性和預警能力(飽和策略);

根據任務的性質設置線程池大小:CPU密集型任務(CPU個數個線程),IO密集型任務(CPU個數兩倍的線程),混合型任務(拆分)。

如何配置線程池

流程聊完了再來看看上文提到了幾個核心參數應該如何配置呢?

有一點是肯定的,線程池肯定是不是越大越好。

通常我們是需要根據這批任務執行的性質來確定的。

  • IO 密集型任務:由於線程並不是一直在運行,所以可以盡可能的多配置線程,比如 CPU 個數 * 2

  • CPU 密集型任務(大量復雜的運算)應當分配較少的線程,比如 CPU 個數相當的大小。

當然這些都是經驗值,最好的方式還是根據實際情況測試得出最佳配置。

19. 優雅的關閉線程池

有運行任務自然也有關閉任務,從上文提到的 5 個狀態就能看出如何來關閉線程池。

其實無非就是兩個方法 shutdown()/shutdownNow()。

但他們有着重要的區別:

  • shutdown() 執行后停止接受新任務,會把隊列的任務執行完畢。

  • shutdownNow() 也是停止接受新任務,但會中斷所有的任務,將線程池狀態變為 stop。

兩個方法都會中斷線程,用戶可自行判斷是否需要響應中斷。

shutdownNow() 要更簡單粗暴,可以根據實際場景選擇不同的方法。

20. springBoot使用線程池

2018 年了,SpringBoot 盛行;來看看在 SpringBoot 中應當怎么配置和使用線程池。

其實也挺簡單,就是創建了一個線程池的 bean,在使用時直接從 Spring 中取出即可。

監控線程池

談到了 SpringBoot,也可利用它 actuator 組件來做線程池的監控。

線程怎么說都是稀缺資源,對線程池的監控可以知道自己任務執行的狀況、效率等。

線程池的隔離

線程池看似很美好,但也會帶來一些問題。

如果我們很多業務都依賴於同一個線程池,當其中一個業務因為各種不可控的原因消耗了所有的線程,導致線程池全部占滿。

這樣其他的業務也就不能正常運轉了,這對系統的打擊是巨大的。

比如我們 Tomcat 接受請求的線程池,假設其中一些響應特別慢,線程資源得不到回收釋放;線程池慢慢被占滿,最壞的情況就是整個應用都不能提供服務。

所以我們需要將線程池進行隔離。

通常的做法是按照業務進行划分:

比如下單的任務用一個線程池,獲取數據的任務用另一個線程池。這樣即使其中一個出現問題把線程池耗盡,那也不會影響其他的任務運行。

hystrix隔離

這樣的需求 Hystrix 已經幫我們實現了。

Hystrix 是一款開源的容錯插件,具有依賴隔離、系統容錯降級等功能。

下面來看看 Hystrix 簡單的應用:

首先需要定義兩個線程池,分別用於執行訂單、處理用戶。

可以看到兩個任務分成了兩個線程池運行,他們之間互不干擾。

獲取任務任務結果支持同步阻塞和異步非阻塞方式,可自行選擇。

它的實現原理其實容易猜到:

利用一個 Map 來存放不同業務對應的線程池。

21. 深入理解線程通信

前言

開發中不免會遇到需要所有子線程執行完畢通知主線程處理某些邏輯的場景。

或者是線程 A 在執行到某個條件通知線程 B 執行某個操作。

可以通過以下幾種方式實現:

等待通知機制

等待通知模式是 Java 中比較經典的線程通信方式。

兩個線程通過對同一對象調用等待 wait() 和通知 notify() 方法來進行通訊。

有一些需要注意:

  • wait() 、notify()、notifyAll() 調用的前提都是獲得了對象的鎖(也可稱為對象監視器)。

  • 調用 wait() 方法后線程會釋放鎖,進入 WAITING 狀態,該線程也會被移動到等待隊列中。

  • 調用 notify() 方法會將等待隊列中的線程移動到同步隊列中,線程狀態也會更新為 BLOCKED

  • 從 wait() 方法返回的前提是調用 notify() 方法的線程釋放鎖,wait() 方法的線程獲得鎖。

join()方法

22. volatitle共享內存

CounDownLatch並發工具

CountDownLatch 可以實現 join 相同的功能,但是更加的靈活。

CountDownLatch 也是基於 AQS(AbstractQueuedSynchronizer) 實現的,

  • 初始化一個 CountDownLatch 時告訴並發的線程,然后在每個線程處理完畢之后調用 countDown() 方法。

  • 該方法會將 AQS 內置的一個 state 狀態 -1 。

  • 最終在主線程調用 await() 方法,它會阻塞直到 state == 0 的時候返回。

CyclicBarrier 並發工具
CyclicBarrier 中文名叫做屏障或者是柵欄,也可以用於線程間通信。

它可以等待 N 個線程都達到某個狀態后繼續運行的效果。

  1. 首先初始化線程參與者。

  2. 調用 await() 將會在所有參與者線程都調用之前等待。

  3. 直到所有參與者都調用了 await() 后,所有線程從 await() 返回繼續后續邏輯。

可以看出由於其中一個線程休眠了五秒,所有其余所有的線程都得等待這個線程調用 await() 。

該工具可以實現 CountDownLatch 同樣的功能,但是要更加靈活。甚至可以調用 reset() 方法重置 CyclicBarrier (需要自行捕獲 BrokenBarrierException 處理) 然后重新執行。

線程響應中斷
可以采用中斷線程的方式來通信,調用了 thread.interrupt() 方法其實就是將 thread 中的一個標志屬性置為了 true。

並不是說調用了該方法就可以中斷線程,如果不對這個標志進行響應其實是沒有什么作用(這里對這個標志進行了判斷)。

但是如果拋出了 InterruptedException 異常,該標志就會被 JVM 重置為 false。

線程池 awaitTermination() 方法

使用這個 awaitTermination() 方法的前提需要關閉線程池,如調用了 shutdown() 方法。

調用了 shutdown() 之后線程池會停止接受新任務,並且會平滑的關閉線程池中現有的任務。

管道通信

Java 雖說是基於內存通信的,但也可以使用管道通信。

需要注意的是,輸入流和輸出流需要首先建立連接。這樣線程 B 就可以收到線程 A 發出的消息了。

實際開發中可以靈活根據需求選擇最適合的線程通信方式。

23. CAS(無鎖算法)

CAS(Compare And Swap) 無鎖算法: CAS是樂觀鎖技術,當多個線程嘗試使用CAS同時更新同一個變量時,只有其中一個線程能更新變量的值,而其它線程都失敗,失敗的線程並不會被掛起,而是被告知這次競爭中失敗,並可以再次嘗試。CAS有3個操作數,內存值V,舊的預期值A,要修改的新值B。當且僅當預期值A和內存值V相同時,將內存值V修改為B,否則什么都不做

CAS : CAS自旋volatile變量,是一種很經典的用法。
  CAS,Compare and Swap即比較並交換,設計並發算法時常用到的一種技術。CAS有3個操作數,內存值V,舊的預期值A,新值B。當且僅當預期值A和內存值V相同時,將內存值V修改為B,否則什么都不做。CAS是通過unsafe類的compareAndSwap (JNI, Java Native Interface) 方法實現的,該方法包括四個參數:第一個參數是要修改的對象,第二個參數是對象中要修改變量的偏移量,第三個參數是修改之前的值,第四個參數是預想修改后的值。

  CAS雖然很高效的解決原子操作,但是CAS仍然存在三大問題:ABA問題、循環時間長開銷大和只能保證一個共享變量的原子操作。

ABA問題:因為CAS需要在操作值的時候檢查下值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那么使用CAS進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了。ABA問題的解決思路就是使用版本號。在變量前面追加上版本號,每次變量更新的時候把版本號加一,那么A-B-A 就會變成1A-2B-3A。

不適用於競爭激烈的情形中:並發越高,失敗的次數會越多,CAS如果長時間不成功,會極大的增加CPU的開銷。因此CAS不適合競爭十分頻繁的場景。

只能保證一個共享變量的原子操作:當對一個共享變量執行操作時,我們可以使用循環CAS的方式來保證原子操作,但是對多個共享變量操作時,循環CAS就無法保證操作的原子性,這個時候就可以用鎖,或者有一個取巧的辦法,就是把多個共享變量合並成一個共享變量來操作。比如有兩個共享變量i=2,j=a,合並一下ij=2a,然后用CAS來操作ij。從Java1.5開始JDK提供了AtomicReference類來保證引用對象之間的原子性,因此可以把多個變量放在一個對象里來進行CAS操作。

AQS : 隊列同步器

  隊列同步器(AbstractQueuedSynchronizer)是用來構建鎖和其他同步組件的基礎框架,技術是 CAS自旋Volatile變量:它使用了一個Volatile成員變量表示同步狀態,通過CAS修改該變量的值,修改成功的線程表示獲取到該鎖;若沒有修改成功,或者發現狀態state已經是加鎖狀態,則通過一個Waiter對象封裝線程,添加到等待隊列中,並掛起等待被喚醒。

  同步器是實現鎖的關鍵,子類通過繼承同步器並實現它的抽象方法來管理同步狀態,利用同步器實現鎖的語義。特別地,鎖是面向鎖使用者的,它定義了使用者與鎖交互的接口,隱藏了實現細節;同步器面向的是鎖的實現者,它簡化了鎖的實現方式,屏蔽了同步狀態管理、線程排隊、等待與喚醒等底層操作。鎖和同步器很好地隔離了鎖的使用者與鎖的實現者所需關注的領域。

  一般來說,自定義同步器要么是獨占方式,要么是共享方式,他們也只需實現tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一種即可。但AQS也支持自定義同步器同時實現獨占和共享兩種方式,如ReentrantReadWriteLock。

  同步器的設計是基於 模板方法模式 的,也就是說,使用者需要繼承同步器並重寫指定的方法,隨后將同步器組合在自定義同步組件的實現中,並調用同步器提供的模板方法,而這些模板方法將會調用使用者重寫的方法。

  AQS維護了一個volatile int state(代表共享資源)和一個FIFO線程等待隊列(多線程爭用資源被阻塞時會進入此隊列)。這里volatile是核心關鍵詞,具體volatile的語義,在此不述。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是能回到零態的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM