併發編程是指在程式中使用多線程技術來實現並行處理的能力。多線程機制使得程式可以分解成互不幹擾的任務,從而提高了程式執行的效率。併發編程可以通過對線程的創建,管理和協作進行控制,以實現更加高效的併發執行。併發編程的優點包括:① 提高程式執行效率:通過多線程並行處理,程式的處理速度可以顯著提高。② 增強... ...
1. JUC前言知識
JUC即 java.util.concurrent
涉及三個包:
- java.util.concurrent
- java.util.concurrent.atomic
- java.util.concurrent.locks
普通的線程代碼:
- Thread
- Runnable 沒有返回值、效率相比入 Callable 相對較低!
- Callable 有返回值!【工作常用】
1.1 進程和線程
進程:是指一個記憶體中運行的程式,每個進程都有一個獨立的記憶體空間,一個應用程式可以同時運行多個進程。進程是資源分配的單位。
記憶:進程的英文為Process,Process也為過程,所以進程可以大概理解為程式執行的過程。
(進程也是程式的一次執行過程,是系統運行程式的基本單位; 系統運行一個程式即是一個進程從創建、運行到消亡的過程)
線程:進程中的一個執行單元,負責當前進程中程式的執行。一個進程中是可以有多個線程的。線程是CPU調度和執行的單位。
【java預設有兩個線程:main、GC】
舉例:打開word使用是一個進程,word會檢查你的拼寫,兩個線程:容災備份,語法檢查
進程與線程的區別:
- 進程:有獨立的記憶體空間,進程中的數據存放空間(堆空間和棧空間)是獨立的,至少有一個線程。
- 線程:堆空間是共用的,棧空間是獨立的,線程消耗的資源比進程小的多
1.2 併發與並行
並行 :指兩個或多個事件在同一時刻發生(同時發生)【多個CPU同時執行多個線程】
併發 :指兩個或多個事件在同一個時間段內發生。(交替執行) 【一個CPU交替執行線程】
拓展:
-
併發編程的本質是充分利用cpu資源
java代碼查詢cpu核數:
//查詢cpu核數 //CPU 密集型,IO密集型 System.out.println(Runtime.getRuntime().availableProcessors());
-
java真的可以開啟線程嗎?不能,通過源碼可知底層開啟線程的start()方法是native修飾的,意思是調用操作系統C++的代碼
//本地方法,底層的C++ java無法直接操作硬體 private native void start0();
1.3 線程六種狀態
- NEW(新建)
線程剛被創建,但是並未啟動。還沒調用start方法 - Runnable(可運行)
線程可以在java虛擬機中運行的狀態,可能正在運行自己代碼,也可能沒有,這取決於操 作系統處理器 - Blocked(鎖阻塞)
當一個線程試圖獲取一個對象鎖,而該對象鎖被其他的線程持有,則該線程進入Blocked狀 態;當該線程持有鎖時,該線程將變成Runnable狀態。 - Waiting(無限等待)
一個線程在等待另一個線程執行一個(喚醒)動作時,該線程進入Waiting狀態。
進入這個 狀態後是不能自動喚醒的,必須等待另一個線程調用notify或者notifyAll方法才能夠喚醒。 - Timed Waiting(計時等待)
同waiting狀態,有幾個方法有超時參數,調用他們將進入Timed Waiting狀態。
這一狀態 將一直保持到超時期滿或者接收到喚醒通知。帶有超時參數的常用方法有Thread.sleep 、 Object.wait - Teminated(被終止)
因為run方法正常退出而死亡,或者因為沒有捕獲的異常終止了run方法而死亡。
上源碼:
public enum State {
/**
* 新建
*/
NEW,
/**
* 運行
*/
RUNNABLE,
/**
* 阻塞
*/
BLOCKED,
/**
* 等待,死死的等
*/
WAITING,
/**
* 超時等待
*/
TIMED_WAITING,
/**
* 停止
*/
TERMINATED;
}
1.4 sleep與wait區別
只要是等待都需要拋出異常,中斷異常
-
來自不同的類
-
wait -> Object
-
sleep -> Thread
-
-
關於鎖的釋放
- wait會釋放鎖
- sleep睡覺了,抱著鎖睡覺,不會釋放!
-
使用的範圍是不同的
- wait必須在同步代碼塊中
- sleep可以在任何地方睡
1.5 解耦寫線程
學生寫法(多耦):
public class SaleTickerDemo01 {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(ticket, "A").start();
new Thread(ticket, "B").start();
new Thread(ticket, "C").start();
}
}
class Ticket implements Runnable{
private int number = 50;
public void run(){
if (number > 0) {
System.out.println(Thread.currentThread().getName() + "買了第" + (number--) + "張票");
}
}
}
工作寫法(解耦):
public class SaleTickerDemo01 {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(() -> {
for (int i = 0; i < 50; i++) {
ticket.sale();
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 50; i++) {
ticket.sale();
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 50; i++) {
ticket.sale();
}
}, "C").start();
}
}
class Ticket {
private int number = 50;
public synchronized void sale() {
if (number > 0) {
System.out.println(Thread.currentThread().getName() + "買了第" + (number--) + "張票");
}
}
}
1.6 鎖基礎
較難,能理解就理解
1.6.1 鎖機制
通過使用synchronized
關鍵字來實現鎖,這樣就能夠很好地解決線程之間爭搶資源的情況。那麼,synchronized
底層到是如何實現的呢?
我們知道,使用synchronized
,一定是和某個對象相關聯的,比如我們要對某一段代碼加鎖,那麼我們就需要提供一個對象來作為鎖本身:
public static void main(String[] args) {
synchronized (Main.class) {
//這裡使用的是Main類的Class對象作為鎖
}
}
我們來看看,它變成位元組碼之後會用到哪些指令:
其中最關鍵的就是monitorenter
指令了,可以看到之後也有monitorexit
與之進行匹配(註意這裡有2個),monitorenter
和monitorexit
分別對應加鎖和釋放鎖,在執行monitorenter
之前需要嘗試獲取鎖,每個對象都有一個monitor
監視器與之對應,而這裡正是去獲取對象監視器的所有權,一旦monitor
所有權被某個線程持有,那麼其他線程將無法獲得(管程模型的一種實現)。
在代碼執行完成之後,我們可以看到,一共有兩個monitorexit
在等著我們,那麼為什麼這裡會有兩個呢,按理說monitorenter
和monitorexit
不應該一一對應嗎,這裡為什麼要釋放鎖兩次呢?
首先我們來看第一個,這裡在釋放鎖之後,會馬上進入到一個goto指令,跳轉到15行,而我們的15行對應的指令就是方法的返回指令,其實正常情況下只會執行第一個monitorexit
釋放鎖,在釋放鎖之後就接著同步代碼塊後面的內容繼續向下執行了。而第二個,其實是用來處理異常的,可以看到,它的位置是在12行,如果程式運行發生異常,那麼就會執行第二個monitorexit
,並且會繼續向下通過athrow
指令拋出異常,而不是直接跳轉到15行正常運行下去。
實際上synchronized
使用的鎖就是存儲在Java對象頭中的,我們知道,對象是存放在堆記憶體中的,而每個對象內部,都有一部分空間用於存儲對象頭信息,而對象頭信息中,則包含了Mark Word用於存放hashCode
和對象的鎖信息,在不同狀態下,它存儲的數據結構有一些不同。
1.6.2 重量級鎖
在JDK6之前,synchronized
一直被稱為重量級鎖,monitor
依賴於底層操作系統的Lock實現,Java的線程是映射到操作系統的原生線程上,切換成本較高。而在JDK6之後,鎖的實現得到了改進。我們先從最原始的重量級鎖開始:
我們說了,每個對象都有一個monitor與之關聯,在Java虛擬機(HotSpot)中,monitor是由ObjectMonitor實現的:
ObjectMonitor() {
_header = NULL;
_count = 0; //記錄個數
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; //處於wait狀態的線程,會被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //處於等待鎖block狀態的線程,會被加入到該列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
每個等待鎖的線程都會被封裝成ObjectWaiter對象,進入到如下機制:
ObjectWaiter首先會進入 Entry Set等著,當線程獲取到對象的monitor
後進入 The Owner 區域並把monitor
中的owner
變數設置為當前線程,同時monitor
中的計數器count
加1,若線程調用wait()
方法,將釋放當前持有的monitor
,owner
變數恢復為null
,count
自減1,同時該線程進入 WaitSet集合中等待被喚醒。若當前線程執行完畢也將釋放monitor
並複位變數的值,以便其他線程進入獲取對象的monitor
。
雖然這樣的設計思路非常合理,但是在大多數應用上,每一個線程占用同步代碼塊的時間並不是很長,我們完全沒有必要將競爭中的線程掛起然後又喚醒,並且現代CPU基本都是多核心運行的,我們可以採用一種新的思路來實現鎖。
在JDK1.4.2時,引入了自旋鎖(JDK6之後預設開啟),它不會將處於等待狀態的線程掛起,而是通過無限迴圈的方式,不斷檢測是否能夠獲取鎖,由於單個線程占用鎖的時間非常短,所以說迴圈次數不會太多,可能很快就能夠拿到鎖並運行,這就是自旋鎖。當然,僅僅是在等待時間非常短的情況下,自旋鎖的表現會很好,但是如果等待時間太長,由於迴圈是需要處理器繼續運算的,所以這樣只會浪費處理器資源,因此自旋鎖的等待時間是有限制的,預設情況下為10次,如果失敗,那麼會進而採用重量級鎖機制。
在JDK6之後,自旋鎖得到了一次優化,自旋的次數限制不再是固定的,而是自適應變化的,比如在同一個鎖對象上,自旋等待剛剛成功獲得過鎖,並且持有鎖的線程正在運行,那麼這次自旋也是有可能成功的,所以會允許自旋更多次。當然,如果某個鎖經常都自旋失敗,那麼有可能會不再採用自旋策略,而是直接使用重量級鎖。
1.6.3 輕量級鎖
從JDK 1.6開始,為了減少獲得鎖和釋放鎖帶來的性能消耗,就引入了輕量級鎖。
輕量級鎖的目標是,在無競爭情況下,減少重量級鎖產生的性能消耗(並不是為了代替重量級鎖,實際上就是賭同一時間只有一個線程在占用資源),包括系統調用引起的內核態與用戶態切換、線程阻塞造成的線程切換等。它不像是重量級鎖那樣,需要向操作系統申請互斥量。它的運作機制如下:
在即將開始執行同步代碼塊中的內容時,會首先檢查對象的Mark Word,查看鎖對象是否被其他線程占用,如果沒有任何線程占用,那麼會在當前線程中所處的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用於複製並存儲對象目前的Mark Word信息(官方稱為Displaced Mark Word)。
接著,虛擬機將使用CAS操作將對象的Mark Word更新為輕量級鎖狀態(數據結構變為指向Lock Record的指針,指向的是當前的棧幀)
CAS(Compare And Swap)是一種無鎖演算法(我們之前在Springboot階段已經講解過了),它並不會為對象加鎖,而是在執行的時候,看看當前數據的值是不是我們預期的那樣,如果是,那就正常進行替換,如果不是,那麼就替換失敗。比如有兩個線程都需要修改變數
i
的值,預設為10,現在一個線程要將其修改為20,另一個要修改為30,如果他們都使用CAS演算法,那麼並不會加鎖訪問i
,而是直接嘗試修改i
的值,但是在修改時,需要確認i
是不是10,如果是,表示其他線程還沒對其進行修改,如果不是,那麼說明其他線程已經將其修改,此時不能完成修改任務,修改失敗。在CPU中,CAS操作使用的是
cmpxchg
指令,能夠從最底層硬體層面得到效率的提升。
如果CAS操作失敗了的話,那麼說明可能這時有線程已經進入這個同步代碼塊了,這時虛擬機會再次檢查對象的Mark Word,是否指向當前線程的棧幀,如果是,說明不是其他線程,而是當前線程已經有了這個對象的鎖,直接放心大膽進同步代碼塊即可。如果不是,那確實是被其他線程占用了。
這時,輕量級鎖一開始的想法就是錯的(這時有對象在競爭資源,已經賭輸了),所以說只能將鎖膨脹為重量級鎖,按照重量級鎖的操作執行(註意鎖的膨脹是不可逆的)
所以,輕量級鎖 -> 失敗 -> 自適應自旋鎖 -> 失敗 -> 重量級鎖
解鎖過程同樣採用CAS演算法,如果對象的MarkWord仍然指向線程的鎖記錄,那麼就用CAS操作把對象的MarkWord和複製到棧幀中的Displaced Mark Word進行交換。如果替換失敗,說明其他線程嘗試過獲取該鎖,在釋放鎖的同時,需要喚醒被掛起的線程。
輕量級鎖的加鎖過程:
(1)當線程執行代碼進入同步塊時,若Mark Word為無鎖狀態,虛擬機先在當前線程的棧幀中建立一個名為Lock Record的空間,用於存儲當前對象的Mark Word的拷貝,官方稱之為“Dispalced Mark Word”
(2)複製對象頭中的Mark Word到鎖記錄中。
(3)複製成功後,虛擬機將用CAS操作將對象的Mark Word更新為執行Lock Record的指針,並將Lock Record里的owner指針指向對象的Mark Word。如果更新成功,則執行4,否則執行5。;
(4)如果更新成功,則這個線程擁有了這個鎖,並將鎖標誌設為00,表示處於輕量級鎖狀態
(5)如果更新失敗,虛擬機會檢查對象的Mark Word是否指向當前線程的棧幀,如果是則說明當前線程已經擁有這個鎖,可進入執行同步代碼。否則說明多個線程競爭,輕量級鎖就會膨脹為重量級鎖,Mark Word中存儲重量級鎖(互斥鎖)的指針,後面等待鎖的線程也要進入阻塞狀態。
1.6.4 偏向鎖
偏向鎖相比輕量級鎖更純粹,乾脆就把整個同步都消除掉,不需要再進行CAS操作了。它的出現主要是得益於人們發現某些情況下某個鎖頻繁地被同一個線程獲取,這種情況下,我們可以對輕量級鎖進一步優化:偏向鎖實際上就是專門為單個線程而生的,當某個線程第一次獲得鎖時,如果接下來都沒有其他線程獲取此鎖,那麼持有鎖的線程將不再需要進行同步操作。
通俗的講,偏向鎖就是在運行過程中,對象的鎖偏向某個線程。即在開啟偏向鎖機制的情況下,某個線程獲得鎖,當該線程下次再想要獲得鎖時,不需要再獲得鎖(即忽略synchronized關鍵詞),直接就可以執行同步代碼,比較適合競爭較少的情況。
可以從之前的MarkWord結構中看到,偏向鎖也會通過CAS操作記錄線程的ID,如果一直都是同一個線程獲取此鎖,那麼完全沒有必要在進行額外的CAS操作。當然,如果有其他線程來搶了,那麼偏向鎖會根據當前狀態,決定是否要恢復到未鎖定或是膨脹為輕量級鎖。
如果我們需要使用偏向鎖,可以添加-XX:+UseBiased
參數來開啟。
所以,最終的鎖等級為:未鎖定 < 偏向鎖 < 輕量級鎖 < 重量級鎖
值得註意的是,如果對象通過調用hashCode()
方法計算過對象的一致性哈希值,那麼它是不支持偏向鎖的,會直接進入到輕量級鎖狀態,因為Hash是需要被保存的,而偏向鎖的Mark Word數據結構,無法保存Hash值;如果對象已經是偏向鎖狀態,再去調用hashCode()
方法,那麼會直接將鎖升級為重量級鎖,並將哈希值存放在monitor
(有預留位置保存)中。
偏向鎖的獲取流程:
(1)查看Mark Word中偏向鎖的標識以及鎖標誌位,若是否偏向鎖為1且鎖標誌位為01,則該鎖為可偏向狀態。
(2)若為可偏向狀態,則測試Mark Word中的線程ID是否與當前線程相同,若相同,則直接執行同步代碼,否則進入下一步。
(3)當前線程通過CAS操作競爭鎖,若競爭成功,則將Mark Word中線程ID設置為當前線程ID,然後執行同步代碼,若競爭失敗,進入下一步。
(4)當前線程通過CAS競爭鎖失敗的情況下,說明有競爭。當到達全局安全點時之前獲得偏向鎖的線程被掛起,偏向鎖升級為輕量級鎖,然後被阻塞在安全點的線程繼續往下執行同步代碼。
偏向鎖的釋放流程:
偏向鎖只有遇到其他線程嘗試競爭偏向鎖時,持有偏向鎖狀態的線程才會釋放鎖,線程不會主動去釋放偏向鎖。偏向鎖的撤銷需要等待全局安全點(即沒有位元組碼正在執行),它會暫停擁有偏向鎖的線程,撤銷後偏向鎖恢復到未鎖定狀態或輕量級鎖狀態。
1.6.5 鎖消除和鎖粗化
鎖消除和鎖粗化都是在運行時的一些優化方案。
- 鎖消除是比如我們某段代碼雖然加了鎖,但是在運行時根本不可能出現各個線程之間資源爭奪的情況,這種情況下,完全不需要任何加鎖機制,所以鎖會被消除。
- 鎖粗化則是我們代碼中頻繁地出現互斥同步操作,比如在一個迴圈內部加鎖,這樣明顯是非常消耗性能的,所以虛擬機一旦檢測到這種操作,會將整個同步範圍進行擴展。
2. Lock鎖
2.0 Lock鎖和synchronized的區別
- Synchronized是內置Java關鍵字;Lock是一個Java類。
- Synchronized無法判斷獲取鎖的狀態;Lock可以判斷是否獲取到了鎖。(boolean b = lock.tryLock();)
- Synchronized會自動釋放鎖;Lock必須要手動釋放鎖,如果不釋放鎖,死鎖。
- Synchronized線程1獲得鎖阻塞時,線程2會一直等待下去;Lock鎖線程1獲得鎖阻塞時,線程2等待足夠長的時間後中斷等待,去做其他的事。
- Synchronized可重入鎖,不可以中斷的,非公平;Lock,可重入鎖,可以判斷鎖,非公平(可以自己設置)。
lock.lockInterruptibly();方法:當兩個線程同時通過該方法想獲取某個鎖時,假若此時線程A獲取到了鎖,而線程B只有在等待,那麼對線程B調用threadB.interrupt()方法能夠中斷線程B的等待過程。 - Synchronized適合鎖少量的代碼同步問題;Lock適合鎖大量的同步代碼。
2.1 Lock介面的三個實現類
由jdk查詢可知,有三種類
2.2 ReentrantLock類
ReentrantLock鎖的對象是調用lock方法的實例對象
使用創建ReentrantLock對象代替傳統的Synchronized鎖
2.2.1 構造方法——公平鎖and非公平鎖
公平鎖:十分公平,不能插隊。
非公平鎖:十分不公平,可以插隊。(預設非公平鎖)
需要更改預設的非公平鎖為公平鎖,需要在創建對象的時候參數設置為true(預設是false)
2.2.2 ReentrantLock類的使用
class X {
private final ReentrantLock lock = new ReentrantLock();
// ...
public void m() {
lock.lock(); // block until condition holds
try {
//業務代碼 ... method body
} finally {
lock.unlock();
}
}
}
2.3 Condition介面
使用await和signal方法代替傳統的wait和notify方法
2.3.1 await() signal() 方法基本使用
就是在最原始的多線程synchronized寫法上修改了使用的方法
使用while的緣故還是:可能會出現虛假喚醒
2.3.2 Condition實現精準通知喚醒
使用ReentrantLock創建的對象lock來創建多個condition對象,每次等待和喚醒都可以指定 如:conditionA.await(); conditionB.signal();
舉例:
public class C {
public static void main(String[] args) {
Data3 data3 = new Data3();
//A執行完,調用B,B執行完,調用C,C執行完,調用A
new Thread(() -> {
for (int i = 0; i < 10; i++) {
data3.printA();
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
data3.printB();
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
data3.printC();
}
}, "C").start();
}
}
class Data3 {
private Lock lock = new ReentrantLock();
Condition conditionA = lock.newCondition();
Condition conditionB = lock.newCondition();
Condition conditionC = lock.newCondition();
private char ch = 'A';
public void printA() {
lock.lock();
try {
while (ch != 'A') {
//等待
conditionA.await();
}
System.out.println(Thread.currentThread().getName() + "--->A");
//喚醒
ch = 'B';
conditionB.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printB() {
lock.lock();
try {
while (ch != 'B') {
//等待
conditionB.await();
}
System.out.println(Thread.currentThread().getName() + "--->B");
//喚醒
ch = 'C';
conditionC.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printC() {
lock.lock();
try {
while (ch != 'C') {
//等待
conditionC.await();
}
System.out.println(Thread.currentThread().getName() + "--->C");
//喚醒
ch = 'A';
conditionA.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
結果:執行順序變成以此執行
A--->A
B--->B
C--->C
A--->A
B--->B
C--->C
A--->A
B--->B
C--->C
A--->A
B--->B
C--->C
A--->A
B--->B
C--->C
A--->A
B--->B
C--->C
A--->A
B--->B
C--->C
A--->A
B--->B
C--->C
A--->A
B--->B
C--->C
A--->A
B--->B
C--->C
3. 生產者與消費者
3.1 傳統的synchronized寫法
synchronized+wait+notifyall
public class A {
public static void main(String[] args) {
Data data = new Data();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "C").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "D").start();
}
}
class Data {
private int number = 0;
public synchronized void increment() throws InterruptedException {
if (number != 0) {
//等待
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName() + "==>" + number);
//通知其他線程,我+1完畢了
this.notifyAll();
}
public synchronized void decrement() throws InterruptedException {
if (number == 0) {
//等待
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName() + "==>" + number);
//通知其他線程,我-1完畢了
this.notifyAll();
}
}
輸出:發現有問題,出現了負數和大於1的數,這就是虛假喚醒問題(看下麵)
A==>1
C==>0
B==>1
A==>2
B==>3
C==>2
C==>1
C==>0
B==>1
A==>2
B==>3
C==>2
C==>1
C==>0
B==>1
A==>2
B==>3
D==>2
D==>1
D==>0
C==>-1
C==>-2
C==>-3
D==>-4
D==>-5
D==>-6
D==>-7
D==>-8
D==>-9
D==>-10
B==>-9
A==>-8
B==>-7
A==>-6
B==>-5
A==>-4
B==>-3
A==>-2
3.2 Lock寫法
ReentrantLock類 和 Condition介面:lock+await+signal
public class PC {
public static void main(String[] args) {
Data data = new Data();
new Thread(()->{
for (int i = 0; i < 10; i++) {
data.decrement();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
data.increment();
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
data.decrement();
}
},"C").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
data.increment();
}
},"D").start();
}
}
class Data {
private int number = 0;
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void increment() {
lock.lock();
try {
if (number > 0) {
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
number++;
System.out.println(Thread.currentThread().getName() + "=>" + number);
condition.signalAll();
} finally {
lock.unlock();
}
}
public void decrement() {
lock.lock();
try {
if (number <= 0) {
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
number--;
System.out.println(Thread.currentThread().getName() + "=>" + number);
condition.signalAll();
} finally {
lock.unlock();
}
}
}
輸出:發現有問題,出現了負數,這就是虛假喚醒問題(看下麵)
B=>1
A=>0
B=>1
A=>0
C=>-1
B=>0
B=>1
A=>0
C=>-1
B=>0
B=>1
A=>0
C=>-1
D=>0
D=>1
B=>2
A=>1
A=>0
C=>-1
D=>0
D=>1
B=>2
A=>1
A=>0
C=>-1
D=>0
D=>1
B=>2
A=>1
A=>0
C=>-1
D=>0
D=>1
B=>2
C=>1
C=>0
D=>1
C=>0
D=>1
C=>0
3.2 虛假喚醒問題
3.2.1 問題描述
白話:一個if條件里有等待語句,兩個消費者都進入這個等待;當生產者生產了1數量的產品並喚醒消費者,此時之前處於等待的兩個消費者會被喚醒,然後進行消費;導致最後消費的產品出現負數,因為產品只有一個,而消費者消費了兩次。
虛假喚醒是一種現象,它只會出現在多線程環境中,指的是在多線程環境下,多個線程等待在同一個條件上,等到條件滿足時,所有等待的線程都被喚醒,但由於多個線程執行的順序不同,後面競爭到鎖的線程在獲得時間片時條件已經不再滿足,線程應該繼續睡眠但是卻繼續往下運行的一種現象。
舉例:
public class A {
public static void main(String[] args) {
Data data = new Data();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "C").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "D").start();
}
}
class Data {
private int number = 0;
public synchronized void increment() throws InterruptedException {
if (number != 0) {
//等待
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName() + "==>" + number);
//通知其他線程,我+1完畢了
this.notifyAll();
}
public synchronized void decrement() throws InterruptedException {
if (number == 0) {
//等待
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName() + "==>" + number);
//通知其他線程,我-1完畢了
this.notifyAll();
}
}
結果:出現了大於1的情況,以及小於0的情況
A==>1
C==>0
B==>1
A==>2
B==>3
C==>2
C==>1
C==>0
B==>1
A==>2
B==>3
C==>2
C==>1
C==>0
B==>1
A==>2
B==>3
D==>2
D==>1
D==>0
C==>-1
C==>-2
C==>-3
D==>-4
D==>-5
D==>-6
D==>-7
D==>-8
D==>-9
D==>-10
B==>-9
A==>-8
B==>-7
A==>-6
B==>-5
A==>-4
B==>-3
A==>-2
3.2.2 解決方法