1 前言
本篇文章預設大家對synchronized
跟ReentrantLock
有一定了解。
1.1 先來段程式碼放鬆一下
下面一段簡單的程式碼,主要是通過3個執行緒對count進行累計來進行模擬多執行緒的場景。
/**
* zhongxianyao
*/
public class Test {
private static final int N = 3;
private int count = 0;
public void doSomething() {
// 實際業務中,這裡可能是遠端獲取資料之類的耗時操作
for (int i=0; i<1000_000; i++) {
synchronized (this) {
count ++;
}
}
}
public static void main(String[] args) throws Exception {
Test test = new Test();
for (int i=0; i<N; i++) {
Runnable runnable = () -> test.doSomething();
new Thread(runnable).start();
}
Thread.sleep(1000);
System.out.println(test.count);
}
}
在多執行緒程式設計中,一旦呼叫start()後,什麼時候真正分配CPU時間片執行是不確定的,執行多久也是不確定的,所以有時候可能根據經驗,預估一下程式的執行時間,然後進行sleep,最後獲取結果。但這種方式太low了,有沒有那麼一種方式,當程式獲取到結果後進行通知呢?下面將引出今天要講的等待/通知機制。
2 Object wait()/notify()
2.1 一段入門程式碼
先來一段程式碼看一下wait()/notify()的基本用法
/**
* zhongxianyao
*/
public class Test {
private static final int N = 3;
private int count = 0;
private int finishCount = 0;
public void doSomething() {
for (int i=0; i<1000_000; i++) {
synchronized (this) {
count ++;
}
}
synchronized (this) {
finishCount ++;
notify();
}
}
public static void main(String[] args) {
Test test = new Test();
for (int i=0; i<N; i++) {
Runnable runnable = () -> test.doSomething();
new Thread(runnable).start();
}
synchronized (test) {
try {
while (test.finishCount != N) {
test.wait();
}
} catch (Exception e) {
e.printStackTrace();
}
}
System.out.println(test.count);
}
}
結果輸出3000000
,結果是正確,是自己想要的。
2.2 問題三連擊
a.為什麼官方說wait() 要放在while裡面?
介面描述如下
As in the one argument version, interrupts and spurious wakeups are possible, and this method should always be used in a loop:
synchronized (obj) {
while (<condition does not hold>)
obj.wait();
... // Perform action appropriate to condition
}
翻譯一下:在一個論點版本中,中斷跟虛假喚醒是可能,所以這個方法應始終放在一個迴圈中。
加上一句自己的解釋:一般在專案中,一個執行緒不可能無緣無故等待,總是需要在某種條件下進行等待,而且其他執行緒喚醒這個執行緒的時候,可能用的是notifyAll(),資料被其他執行緒消費了,這裡需要在判斷一下是否滿足特定的條件再繼續執行。
b.為什麼wait()必須在同步方法/程式碼塊中呼叫?
解釋1:wait()本身設計的邏輯就是在釋放鎖進行等待,如果沒有獲取鎖,談何釋放。
解釋2:通常在wait()的方法前面都會有while語句的判斷,在這兩條語句中會有時間間隔,可能會破壞程式,需要加上synchronized同步程式碼塊來保證原子操作。
c.為什麼wait(), notify() 和 notifyAll()是定義在Object裡面而不是在Thread裡面?
因為wait()等方法都是鎖級別操作,再者Java提供的鎖都是物件級別的而不是執行緒級別的,每個物件都有鎖。如果wait()方法定義在Thread類中,執行緒正在等待的是哪個鎖就不明顯了。
2.3 wait(long timeout)
在上面的例子中,如果notify();
那行程式碼刪除,wait()
改為wait(100)
,如下,那麼程式是否可以獲取到正確的結果呢?
/**
* zhongxianyao
*/
public class Test {
private static final int N = 3;
private int count = 0;
private int finishCount = 0;
public void doSomething() {
for (int i=0; i<1000_000; i++) {
synchronized (this) {
count ++;
}
}
synchronized (this) {
finishCount ++;
//notify();
}
}
public static void main(String[] args) {
Test test = new Test();
for (int i=0; i<N; i++) {
Runnable runnable = () -> test.doSomething();
new Thread(runnable).start();
}
synchronized (test) {
try {
while (test.finishCount != N) {
test.wait(100);
}
} catch (Exception e) {
e.printStackTrace();
}
}
System.out.println(test.count);
}
}
執行結果是3000000
,是正確的結果,看了一下文件,發現這個欄位跟直覺理解的不一樣,直覺告訴我,這個是最長等多久,等太久了就InterruptedException
,結果不是。這個方法設定的時間,是說等待多久就喚醒自己。
3 Condition await()/signal()
3.1 用Condition進行替換
下面的程式碼,把前一個例子中的synchronized程式碼塊,換成了lock()/unlock,notify()換成了condition.signal(),wait()換成了condition.await()。執行結果也是正確的。
/**
* zhongxianyao
*/
public class Test {
private static final int N = 3;
private int count = 0;
private int finishCount = 0;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void doSomething() {
for (int i=0; i<1000_000; i++) {
synchronized (this) {
count ++;
}
}
lock.lock();
finishCount ++;
if (finishCount == N) {
condition.signal();
}
lock.unlock();
}
public static void main(String[] args) {
Test test = new Test();
for (int i=0; i<N; i++) {
Runnable runnable = () -> test.doSomething();
new Thread(runnable).start();
}
test.lock.lock();
try {
test.condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
test.lock.unlock();
}
System.out.println(test.count);
}
}
3.2 signal()方法後不建議新增邏輯
public class ConditionTest {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
new Thread(() -> {
try {
long time = System.currentTimeMillis();
lock.lock();
System.out.println("await start");
condition.await();
System.out.println("await end " + (System.currentTimeMillis() - time) + "ms");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "Thread-await").start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
try {
lock.lock();
System.out.println("signal start");
TimeUnit.SECONDS.sleep(5);
condition.signal();
System.out.println("signal end");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println("signal unlock");
}
}, "Thread-signal").start();
}
}
多次執行,結果都是一樣的,如下:
await start
signal start
signal end
signal unlock
await end 5005ms
從執行結果可以看出,await()後,鎖就釋放了,但signal()後,鎖不釋放,一定要在unlock()之後,鎖才釋放,await()才會往下執行。
既然喚醒了其他執行緒,又不釋放鎖,可以調整喚醒的時機。一般在實際程式碼中,也是不建議signal()方法後新增邏輯,應該直接釋放鎖。
同理,上面的notify()也是在synchronized程式碼塊結束後,wait()後面的語句才能真正執行。
3.3 boolean await(long time, TimeUnit unit)
把上面的condition.await()
改為condition.await(1, TimeUnit.SECONDS)
,然後獲取返回值,執行結果返回的是false
。
這個時候,如果把TimeUnit.SECONDS.sleep(5)
,condition.signal()
這兩行程式碼順序調換一下,那麼await
的返回值就是true
。
再看到官方文件對這個返回值的描述,如下
{@code false} if the waiting time detectably elapsed
before return from the method, else {@code true}
翻譯過來,大致意思就是“如果等待時間可以在方法返回之前檢測到返回false,否則返回true”。但實際測試結果卻是await()
被喚醒的時候,而不是方法返回的時候。
4 區別
- Object wait() notify() 搭配synchronized使用
- Condition await() signal() 搭配Lock使用
- Object notify() 是隨機喚醒一個
- Condition signal() 是喚醒第一個await()的執行緒
- Object wait()有虛假喚醒,而Condition await() 沒有
5 參考文件
- https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html
- https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/locks/Condition.html
- https://stackoverflow.com/questions/2779484/why-must-wait-always-be-in-synchronized-block