JUC
一. 概述
- JUC指的是JDK1.5中提供的一套並發包及其子包:
- java.util.concurrent
- java.util.concurrent.lock
- java.util.cncurrent.atomic
- 主要內容有:阻塞式隊列、並發映射、鎖、執行器服務、原子性操作。
二. 原子性操作
原子性操作實際上是保證了屬性的原子性,底層是基於CAS+volatile來實現的
Ⅰ. 關於CAS
👉CAS
Ⅱ.關於volatile
volatile是java中的關鍵字之一,是Java中提供的用於保證線程通信間的輕量級通信機制。
- 特性:
- 保證線程的可見性。一個線程對主內存的數據做了改變,其他線程能夠立即感知到這個改變。
- 對單個讀/寫具有原子性,但是復合操作除外,例如i++不保證線程的原子性。原子性指線程的執行過程不可拆分,換言之,線程在執行過程中不會中斷。加鎖就是為了保證原子性。
- 內存語義:
- 當寫一個volatile變量時,JMM會把該線程對應的本地內存中的共享變量值立即刷新到主內存中。
- 當讀一個volatile變量時,JMM會把該線程對應的本地內存設置為無效,直接從主內存中讀取共享變量
- 實施機制
- 禁止指令重排。指令沒有按照預定順序調用執行,而是在底層產生了所謂的優化,導致順序發生了改變。指令重排不能違背happen-before原則。
- 內存屏障
三. LOCK鎖
Ⅰ. 鎖一些概念
-
鎖的公平和非公平原則:
公平鎖:鎖的獲取順序應該符合請求的絕對時間順序,也就是FIFO。
非公平鎖:只要CAS設置同步狀態成功,則表示當前線程獲取了鎖
- 在資源有限的情況下,線程之間實際執行的次數並不均等,這種現象稱之為非公平原則。在公平策略下,線程不能直接搶占資源,而是搶占入隊順序。此時線程之間實際執行次數大致相等,我們稱之為公平策略。
- 相對而言,非公平的效率更高(不需要考慮調度問題)
-
鎖的獨占和共享
獨占鎖:獨占鎖也叫排他鎖,是指該鎖一次只能被一個線程所持有。如果線程T對數據A加上排他鎖后,則其他線程不能再對A加任何類型的鎖。獲得排它鎖的線程即能讀數據又能修改數據。ReentrantLock 和 synchronized 都是獨占鎖
共享鎖:享鎖是指該鎖可被多個線程所持有。如果線程T對數據A加上共享鎖后,則其他線程只能對A再加共享鎖,不能加排它鎖。獲得共享鎖的線程只能讀數據,不能修改數據。
獨享鎖與共享鎖都是通過AQS來實現的,通過實現不同的方法,來實現獨享或者共享ReentrantReadWriteLock中讀鎖是共享鎖,寫鎖是獨占鎖。讀鎖的共享可以保證並發讀是高效的,讀寫,寫讀,寫寫是互斥的。
-
鎖的重入和非重入
可重入鎖:可重入鎖也叫做遞歸鎖,指的是同一個線程T在進入外層函數A獲得鎖L之后,T繼續進入內層遞歸函數B,也需要獲取該鎖L的代碼時,在不釋放鎖L的情況下,可以重復獲取該鎖L。
非重入鎖:非可重入鎖也叫做自旋鎖,對比上面,指的是同一個線程T在進入外層函數A獲得鎖L之后,T繼續進入內層遞歸函數B時,仍然有獲取該鎖L的代碼,必須要先釋放進入函數A的鎖L,才可以獲取進入函數B的鎖L。
-
鎖的樂觀和悲觀
樂觀鎖:就像它的名字一樣,對於並發間操作產生的線程安全問題持樂觀狀態,樂觀鎖認為競爭不總是會發生,因此它不需要持有鎖,將樂觀鎖的核心算法是CAS,比較-替換這兩個動作作為一個原子操作嘗試去修改內存中的變量,如果失敗則表示發生沖突,那么就應該有相應的重試邏輯。(不加鎖就修改)
悲觀鎖:還是像它的名字一樣,對於並發間操作產生的線程安全問題持悲觀狀態,悲觀鎖認為競爭總是會發生,因此每次對某資源進行操作時,都會持有一個獨占的鎖,就像synchronized,不管三七二十一,直接上了鎖就操作資源了。(加鎖才修改)
-
讀寫鎖
讀鎖:當線程獲取讀鎖時,允許其他線程的讀操作,不允許寫操作。
寫鎖:當線程獲取讀寫時,不允許其他線程的任何操作。
-
自旋
很多synchronized里面的代碼只是一些很簡單的代碼,執行時間非常快,此時等待的線程都加鎖可能是一種不太值得的操作,因為線程阻塞涉及到用戶態和內核態切換的問題。既然synchronized里面的代碼執行得非常快,不妨讓等待鎖的線程不要被阻塞,而是在synchronized的邊界做忙循環,這就是自旋。如果做了多次忙循環發現還沒有獲得鎖,再阻塞,這樣可能是一種更好的策略。
Ⅱ. ReentrantLock
JDK1.5增加了LOCK鎖,可以通過顯示定義同步鎖對像實現同步,是對共享資源進行訪問的工具。相比synchronized,LOCK更加精細靈活。唯一實現類:ReentrantLock
【特點】
- 可重入。(Synchronized同)
- 如果不指定,默認非公平。(Synchronized同)
- 獨占(Synchronized同)
- 悲觀(Synchronized同)
- 底層采用AQS實現。
【案例】
import java.util.concurrent.locks.ReentrantLock;
/**
* 銀行賬戶類
* 此類為可變類,亦是線程不安全類。
* 若想變為線程安全類,需付出額外的方法
*
* 此例中,需要將修改balance的方法同步
* 若使用Synchronized同步,則鎖是this
*
* 此例也可顯示定義鎖對象,來同步方法
* 注意要顯示的釋放鎖
*/
public class Account{
private String accountNo;
private double balance;
//定義鎖對象
private final ReentrantLock lock=new ReentrantLock();
public void draw(double drawAmount){
//加鎖
lock.lock();
try{
if(drawAmount<balance){
System.out.println("目前余額:"+balance);
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
balance-=drawAmount;
System.out.println("余額:"+balance);
}
else {
System.out.println("余額不足,提款失敗");
}
}finally {
//修改完成,釋放鎖
lock.unlock();
}
}
}
Ⅲ. ReadWriteLock
ReadWriteLock:讀寫鎖。在使用的時候先創建ReentrantReadWriteLock,通過這個對象獲取讀鎖或者寫鎖,之后再加鎖解鎖或者解鎖。
相比ReadWriteLock,ReentrantLock某些時候有局限。如果使用ReentrantLock,可能本身是為了防止線程A在寫數據、線程B在讀數據造成的數據不一致,但這樣,如果線程C在讀數據、線程D也在讀數據,讀數據是不會改變數據的,沒有必要加鎖,但是還是加鎖了,降低了程序的性能。
因為這個,才誕生了讀寫鎖ReadWriteLock。ReadWriteLock是一個讀寫鎖接口,ReentrantReadWriteLock是ReadWriteLock接口的一個具體實現,實現了讀寫的分離,讀鎖是共享的,寫鎖是獨占的,讀和讀之間不會互斥,讀和寫、寫和讀、寫和寫之間才會互斥,提升了讀寫的性能。
【關於StampedLock】
StampedLock是Java8新增的鎖,在絕大多數場景下可以替代傳統的讀寫鎖。其在提供讀寫鎖的同時,還支持優化讀模式。優化讀基於假設:大多數情況下讀操作並不會和寫操作沖突,所以可以先試着修改,然后通過validate方法確認是否進入了寫模式,如果沒有進入,就成功避免了開銷;如果進入了,則嘗試獲取讀鎖。
Ⅳ.Condition
ConditionObject
是同步器AbstractQueuedSynchronizer
的內部類 ,因為Condition的操作需要獲取相關聯的鎖,所以作為同步器的內部類也會是比較合理的。
每個Condition對象都包含着一個隊列(等待隊列),是Condition對象實現等待/通知功能的關鍵。
-
等待隊列是一個FIFO隊列,隊列的每個節點都包含一個線程引用, 線程就是在Condition對象中等待的線程,如果一個線程調用了
Condition.await()
方法,那么該線程將會釋放鎖、構造節點加入等待隊列進入等待狀態。事實上,節點的定義復用了同步器中節點的定義,也就是說,同步隊列和等待隊列中節點類型都是同步器的經靜態內部類AbstractQueuedSynchronizer.Node
一個Condition包含一個等待隊列,Condition擁有首節點(fristWaiter)和尾節點(lastWriter)。當前線程調用
Condition.await()
方法,將會以當前線程構造節點,並將節點從尾部加入等待隊列。 -
調用Condition的
await()
方法,會使當前線程進入等待隊列並釋放鎖,同時線程狀態變為等待狀態,當從await()
方法返回時,當前線程一定獲取了Condition相關聯的鎖,如果從隊列 (同步隊列和等待隊列)的角度看
await()
方法,當調用await()方法時,相當於同步隊列的首節點(獲取了鎖的節點)移動到Condition的等待隊列中。 -
調用Condition的
signal()
方法,將喚醒在等待隊列中等待時間最長的節點(首節點),在喚醒節點之前,會將節點移動到同步隊列中。
【案例】👉線程通信
Ⅴ. synchronized 和 ReentrantLock的區別
- synchronized是和if、else、for、while一樣的關鍵字,ReentrantLock是類,這是二者的本質區別。
- synchronized不需要顯示的定義鎖和釋放鎖。
- 既然ReentrantLock是類,那么它就提供了比synchronized更多更靈活的特性,可以被繼承、可以有方法、可以有各種各樣的類變量,ReentrantLock比synchronized的擴展性體現在幾點上:
- ReentrantLock可以對獲取鎖的等待時間進行超時設置,這樣就避免了死鎖。
- 等待可中斷:當持有鎖的線程長期不釋放鎖的時候,正在等待的線程可以選擇放棄等待,對處理執行時間非常長的同步塊很有用。
- 可以實現公平策略
- ReentrantLock可以獲取各種鎖的信息。
- ReentrantLock可以靈活地實現多路通知
四. BlockingQueue - 阻塞式隊列
Ⅰ. 特點
-
滿足隊列特點:FIFO(First In First Out)
-
阻塞:如果隊列為空,則試圖獲取元素的線程會被阻塞;如果隊列已滿,則試圖放入元素的線程會被阻塞。
-
不允許元素為null(LinkedList允許)
-
重要方法
拋出異常 返回特殊值 永久阻塞 定時阻塞 添加 add - IllegalStateException offer - false put offer 獲取 remove - NoSuchElementException poll - null take poll
Ⅱ. 常用的實現類
- ArrayBlockingQueue阻塞式順序隊列
- 底層基於數組存儲數據
- 使用的時候需要指定容量,不能擴容
- 在多線程環境下不保證“公平性”
- 實現:ReentrantLock+Condition
- LinkedBlockingQueue阻塞式鎖式隊列
- 底層基於節點來存儲數據
- 在使用的時候可以指定容量也可以不指定。如果指定容量,則容量不可變;如果不指定容量,則容量默認為Integer.MAX_VALUE = 231-1不可變。因為實際開發中,一般不會在隊列中存儲21億個元素,所以一般認為此時的容量是無限的
- PriorityBlockingQueue具有優先級的阻塞式隊列:
- 底層基於節點來存儲數據
- 使用的時候可以指定容量也可以不指定。如果不指定則默認初始容量是11
- PriorityBlockingQueue會對放入的元素來進行排序,默認情況下元素采用自然順序升序排序,要求元素對應的類實現Comparable接口,覆蓋compareTo方法指定比較規則。
- SynchronousQueue 同步隊列
- 在使用的時候不需要指定容量,默認容量為1且只能為1
- 應用:交換工作,生產者的線程和消費者的線程同步以傳遞某些信息、事件或者任務
另:BlockingDeque阻塞式雙端隊列
- 允許從兩端放入/獲取元素。
- 遵循阻塞特點,在使用的時候需要指定容量。