多線程筆記(二) 1. Synchronized 和 Lock 的區別 synchronized是Java的關鍵字,是 JVM 層面的內置功能和實現。 Lock是一個介面,是代碼層面的實現 synchronized可以隱式的獲取,釋放鎖 lock是顯式的獲取,釋放鎖 synchronized在發生異 ...
多線程筆記(二)
1. Synchronized 和 Lock 的區別
-
synchronized是Java的關鍵字,是 JVM 層面的內置功能和實現。
Lock是一個介面,是代碼層面的實現
-
synchronized可以隱式的獲取,釋放鎖
lock是顯式的獲取,釋放鎖
-
synchronized在發生異常的時候會自動釋放鎖
lock在發生異常的時候,不會自動釋放鎖,必須要調用unlock方法才會釋放鎖,否則容易引起死鎖
-
Lock可以嘗試非阻塞獲取鎖,可中斷獲取鎖,超時獲取鎖。
synchronized並沒有這些功能
2. LockSupport
LockSupport是一個編程工具類,主要是為了阻塞線程(park)和喚醒線程(unpark)時使用
設計原理的核心:許可
park:掛起當前線程,等待一個許可
unpark:為某個線程提供一個許可,喚醒某個指定的線程
park/unpark和wait/notify很類似,但其具有以下的優點
- park/unpark是以thread為操作對象,語義更加直觀
- 操作更為精準和靈活,可以準確的去喚醒某一個線程
park/unpark和wait/notify的區別
wait/notify和synchronized聯繫在一起的,wait過後,線程是進入Blocked狀態
park方法使當前線程掛起,進入到waiting狀態
3. CAS
CAS(Compare And Swap, 比較並替換)中有3個基本的操作數:V:記憶體地址的值; A:舊的預期的值;B:要修改的新的值
基本實現方式:
使用CAS去更新一個變數的時候,只有變數的舊的預期的值A 和記憶體地址的值V 相同的時候,才會將V 修改為新的值B。如果修改失敗,會自旋等待,直到修改成功。
CAS實現的基石:Unsafe類
CAS想要保證操作時線程安全的,一個實現的關鍵在於如何保證 比較並替換 是一個原子操作
在Java中,用Unsafe類來實現CAS的原子操作,Unsafe類 ==> JNI(Java本地介面) ==>本地實現的C++庫 ==>操作記憶體空間
CAS在Java中的應用和缺點
應用:
-
Atomic包, Lock包下系列的類
-
在JDK1.6以後,sychronized升級為重量級鎖之前也採用的CAS機制
缺點
-
CAS採用自旋的方式,會浪費CPU的資源
-
不能保證代碼塊的原子性,保證的是對一個變數的 比較和替換 的操作是原子的
-
ABA問題:CAS操作記憶體值,由A改成了B,但是又改回了A,從而導致後續本不應該成功的操作,最後成功執行
自己舉個慄子:由於網路延時,線程一線程二都想對記憶體值A操作,目的就是將記憶體值A改成B(只修改一次),按理說一個線程操作成功,那另一個線程就要操作失敗。線程一和線程二取到的舊的值都是A,假定線程一操作成功,將A改成了B。按理說接下來線程二拿到的記憶體的值是B,和取到的舊的值A比較,B不等於A,就會提交失敗,但是捏,好巧不巧,線上程二修改之前,線程三過來執行它自己的任務,將B改成了A。這個時候線程二拿到的記憶體值是A,之前取到的舊的值也是A,A等於A,線程二就會對A進行修改。這個時候就對記憶體值修改了兩次,而我們只想讓它修改一次,就出錯了。可以把記憶體值想成自己的工資,誰都不想自己的工資被莫名其妙的多改幾次把,改多了當我沒說,哈哈哈。
ABA的解決方案:給數據加上版本號,每次不僅要比較記憶體的值,還要比較版本號
4. AQS
AQS是什麼?
AQS(AbstractQueuedSynchronizer,抽象隊列同步器)是構建鎖和其他同步組件的基礎框架
AQS能幹什麼?
- 同步隊列的管理和維護
- 同步狀態的光臨
- 線程的阻塞,喚醒的管理
基本設計思路
- 把競爭的線程和等待狀態,封裝成為Node對象
- AQS把這些Node,放到一個同步隊列中去,這個同步隊列是一個FIFO(先進先出)的一個雙向隊列,是基於CLH(貢獻者名字縮寫首字母,不用糾結這個)隊列實現的
- AQS使用int類型的成員變數來表示同步狀態,比如:是否有線程獲取鎖,鎖的重入次數等,具體的含義由具體的子類來定義
- AQS使用LockSupport來實現對線程的喚醒和阻塞,線程的喚醒和阻塞便隨著同步隊列的維護。
AQS如何把基礎功能提供出去?
AQS使用模板方法模式,大概的意思是規定了整體的流程,自己可以具體實現子流程,整體的流程是不能變的。後續把設計模式學了再做補充
非阻塞的獲取獨占鎖的流程
自己畫的簡化版流程,沒有涉及到裡面的中斷
AQS中獲取和釋放獨占鎖和共用鎖區別
獨占鎖:正常情況下,只有持有鎖的線程運行結束了,釋放鎖了,該節點才會出隊。
共用鎖:當前節點喚醒了下一個節點並且將下一個節點設置尾Head之後,該節點出隊。
獨占鎖:只有在釋放鎖的時候,才會去看看要不要喚醒下一個節點。
共用鎖:在獲取鎖的過程中會在兩個地方看看要不要去喚醒下一個節點。一個是在獲取鎖的流程中調用setHeadAndPropagate()
方法的時候,一個是在釋放鎖的時候。
5. ReentrantLock
ReentrantLock是Lock介面的實現,主要實現了可重入的獨占鎖的功能,與synchronized關鍵字功能類型
ReentrantLock與synchronized對比
ReentrantLock功能更加強大和靈活
- 可非中斷的獲取鎖
- 可中斷式的獲取鎖
- 可超時獲取鎖
- 提供了公平鎖和非公平鎖
公平鎖和非公平鎖的卻別主要體現在獲取鎖的方式上
公平鎖:多個線程按照申請獲取鎖的先後順序來獲取鎖
非公平鎖:多個線程按照不是按照申請獲取鎖的先後順序來獲取鎖。比如搶占式獲取鎖。高併發的情況可能會造成饑餓現象
在ReentrantLock的源碼中,公平鎖主要是通過判斷當前的AQS隊列是否有節點來控制當前節點是否獲得鎖。隊列中如果有節點那麼tryAcquire()
方法直接返回false表示獲取鎖失敗,再將節點其排到隊列末尾。
ReentrantReadWriteLock
在實際的業務中,往往讀數據比寫數據更加頻繁,如果我們對讀數據使用共用鎖,對寫數據使用獨占鎖,那麼整個讀寫的性能就會提高。
讀鎖:用在讀取臨界資源的地方
寫鎖:用在更新臨界資源的地方
讀鎖和寫鎖的互斥規則:
- 一個線程讀,另一個線程讀:共用
- 一個線程讀,另一個線程寫:互斥
- 一個線程寫,另一個線程讀:互斥
- 一個線程寫,另一個線程寫:互斥
ReadWriteLock是一個介面,該介面中只有兩個方法,分別為Lock readLock();
和Lock writeLock();
ReentrantReadWriteLock:可重入式讀寫鎖,是讀寫鎖(ReadWriteLock)的實現類。
- 支持讀鎖和寫鎖
- 支持公平鎖和非公平鎖
- 支持可重入鎖
- 支持鎖降級(如果一個線程持有寫鎖,在不釋放寫鎖的情況下,它還可以繼續持有讀鎖,這種情況就是鎖降級)
讀寫鎖的狀態存儲機制
AQS里的state是一個int值。在讀寫鎖中,需要同時保存兩種鎖的狀態。其同樣使用int類型的變數表示state,總共32位,前16位表示讀鎖的同步狀態,後面16位表示寫鎖的同步狀態。獲取讀鎖狀態就將state無符號右移16位。獲取寫鎖狀態就將state與掩碼相與,保留後16位。
6. StampedLock類
ReentrantReadWriteLock中存在著一些問題,寫線程可能會出現“饑餓”問題;如果有線程在讀,那麼寫線程是無法獲取寫鎖的。
優點:
在Java8中引入了StampedLock,其對ReentrantReadWriteLock進行了增強,優化了讀鎖和寫鎖的訪問,使讀寫鎖之間可以相互轉換,因此可以更細粒度地控制併發。
缺點:
其設計初衷使作為一個內部工具類來使用,用於輔助開發其他的線程安全組件。用不好的花會產生死鎖,產生莫名其妙的問題。不支持可重入也是一個問題。
特點:
- 所有獲取鎖的方法,都會返回一個stamp
- 所有釋放鎖的方法,都需要一個stamp
- 是不可重入的
- 有三種訪問方式,分別為讀模式,寫模式,樂觀讀模式
- 支持讀鎖和寫鎖的相互轉換
- 不支持Condition
7. Condition
該介面對原生的wait, notify/notifyAll這些方法進行增強,從Java語言層面,實現類似的功能。
AQS是使用同步隊列來控制節點獲取鎖,在Condition中使用條件隊列來控制節點什麼時候await(),什麼時候signal()。與多個節點共用一個同步隊列不同的是,一個Conditon對象就對應一個條件隊列。
總體流程為,調用await()時,將節點加入等待隊列,然後將線程掛起,等待其他線程對其調用signal()方法。其他線程對其調用signal()方法後,將該節點從條件隊列中出隊,將其添加到同步隊列的末尾,然後將其喚醒。然後就走同步隊列的那一套流程。
8. ThreadLocal
ThreadLocal是用來存放線程自身相關數據的一個容器。提供線程本地變數,訪問這個變數的每個線程都會有這個變數的一個副本。線程操作數據的時候就會操作線程本地的數據,從而避免了線程安全性問題。
threadLocals其實是一個ThreadLocalMap類型的,在Thread類中的一個屬性,伴隨的線程的存在而存在。當我們設置ThreadLocal變數的時候,ThreadLocalMap中的key就是ThreadLocal,value就是ThreadLocal變數的值。
-
由於threadLocals是Thread的一個屬性,會跟著線程一直存在,為了避免記憶體溢出,在確定ThreadLocal數據以後不再使用後,要及時remove掉。
-
由於ThreadLocalMap使用ThreadLocal的弱引用作為key,如果一個ThreadLocal沒有外部關聯的強引用,在垃圾回收的時候,JVM會回收掉ThreadLocal,就會出現ThreadLocalMap中key為空,但是value值還在。造成記憶體泄漏,所以在確定ThreadLocal數據以後不再使用後,要及時remove掉。