Java 線程通信之 wait/notify 機(jī)制
作者丨ytao
來源丨ytao
前言
Java 線程通信是將多個(gè)獨(dú)立的線程個(gè)體進(jìn)行關(guān)聯(lián)處理,使得線程與線程之間能進(jìn)行相互通信。比如線程 A 修改了對(duì)象的值,然后通知給線程 B,使線程 B 能夠知道線程 A 修改的值,這就是線程通信。
wait/notify 機(jī)制
一個(gè)線程調(diào)用 Object 的 wait() 方法,使其線程被阻塞;另一線程調(diào)用 Object 的 notify()/notifyAll() 方法,wait() 阻塞的線程繼續(xù)執(zhí)行。
wai/notify 方法
| 方法 | 說明 |
|---|---|
| wait() | 當(dāng)前線程被阻塞,線程進(jìn)入 WAITING 狀態(tài) |
| wait(long) | 設(shè)置線程阻塞時(shí)長,線程會(huì)進(jìn)入 TIMED_WAITING 狀態(tài)。如果設(shè)置時(shí)間內(nèi)(毫秒)沒有通知,則超時(shí)返回 |
| wait(long, int) | 納秒級(jí)別的線程阻塞時(shí)長設(shè)置 |
| notify() | 通知同一個(gè)對(duì)象上已執(zhí)行 wait() 方法且獲得對(duì)象鎖的等待線程 |
| notifyAll() | 通知同一對(duì)象上所有等待的線程 |
實(shí)現(xiàn) wait/notify 機(jī)制的條件:
調(diào)用 wait 線程和 notify 線程必須擁有相同對(duì)象鎖。
wait() 方法和 notify()/notifyAll() 方法必須在 Synchronized 方法或代碼塊中。
由于 wait/notify 方法是定義在 java.lang.Object中,所以在任何 Java 對(duì)象上都可以使用。
wait 方法
在執(zhí)行 wait() 方法前,當(dāng)前線程必須已獲得對(duì)象鎖。調(diào)用它時(shí)會(huì)阻塞當(dāng)前線程,進(jìn)入等待狀態(tài),在當(dāng)前 wait() 處暫停線程。同時(shí),wait() 方法執(zhí)行后,會(huì)立即釋放獲得的對(duì)象鎖。
下面通過案例來查看 wait() 釋放鎖。
首先查看不使用 wait() 方法時(shí)的代碼執(zhí)行情況:
package top.ytao.demo.thread.waitnotify;
/**
* Created by YangTao
*/
publicclassWaitTest{
staticObject object = newObject();
publicstaticvoid main(String[] args) {
newThread(() -> {
synchronized(object){
System.out.println("開始線程 A");
try{
Thread.sleep(2000L);
} catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println("結(jié)束線程 A");
}
}, "線程 A").start();
newThread(() -> {
try{
Thread.sleep(500L);
} catch(InterruptedException e) {
e.printStackTrace();
}
synchronized(object){
System.out.println("開始線程 B");
System.out.println("結(jié)束線程 B");
}
}, "線程 B").start();
}
}
創(chuàng)建 A、B 兩個(gè)線程,。首先在 B 線程創(chuàng)建后 sleep ,保證 B 線程的打印后于 A 線程執(zhí)行。在 A 線程中,獲取到對(duì)象鎖后,sleep 一段時(shí)間,且時(shí)間大于 B 線程的 sleep 時(shí)間。
執(zhí)行結(jié)果為:

從上圖結(jié)果中,可以看到,B 線程一定等 A 線程執(zhí)行完 synchronize 代碼塊釋放對(duì)象鎖后 A 線程再獲取對(duì)象鎖進(jìn)入 synchronize 代碼塊中。在這過程中,Thread.sleep() 方法也不會(huì)釋放鎖。
當(dāng)前在 A 線程 synchronize 代碼塊中執(zhí)行 wait() 方法后,就會(huì)主動(dòng)釋放對(duì)象鎖,A 線程代碼如下:
newThread(() -> {
synchronized(object){
System.out.println("開始線程 A");
try{
// 調(diào)用 object 對(duì)象的 wait 方法
object.wait();
Thread.sleep(2000L);
} catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println("結(jié)束線程 A");
}
}, "線程 A").start();
執(zhí)行結(jié)果(這里結(jié)果圖片放錯(cuò),查看原文有正確圖片):

同時(shí) A 線程一直處于阻塞狀態(tài),不會(huì)打印 結(jié)束線程A。
wait(long) 方法是設(shè)置超時(shí)時(shí)間,當(dāng)?shù)却龝r(shí)間大于設(shè)置的超時(shí)時(shí)間后,會(huì)繼續(xù)往 wait(long) 方法后的代碼執(zhí)行。
newThread(() -> {
synchronized(object){
System.out.println("開始線程 A");
try{
object.wait(1000);
Thread.sleep(2000L);
} catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println("結(jié)束線程 A");
}
}, "線程 A").start();
執(zhí)行結(jié)果

同理,wait(long, int) 方法與 wait(long) 同樣,只是多個(gè)納秒級(jí)別的時(shí)間設(shè)置。
notify 方法
同樣,在執(zhí)行 notify() 方法前,當(dāng)前線程也必須已獲得線程鎖。調(diào)用 notify() 方法后,會(huì)通知一個(gè)執(zhí)行了 wait() 方法的阻塞等待線程,使該等待線程重新獲取到對(duì)象鎖,然后繼續(xù)執(zhí)行 wait() 后面的代碼。但是,與 wait() 方法不同,執(zhí)行 notify() 后,不會(huì)立即釋放對(duì)象鎖,而需要執(zhí)行完 synchronize 的代碼塊或方法才會(huì)釋放鎖,所以接收通知的線程也不會(huì)立即獲得鎖,也需要等待執(zhí)行 notify() 方法的線程釋放鎖后再獲取鎖。
notify()
下面是 notify() 方法的使用,實(shí)現(xiàn)一個(gè)完整的 wait/notify 的例子,同時(shí)驗(yàn)證發(fā)出通知后,執(zhí)行 notify() 方法的線程是否立即釋放鎖,執(zhí)行 wait() 方法的線程是否立即獲取鎖。
package top.ytao.demo.thread.waitnotify;
/**
* Created by YangTao
*/
publicclassWaitNotifyTest{
staticObject object = newObject();
publicstaticvoid main(String[] args) {
System.out.println();
newThread(() -> {
synchronized(object){
System.out.println("開始線程 A");
try{
object.wait();
System.out.println("A 線程重新獲取到鎖,繼續(xù)進(jìn)行");
} catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println("結(jié)束線程 A");
}
}, "線程 A").start();
newThread(() -> {
try{
Thread.sleep(500L);
} catch(InterruptedException e) {
e.printStackTrace();
}
synchronized(object){
System.out.println("開始線程 B");
object.notify();
System.out.println("線程 B 通知完線程 A");
try{
// 試驗(yàn)執(zhí)行完 notify() 方法后,A 線程是否能立即獲取到鎖
Thread.sleep(2000L);
} catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println("結(jié)束線程 B");
}
}, "線程 B").start();
}
}
以上 A 線程執(zhí)行 wait() 方法,B 線程執(zhí)行 notify() 方法,執(zhí)行結(jié)果為:

執(zhí)行結(jié)果中可以看到,B 線程執(zhí)行 notify() 方法后,即使 sleep 了,A 線程也沒有獲取到鎖,可知,notify() 方法并沒有釋放鎖。
notify() 是通知到等待中的線程,但是調(diào)用一次 notify() 方法,只能通知到一個(gè)執(zhí)行 wait() 方法的等待線程。如果有多個(gè)等待狀態(tài)的線程,則需多次調(diào)用 notify() 方法,通知到線程順序則根據(jù)執(zhí)行 wait() 方法的先后順序進(jìn)行通知。
下面創(chuàng)建有兩個(gè)執(zhí)行 wait() 方法的線程的代碼:
package top.ytao.demo.thread.waitnotify;
/**
* Created by YangTao
*/
publicclassMultiWaitNotifyTest{
staticObject object = newObject();
publicstaticvoid main(String[] args) {
System.out.println();
newThread(() -> {
synchronized(object){
System.out.println("開始線程 A");
try{
object.wait();
} catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println("結(jié)束線程 A");
}
}, "線程 A").start();
newThread(() -> {
try{
Thread.sleep(500L);
} catch(InterruptedException e) {
e.printStackTrace();
}
synchronized(object){
System.out.println("開始線程 B");
try{
object.wait();
} catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println("結(jié)束線程 B");
}
}, "線程 B").start();
newThread(() -> {
try{
Thread.sleep(3000L);
} catch(InterruptedException e) {
e.printStackTrace();
}
synchronized(object){
System.out.println("開始通知線程 C");
object.notify();
object.notify();
System.out.println("結(jié)束通知線程 C");
}
}, "線程 C").start();
}
}
先 A 線程執(zhí)行 wait() 方法,然后 B 線程執(zhí)行 wait() 方法,最后 C 線程調(diào)用兩次 notify() 方法,執(zhí)行結(jié)果:

notifyAll()
通知多個(gè)等待狀態(tài)的線程,通過多次調(diào)用 notify() 方法實(shí)現(xiàn)的方案,在實(shí)際應(yīng)用過程中,實(shí)現(xiàn)過程不太友好,如果是想通知所有等待狀態(tài)的線程,可使用 notifyAll() 方法,就能喚醒所有線程。
實(shí)現(xiàn)方式,只需將上面 C 線程的多次調(diào)用 notify() 方法部分改為調(diào)用一次 notifyAll() 方法即可。
newThread(() -> {
try{
Thread.sleep(3000L);
} catch(InterruptedException e) {
e.printStackTrace();
}
synchronized(object){
System.out.println("開始通知線程 C");
object.notifyAll();
System.out.println("結(jié)束通知線程 C");
}
}, "線程 C").start();
執(zhí)行結(jié)果:

根據(jù)不同 JVM 的實(shí)現(xiàn),notifyAll() 的喚醒順序會(huì)有所不同,當(dāng)前測(cè)試環(huán)境中,以倒序順序喚醒線程。
實(shí)現(xiàn)生產(chǎn)者消費(fèi)者模式
生產(chǎn)消費(fèi)者模式就是一個(gè)線程生產(chǎn)數(shù)據(jù)進(jìn)行存儲(chǔ),另一線程進(jìn)行數(shù)據(jù)提取消費(fèi)。下面就以兩個(gè)線程來模擬,生產(chǎn)者生成一個(gè) UUID 存放到 List 對(duì)象中,消費(fèi)者讀取 List 對(duì)象中的數(shù)據(jù),讀取完成后進(jìn)行清除。
實(shí)現(xiàn)代碼如下:
package top.ytao.demo.thread.waitnotify;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* Created by YangTao
*/
publicclassWaitNotifyModelTest{
// 存儲(chǔ)生產(chǎn)者產(chǎn)生的數(shù)據(jù)
staticList<String> list = newArrayList<>();
publicstaticvoid main(String[] args) {
newThread(() -> {
while(true){
synchronized(list){
// 判斷 list 中是否有數(shù)據(jù),如果有數(shù)據(jù)的話,就進(jìn)入等待狀態(tài),等數(shù)據(jù)消費(fèi)完
if(list.size() != 0){
try{
list.wait();
} catch(InterruptedException e) {
e.printStackTrace();
}
}
// list 中沒有數(shù)據(jù)時(shí),產(chǎn)生數(shù)據(jù)添加到 list 中
list.add(UUID.randomUUID().toString());
list.notify();
System.out.println(Thread.currentThread().getName() + list);
}
}
}, "生產(chǎn)者線程 A ").start();
newThread(() -> {
while(true){
synchronized(list){
// 如果 list 中沒有數(shù)據(jù),則進(jìn)入等待狀態(tài),等收到有數(shù)據(jù)通知后再繼續(xù)運(yùn)行
if(list.size() == 0){
try{
list.wait();
} catch(InterruptedException e) {
e.printStackTrace();
}
}
// 有數(shù)據(jù)時(shí),讀取數(shù)據(jù)
System.out.println(Thread.currentThread().getName() + list);
list.notify();
// 讀取完畢,將當(dāng)前這條 UUID 數(shù)據(jù)進(jìn)行清除
list.clear();
}
}
}, "消費(fèi)者線程 B ").start();
}
}
運(yùn)行結(jié)果:

生產(chǎn)者線程運(yùn)行時(shí),如果已存在未消費(fèi)的數(shù)據(jù),則當(dāng)前線程進(jìn)入等待狀態(tài),收到通知后,表明數(shù)據(jù)已消費(fèi)完,再繼續(xù)向 list 中添加數(shù)據(jù)。
消費(fèi)者線程運(yùn)行時(shí),如果不存在未消費(fèi)的數(shù)據(jù),則當(dāng)前線程進(jìn)入等待狀態(tài),收到通知后,表明 List 中已有新數(shù)據(jù)被添加,繼續(xù)執(zhí)行代碼消費(fèi)數(shù)據(jù)并清除。
不管是生產(chǎn)者還是消費(fèi)者,基于對(duì)象鎖,一次只能一個(gè)線程能獲取到,如果生產(chǎn)者獲取到鎖就校驗(yàn)是否需要生成數(shù)據(jù),如果消費(fèi)者獲取到鎖就校驗(yàn)是否有數(shù)據(jù)可消費(fèi)。
一個(gè)簡(jiǎn)單的生產(chǎn)者消費(fèi)者模式就以完成。
總結(jié)
等待/通知機(jī)制是實(shí)現(xiàn) Java 線程間通信的一種方式,將多線程中,各個(gè)獨(dú)立運(yùn)行的線程通過相互通信來更高效的協(xié)作完成工作,更大效率利用 CPU 處理程序。這也是學(xué)習(xí)或研究 Java 線程的必學(xué)知識(shí)點(diǎn)。
-End-
最近有一些小伙伴,讓我?guī)兔φ乙恍?nbsp;面試題 資料,于是我翻遍了收藏的 5T 資料后,匯總整理出來,可以說是程序員面試必備!所有資料都整理到網(wǎng)盤了,歡迎下載!

面試題】即可獲取