什麼是多線程 利用對象,可將一個程式分割成相互獨立的區域。我們通常也需要將一個程式轉換成多個獨立運行的子任 務。象這樣的每個子任務都叫作一個“線程”(Thread)。編寫程式時,可將每個線程都想象成獨立運行,而且 都有自己的專用CPU。一些基礎機制實際會為我們自動分割CPU的時間。我們通常不必關心這 ...
什麼是多線程
利用對象,可將一個程式分割成相互獨立的區域。我們通常也需要將一個程式轉換成多個獨立運行的子任
務。象這樣的每個子任務都叫作一個“線程”(Thread)。編寫程式時,可將每個線程都想象成獨立運行,而且
都有自己的專用CPU。一些基礎機制實際會為我們自動分割CPU的時間。我們通常不必關心這些細節問題,
所以多線程的代碼編寫是相當簡便的。引自《Java編程思想》第四版
生命周期
如何開啟一個線程
- 繼承Thread類
Thread類本質上是實現了Runnable介面的一個實例,代表一個線程的實例。啟動線程的唯一方法就是通過Thread類的start()實例方法。start()方法是一個native方法,它將啟動一個新線程,並執行run()方法。這種方式實現多線程很簡單,通過自己的類直接extend Thread,並覆寫run()方法,就可以啟動新線程並執行自己定義的run()方法。例如:
public class StartThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 120; i++) {
System.out.println("聽歌。。。");
}
}
public static void main(String[] args) {
StartThread st = new StartThread();
st.start();
for (int i = 0; i < 120; i++) {
System.out.println("敲代碼。。。");
}
}
}
- 實現Runnable介面
如果自己的類已經extends另一個類,就無法直接extends Thread,此時,可以實現一個Runnable介面,如下:
public class StartRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 120; i++) {
System.out.println("聽歌。。。");
}
}
public static void main(String[] args) {
StartRunnable sr = new StartRunnable();
Thread t = new Thread(sr);
t.start();
for (int i = 0; i < 120; i++) {
System.out.println("敲代碼。。。");
}
}
}
- 實現Callable介面
實現Callable介面實現線程開啟相比較上兩個步驟會多一些,是juc併發包下的一個介面。
public class StartCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
for (int i = 0; i < 120; i++) {
System.out.println("聽歌。。。");
}
return 1;
}
public static void main(String[] args) throws Exception {
StartCallable sc = new StartCallable();
ExecutorService es = Executors.newFixedThreadPool(1);
Future<Integer> submit = es.submit(sc);
Integer result = submit.get();
es.shutdownNow();
System.out.println(result);
for (int i = 0; i < 120; i++) {
System.out.println("敲代碼。。。");
}
}
}
線程狀態
線程在生命周期內,共五大狀態
具體說明:
- 實例線程--->新生狀態
- 調用start()--->就緒狀態
- 操作系統CPU調度器分配好進行調用--->運行狀態
- 線程正常執行完畢或外部干涉--->死亡狀態
IO文件讀寫、調用sleep()、調用wait()--->阻塞狀態
線程主要方法:
線程終止
停止線程的stop()方法jdk已經過失,不推薦使用,如何優雅的停止線程呢?舉例:
public class Study implements Runnable {
// 線程類中,定義線程體使用的標識
private boolean stop = true;
private String name;
Study(String name) {
this.name = name;
}
@Override
public void run() {
int i = 0;
// 線程體使用該標識
while (stop) {
System.out.println(name + "線程正常運行。。。===>" + i++);
}
}
// 對外提供方法改變標識
public void stop() {
this.stop = false;
}
public static void main(String[] args) {
Study s = new Study("張三");
Thread thread = new Thread(s);
thread.start();
for (int i = 0; i < 1000; i++) {
if (i == 900) {
s.stop();
System.out.println("game over。。。。。。");
}
System.out.println("main====>" + i);
}
}
}
暫停 sleep()
- sleep(時間)指定當前線程阻塞的毫秒數
- sleep()存在異常 InterruptedExcetion
- sleep()時間到達後線程進入就緒狀態
- sleep()可以模擬網路延時、倒計時等
- 每一個對象都有一個鎖,sleep()不會釋放鎖
- 舉例:
// sleep() 模擬倒計時
public static void main(String[] args) throws InterruptedException {
// 當前時間戳
long millis = System.currentTimeMillis();
// 當前時間加十秒
Date endTime = new Date(millis + 1000 * 10);
long end = endTime.getTime();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
for (; ; ) {
System.out.println(sdf.format(endTime));
// 睡眠一秒
Thread.sleep(1000L);
// 獲取倒計時一秒後的時間
endTime = new Date(endTime.getTime() - 1000L);
if ((end - 10000) >= endTime.getTime()) {
break;
}
}
}
yield()
- 禮讓線程,讓當前正在執行的線程暫停
- 不是阻塞線程,而是將線程從 運行狀態 轉入就緒狀態
- 讓CPU調度器重新調度
舉例:
public class Yield {
public static void main(String[] args) {
new Thread(()-> {
for (int i = 0; i < 100; i++) {
System.out.println("我是另一個線程。。。");
}
}).start();
for (int i = 0; i < 100; i++) {
if (i % 20 == 0) {
System.out.println(i + "-->開始禮讓");
// main線程進行禮讓
Thread.yield();
}
System.out.println("main 線程-->" + i);
}
}
}
註意:yield是讓線程進入就緒狀態,不是阻塞狀態。
插隊 join()
join() 合併線程,待此線程執行完畢後,再執行其他線程,其他線程阻塞。
舉例:
public class Example {
public static void main(String[] args) {
System.out.println("買煙的故事。。。");
new Thread(new Father()).start();
}
}
class Father extends Thread {
@Override
public void run() {
System.out.println("老爸抽煙,煙沒了,讓兒子買煙。");
Thread t = new Thread(new Son());
t.start();
try {
// Father 線程被阻塞
t.join();
System.out.println("兒子把煙買來了,交給老爸,老爸把剩餘零錢給了兒子。");
} catch (InterruptedException e) {
System.out.println("兒子走丟了。。。");
}
}
}
class Son extends Thread {
@Override
public void run() {
System.out.println("接過老爸的錢去買煙");
System.out.println("路邊有個游戲廳,玩了10秒");
for (int i = 0; i < 10; i++) {
System.out.println(i + "秒過去了。。。");
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("突然想起還要買煙,趕緊買煙。");
System.out.println("手拿一包中華,回家了。");
}
}
深度觀察狀態
/**
* 觀察線程狀態
*/
public class AllState {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()-> {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(100L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("....");
}
});
// 觀察狀態
Thread.State state = t.getState();
// 新創建處理新生狀態,state為NEW
System.out.println(state.toString());
t.start();
state = t.getState();
// 線程就緒,就緒狀態到運行狀態是CPU控制的,所以一般調用了線程的 start()方法後,
// state為RUNNABLE
System.out.println(state.toString());
for (; state != Thread.State.TERMINATED;) {
Thread.sleep(200L);
// 線程阻塞 state為TIMED_WAITING
state = t.getState();
System.out.println(state.toString());
}
// 線程運行結束 state為TERMINATED
state = t.getState();
System.out.println(state.toString());
}
}
線程優先順序
在多個線程同時執行的時候,線程調度器會根據優先順序,優先調用級別高的線程。(只是決定優先調用,不代表先後順序。)
- MAX_PRIORITY(10,線程可以擁有的最大優先順序)
- MIN_PRIORITY(1,線程可以擁有的最小優先順序)
- NORM_PRIORITY(5,分配給線程的預設優先順序)
舉例:
public class PriorityTest {
public static void main(String[] args) {
TestPriority tp = new TestPriority();
Thread t = new Thread(tp);
t.setPriority(Thread.MAX_PRIORITY);
t.start();
Thread t1 = new Thread(tp);
t1.setPriority(Thread.MAX_PRIORITY);
t1.start();
Thread t2 = new Thread(tp);
t2.setPriority(Thread.MAX_PRIORITY);
t2.start();
Thread t3 = new Thread(tp);
t3.setPriority(Thread.MIN_PRIORITY);
t3.start();
Thread t4 = new Thread(tp);
t4.setPriority(Thread.MIN_PRIORITY);
t4.start();
Thread t5 = new Thread(tp);
t5.setPriority(Thread.MIN_PRIORITY);
t5.start();
}
}
class TestPriority implements Runnable {
@Override
public void run() {
Thread thread = Thread.currentThread();
String threadName = thread.getName();
int threadPriority = thread.getPriority();
System.out.println(threadName + "--->" + threadPriority);
Thread.yield();
}
}
如果優先順序高的話,那麼先執行的概率會大一些,但不是絕對。
守護線程
守護線程是為用戶線程服務的,JVM停止不用等待守護線程執行完畢。
- 線程分為用戶線程和守護線程
- 虛擬機必須確保用戶線程執行完畢
- 虛擬機不用等待守護線程執行完畢
- 如後臺記錄操作日誌、監控記憶體使用等
預設情況下,線程都是用戶線程,虛擬機等待所有用戶線程執行完畢才會停止。
public class DaemonTest {
public static void main(String[] args) {
God god = new God();
You you = new You();
Thread t = new Thread(god);
// 將用戶線程調整為守護線程(預設為false)
t.setDaemon(true);
t.start();
new Thread(you).start();
// God調整為守護線程時,當You線程結束後,God也隨即結束。
}
}
// 我
class You implements Runnable {
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
System.out.println("幸福的生活著。。。");
}
System.out.println("Game Over!");
}
}
// 上帝
class God implements Runnable {
@Override
public void run() {
for (; ;) {
System.out.println("上帝守護你。。。");
}
}
}
其它常用方法
- isAlive(),判斷當前線程是否活著,即線程是否還未終止
- setName(),給當前線程賦名字
- getName(),獲取當前線程名字
- currentThread(),取得當前運行的線程對象,或者說是獲取自己本身
舉例 :
public class OtherMethod {
public static void main(String[] args) throws Exception {
Thread thread = Thread.currentThread();
System.out.println(thread);
System.out.println(thread.isAlive());
System.out.println(thread.getName());
MyInfo myInfo = new MyInfo("張三");
Thread t = new Thread(myInfo);
t.setName("李四");
t.start();
Thread.sleep(2000L);
System.out.println(t.isAlive());
}
}
class MyInfo implements Runnable {
private String name;
MyInfo(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "--->" + name);
}
}
線程同步
講到線程同步就要理解什麼是併發了,併發就是同一個對象同時被多個線程訪問。一旦出現併發就會線程不安全最終就可能導致數據結果不准確等一些問題。
線程同步本質就是一種等待機制,多個需要同時訪問此對象的線程進入這個對象的 等待池形成隊列 ,等待前面的線程使用完畢後,先一個線程再使用。
如何保證線程安全:
隊列形式,排隊執行,通過鎖的方式,標誌某個線程正在占用某個資源,處理完畢後釋放鎖讓隊列中其它線程繼續執行。
synchronized
synchronized 關鍵字包括兩種用法,synchronized 方法 和 synchronized 代碼塊
synchronized 方法控制對“成員變數|類變數”對象的訪問,每個對象對應一把鎖,每個 synchronized 方法都必須獲得調用該方法的對象的鎖才能執行,否則所屬線程阻塞,方法一旦執行,就獨占該鎖,直到從該方法返回時才將鎖釋放,此後被阻塞的線程方能獲得該鎖,重新進入可執行狀態。
synchronized方法
/**
* 模擬搶票
* 保證線程安全,併發時保證數據的正確性、效率儘可能高
*/
public class SyncWeb12306 implements Runnable {
// 票數
private int ticketNums = 10;
private boolean flag = true;
@Override
public void run() {
for (; ; ) {
if (!flag) {
break;
}
test();
}
}
// 使用 synchronized 關鍵字 鎖住資源(this)
public synchronized void test() {
if (ticketNums <= 0) {
this.flag = false;
return;
}
try {
Thread.sleep(200L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "--->" + ticketNums--);
}
public static void main(String[] args) {
// 一份資源
SyncWeb12306 web = new SyncWeb12306();
// 多個線程同時訪問
new Thread(web, "張三").start();
new Thread(web, "李四").start();
new Thread(web, "王五").start();
}
}
synchronized代碼塊
/**
* 模擬併發操作取錢
*/
public class SyncDrawMoney {
int money; // 金額
String name; // 名稱
SyncDrawMoney(int money, String name) {
this.money = money;
this.name = name;
}
public static void main(String[] args) {
SyncDrawMoney drawMoney = new SyncDrawMoney(100, "買車");
SyncDrawing you = new SyncDrawing(drawMoney, 100, "張三");
SyncDrawing wife = new SyncDrawing(drawMoney, 100, "張三老婆");
you.start();
wife.start();
}
}
// 模擬取款
class SyncDrawing extends Thread {
SyncDrawMoney drawMoney; // 取錢賬戶
int drawingMoney; // 取的錢數
int packetTotal; // 口袋裡的錢
public SyncDrawing(SyncDrawMoney drawMoney, int drawingMoney, String name) {
super(name);
this.drawMoney = drawMoney;
this.drawingMoney = drawingMoney;
}
@Override
public void run() {
test();
}
private void test() {
if (drawMoney.money > 0) {
// 鎖住具體要操作的對象
synchronized (drawMoney) {
if (drawMoney.money < drawingMoney) {
System.out.println(Thread.currentThread().getName() + "取" + drawingMoney + "但是,餘額還有" + drawMoney.money);
System.out.println("錢不夠了。。。");
return;
}
if (drawMoney.money - drawingMoney < 0) {
System.out.println(Thread.currentThread().getName() + "取" + drawingMoney + "但是,餘額還有" + drawMoney.money);
System.out.println("沒錢了。。。");
return;
}
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
drawMoney.money -= drawingMoney;
packetTotal += drawingMoney;
System.out.println(Thread.currentThread().getName() + "--->賬戶餘額為:" + drawMoney.money);
System.out.println(Thread.currentThread().getName() + "--->口袋錢為:" + packetTotal);
}
} else {
System.out.println("賬戶沒錢了。。。");
}
}
}
性能分析
public class PropertyWeb12306 implements Runnable {
// 票數
private Integer ticketNums = 10;
private boolean flag = true;
@Override
public void run() {
for (; ; ) {
if (!flag) {
break;
}
test3();
}
}
// 儘可能鎖定合理範圍(合理範圍不是指代碼,指的是數據的完整性)
void test3() {
if (ticketNums <= 0) {
this.flag = false;
return;
}
synchronized (this) {
if (ticketNums <= 0) {
this.flag = false;
return;
}
try {
Thread.sleep(200L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "--->" + ticketNums--);
}
}
// 同步代碼塊範圍小,鎖不住,導致線程不安全
void test2() {
synchronized (this) {
if (ticketNums <= 0) {
this.flag = false;
return;
}
}
try {
Thread.sleep(200L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "--->" + ticketNums--);
}
// 同步代碼塊,鎖住當前對象,範圍大,雖然實現了線程安全,但是性能低下。
void test1() {
synchronized (this) {
if (ticketNums <= 0) {
this.flag = false;
return;
}
try {
Thread.sleep(200L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "--->" + ticketNums--);
}
}
// 線程安全,同步
synchronized void test() {
if (ticketNums <= 0) {
this.flag = false;
return;
}
try {
Thread.sleep(200L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "--->" + ticketNums--);
}
public static void main(String[] args) {
// 一份資源
PropertyWeb12306 web = new PropertyWeb12306();
// 多個線程同時訪問
new Thread(web, "張三").start();
new Thread(web, "李四").start();
new Thread(web, "王五").start();
}
}
註意:實際開發中,儘量多用同步代碼塊,少用同步方法
線程同步小案例
購買電影票小案例
/**
* 快樂影院
*/
public class HappyCinema {
public static void main(String[] args) {
// 可用位置
List<Integer> available = new ArrayList<>();
available.add(1);
available.add(2);
available.add(4);
available.add(6);
available.add(5);
available.add(7);
available.add(3);
// 影院
Cinema c = new Cinema(available, "夜上海影院");
// 來兩個顧客
List<Integer> seats01 = new ArrayList<>();
seats01.add(1);
seats01.add(2);
seats01.add(4);
new Thread(new Customer(c, seats01), "張三").start();
List<Integer> seats02 = new ArrayList<>();
seats02.add(4);
seats02.add(6);
seats02.add(7);
new Thread(new Customer(c, seats02), "李四").start();
}
}
/**
* 顧客
*/
class Customer implements Runnable {
// 電影院
Cinema cinema;
// 需要幾個位置
List<Integer> seats;
Customer(Cinema cinema, List<Integer> seats) {
this.cinema = cinema;
this.seats = seats;
}
@Override
public void run() {
Thread thread = Thread.currentThread();
synchronized (cinema) {
boolean flag = cinema.bookTickets(this.seats);
if (flag) {
System.out.println(thread.getName() + "-->購票成功。。。" + "位置為:" + this.seats);
} else {
System.out.println(thread.getName() + "-->購票失敗***" + "位置不夠");
}
}
}
}
/**
* 影院
*/
class Cinema {
List<Integer> available; // 可用位置
String name; // 名稱
Cinema(List<Integer> available, String name) {
this.available = available;
this.name = name;
}
// 購票
boolean bookTickets(List<Integer> seats) {
System.out.println("歡迎光臨【" + this.name + "】當前可用的位置為" + available);
System.out.println(Thread.currentThread().getName() + "-->想要購買" + seats);
List<Integer> copy = new ArrayList<>(available);
// 相減
copy.removeAll(seats);
// 判斷大小
if (seats.size() != (available.size() - copy.size())) {
return false;
}
available = copy;
return true;
}
}
購買火車票小案例
/**
* 購買火車票 線程安全版
*/
public class Happy12306 {
public static void main(String[] args) {
Web12306 web = new Web12306(4, "杭州");
new Passenger(web, "張三", 2).start();
new Passenger(web, "李四", 1).start();
}
}
// 乘客
class Passenger extends Thread {
int seats;
public Passenger(Runnable target, String name, int seats) {
super(target, name);
this.seats = seats;
}
}
// 火車票網站
class Web12306 implements Runnable {
int available; // 可用位置
String name; // 名稱
public Web12306(int available, String name) {
this.available = available;
this.name = name;
}
@Override
public void run() {
Passenger p = (Passenger) Thread.currentThread();
boolean flag = this.bookTickets(p.seats);
if (flag) {
System.out.println("出票成功" + p.getName() + "位置為:" + p.seats);
} else {
System.out.println("出票失敗" + p.getName() + "位置不夠");
}
}
// 購票
synchronized boolean bookTickets(int seats) {
System.out.println("可用位置為:" + this.available);
if (seats > this.available) {
return false;
}
available -= seats;
return true;
}
}
死鎖
多個線程各自占有一些共用資源,並且互相等待其他線程占有的資源才能進行,而導致兩個或者多個線程都在等待對方釋放資源,都停止執行的情形。某一個同步塊同時擁有“兩個以上對象的鎖”時,就可能會發生“死鎖”的問題。
通俗一些就是:你給錢,我給貨。我想讓你先給貨,你想讓我先給錢,一直僵持著。
過多的同步有可能會造成死鎖
如何避免:
不要再同步一個代碼塊同時持有多個對象的鎖
併發協作
生產者消費者模式
本質是一個線程同步問題,生產者和消費者共用同一個資源,並且生產者和消費者之間相互依賴,互為條件。
- 對於生產者而言,沒有生產產品之前,要通知消費者等待。而生產了之後,需要馬上通知消費者
- 對於消費者而言,在消費之後,要通知生產者已經消費結束,需要繼續生產新產品以供消費
- 在生產者消費者問題中,僅有 synchronized 時不夠的,還需要等待和通知等操作
- synchronized 可阻止併發更新同一個共用資源,實現同步
- synchronized 不能用來實現不同線程之間的消息傳遞(通信)
Java提供了3個方法解決線程之間的通信問題
方法名 | 作用 |
---|---|
final void wait() | 表示線程一直等待,直到其他線程通知,與sleep()不同,會釋放鎖 |
final void wait(long timeout) | 指定等待的毫秒數 |
final void notifiy() | 喚醒一個處於等待狀態的線程 |
final void notifiyAll() | 喚醒同一個對象上所有調用wait()方法的線程,優先順序別高的線程優先調度 |
註意:以上方法都只能在同步方法或者同步代碼塊中使用,否則會拋出異常
實現方式一:管程法
/**
* 協作模型:
* 生產者消費者模式方式一:管程法
*/
public class CoTest01 {
public static void main(String[] args) {
SyncContainer container = new SyncContainer();
// 生產者
new Productor(container).start();
// 消費者
new Consumer(container).start();
}
}
// 生產者
class Productor extends Thread {
SyncContainer container;
public Productor(SyncContainer container) {
this.container = container;
}
@Override
public void run() {
// 開始生產
for (int i = 0; i < 100; i++) {
System.out.println("生產第" + i + "個饅頭。。。");
container.push(new SteamedBun(i));
}
}
}
// 消費者
class Consumer extends Thread {
SyncContainer container;
public Consumer(SyncContainer container) {
this.container = container;
}
@Override
public void run() {
// 開始消費
for (int i = 0; i < 100; i++) {
System.out.println("消費第" + container.pop().id + "個饅頭。。。");
}
}
}
// 緩衝區
class SyncContainer {
SteamedBun[] steamedBuns = new SteamedBun[10]; // 存儲容器
int count = 0; // 計數器
// 存儲 生產
synchronized void push(SteamedBun steamedBun) {
// 不能生產
if (count == steamedBuns.length) {
try {
this.wait(); // 線程阻塞 消費者通知生產解除
} catch (InterruptedException e) {
e.printStackTrace();
}
}
steamedBuns[count] =steamedBun;
count++;
// 存在數據了,可以通知消費
this.notifyAll();
}
// 獲取 消費
synchronized SteamedBun pop() {
if (count == 0) {
try {
this.wait(); // 線程阻塞 生產者通知消費,解除阻塞
} catch (InterruptedException e) {
e.printStackTrace();
}
}
count--;
this.notifyAll(); // 存在空間 喚醒所有
return steamedBuns[count];
}
}
// 數據,舉例為饅頭
class SteamedBun {
int id;
public SteamedBun(int id) {
this.id = id;
}
}
實現方式二:信號燈法
/**
* 協作模型:
* 生產者消費者模式方式一:信號燈法
* 藉助標註位
*/
public class CoTest02 {
public static void main(String[] args) {
Tv tv = new Tv();
new Player(tv).start();
new Watcher(tv).start();
}
}
// 生產者 演員
class Player extends Thread {
Tv tv;
public Player(Tv tv) {
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
if ((i % 2) == 0) {
this.tv.play("奇葩說。。。");
} else {
this.tv.play("直播廣告。。。");
}
}
}
}
// 消費者觀眾
class Watcher extends Thread {
Tv tv;
public Watcher(Tv tv) {
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
tv.watch();
}
}
}
// 同一個資源 電視
class Tv {
String voice;
boolean flag = true; // 信號燈,如果為 true 演員表演觀眾觀看,否則觀眾觀看演員等待
// 表演
synchronized void play(String voice) {
// 演員等待
if (!flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.voice = voice;
// 表演
System.out.println("表演了:" + this.voice);
this.notifyAll(); // 喚醒
this.flag = !this.flag;
}
// 觀看
synchronized void watch() {
// 觀眾等待
if (flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 觀看
System.out.println("聽到了:" + this.voice);
this.notifyAll(); // 喚醒
this.flag = !this.flag;
}
}