一、線程間通信
- 1.定義
- 線程間通信就是多個線程操作同一資源,但是操作的動作不同。
- 2.等待喚醒機制
- 等待喚醒機制,是由
wait()
,notify()
或notifyAll()
等方法組成。對於有些資源的操作,需要一個線程完成一步,進入等待狀態,將CPU執行權交由另一個線程,讓它完成下一步的操作,如此交替進行。這個過程中,一個線程需要在完成一步操作後,先通知(notify()
)另一個線程運行,再等待(wait()
),進入凍結狀態,以此類推。等待中的線程,都儲存在系統線程池中,等待這被notify()
喚醒。以下代碼,通過等待喚醒機制,實現了生產一個披薩,消費一個披薩:
package com.heisejiuhuche;
public class ProductionConsumptionModel {
public static void main(String[] args) {
Pizza pizza = new Pizza();
new Thread(new Producer(pizza)).start();
new Thread(new Consumer(pizza)).start();
}
}
class Pizza {
private String pizza;
private int count = 1;
//包子存在與否的旗標,false代表沒有pizza,true代表有
private boolean flag = false;
public synchronized void producePizza(String pizza) {
if (flag) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.pizza = pizza + "---" + count++;
System.out.println(Thread.currentThread().getName() + "-生產-----"
+ this.pizza);
flag = true;
notify();
}
public synchronized void consumePizza() {
//如果沒有pizza,則執行生產包子的代碼
if (!flag) {
try {
//如果有pizza,則線程等待
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()
+ "-消費--------------" + this.pizza);
//如果沒有pizza,生產完之後,將flag設爲true
flag = false;
//線程進入凍結狀態之前,通知另一線程開始啓動,消費pizza
notify();
}
}
class Producer implements Runnable {
private Pizza pizza;
Producer(Pizza pizza) {
this.pizza = pizza;
}
public void run() {
while (true) {
pizza.producePizza("pizza");
}
}
}
class Consumer implements Runnable {
private Pizza pizza;
Consumer(Pizza pizza) {
this.pizza = pizza;
}
public void run() {
while (true) {
pizza.consumePizza();
}
}
}
程序運行部分結果如下:
Thread-1-消費--------------pizza---20609
Thread-0-生產-----pizza---20610
Thread-1-消費--------------pizza---20610
Thread-0-生產-----pizza---20611
Thread-1-消費--------------pizza---20611
- 3.Object類中的wait等方法
wait()
等多線程同步等待喚醒機制中的方法,被定義在Object
類中是因爲: 首先,在等待喚醒機制中,無論是等待操作,還是喚醒操作,都必須標識出等待的這個線程和被喚醒的這個線程鎖持有的鎖;表現爲代碼是:
鎖.wait()
;鎖.notify()
;而這個鎖,由synchronized
關鍵字格式可知,可以是任意對象;那麼,可以被任意對象調用的方法,一定是定義在了Object
類當中。wait()
,notify()
,notifyAll()
這些方法都被定義在了Object
類中,因爲這些方法是要使用在多線程同步的等待喚醒機制當中,必須具備能被任意對象調用的特性。所以,這些方法要被定義在Object
類中。- 4、生產者消費者模型
- 在實際生產時,會有多個線程負責生產,多個線程負責消費;那麼在上述代碼中啓動新線程,來模擬多線程生產消費的情況。
示例代碼:
package com.heisejiuhuche;
public class ProductionConsumptionModel {
public static void main(String[] args) {
Pizza pizza = new Pizza();
//兩個線程負責生產,兩個線程負責消費
new Thread(new Producer(pizza)).start();
new Thread(new Producer(pizza)).start();
new Thread(new Consumer(pizza)).start();
new Thread(new Consumer(pizza)).start();
}
}
用這樣的方式,運行會出現如下結果:
Thread-0-生產-----pizza---198
Thread-1-生產-----pizza---199
Thread-2-消費--------------pizza---199
生產了兩個披薩,但只消費了一個。現在0,1線程負責生產,2,3線程負責消費,原因推斷:
1)當0線程生產完一個披薩,進入凍結;
2)1線程判斷有披薩,進入凍結;
3)2線程消費一個披薩,喚醒0線程,進入凍結;
4)3線程判斷沒披薩,進入凍結;
5)現在出於運行狀態的只有0線程,0線程生產一個披薩,喚醒1線程(1線程是線程池中第一個線程),進入凍結;
6)1線程又生產了一個披薩
這導致了生產兩個,只消費一個的問題。這個問題的發生是因爲,第5
步0
線程喚醒1
線程的時候,由於1
線程的等待代碼在if
語句中,1
線程醒了之後,不需要再判斷flag
的值所導致。如果1
線程被喚醒,還要繼續判斷flag的值,就不會產生這個情況。因此,要將if判斷,改爲while
循環,讓線程被喚醒之後,再次判斷flag
的值。
示例代碼:
while (flag) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
每次被喚醒,都要判斷flag的值。代碼運行結果如下:
Thread-0-生產-----pizza---1
Thread-2-消費--------------pizza---1
Thread-0-生產-----pizza---2
Thread-3-消費--------------pizza---2
程序出現了無響應,因爲使用while
循環,可能會出現所有線程全部進入凍結狀態的情況。要解決這個問題,必須用到另一個方法notifyAll();
喚醒所有線程。由於用了while
循環,所有線程被喚醒之後第一件事是判斷flag
的值,所以不會再出現多生產或多消費問題。至此,程序運行正常。
示例代碼:
public synchronized void consumePizza() {
while(!flag) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()
+ "-消費--------------" + this.pizza);
//如果沒有pizza,生產完之後,將flag設爲true
flag = false;
//線程進入凍結狀態之前,喚醒所有其他線程
notifyAll();
}
}
程序運行部分結果:
Thread-2-消費--------------pizza---198
Thread-0-生產-----pizza---199
Thread-3-消費--------------pizza---199
Thread-1-生產-----pizza---200
Thread-3-消費--------------pizza---200
二、jdk5新特性
- 1.概述
- jdk5開始,提供了多線程同步的升級解決方案。將
synchronized
關鍵字,替換成Lock接口
;將Object
對象,替換爲Condition對象
;將wai()
,notify()
,notifyAll()
方法,替換爲await()
,signal()
,signalAll()
方法。一個鎖,可以對應多個Condition對象
。這個特性的出現,可以讓多線程在喚醒其他線程時,不必喚醒本方的線程,只喚醒對方線程。例如在生產者消費者模型中,使用Lock
和Condition
類,可以實現只喚醒消費者線程,或只喚醒生產者線程。- 2.Lock接口和Condition接口
- 1)Lock接口已知實現類中,有ReentrantLock類。這個子類可以用來實例化,創建ReentrantLock對象
ReentrantLock lock = new ReentrantLock();
- 2)Condition接口的實例可以通過newCondition()方法獲得
Condition conditon = Lock.newCondition();
- 3)一個Lock對象可以對應多個Condition對象
Condition condition1 = Lock.newCondition();
Condition condition2 = Lock.newCondition();
- 3.新特性應用
- 將此新特性應用在消費者生產者模型中,實現只喚醒對方線程。
修改之後的Pizza類代碼如下:
class Pizza {
private String pizza;
private int count = 1;
private boolean flag = false;
//獲取Lock和Condition對象
private final ReentrantLock lock = new ReentrantLock();
//分別指定生產者和消費者的Condition對象
private final Condition conditionPro = lock.newCondition();
private final Condition conditionCon = lock.newCondition();
public void producePizza(String pizza) {
//上鎖
lock.lock();
try {
while (flag) {
//如果有披薩,線程凍結
conditionPro.await();
}
this.pizza = pizza + "---" + count++;
System.out.println(Thread.currentThread().getName() + "-生產-----"
+ this.pizza);
flag = true;
//只喚醒消費者線程中的一個
conditionCon.signal();
} catch(InterruptedException e) {
e.printStackTrace();
} finally {
//這是一定要執行的代碼,解鎖
lock.unlock();
}
}
public void consumePizza() {
lock.lock();
try {
while(!flag) {
conditionCon.await();
}
System.out.println(Thread.currentThread().getName()
+ "-消費--------------" + this.pizza);
flag = false;
conditionPro.signal();
} catch(InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
分別創建的conditionPro
和conditionCon
對象,用於實現只喚醒對方線程,代碼更優。
三、停止線程
- 1.線程停止原理
stop()
方法已經過時,停止的唯一標準就是run()
方法結束。開啓多線程運行,運行代碼通常都是循環結構,只要控制住循環,就可以讓run()
方法結束,就可以讓線程結束。注意:
當線程處於凍結狀態,無法讀取控制循環的標記,線程就不會結束。
- 2.interrupt()方法
- 將處於凍結狀態的線程,強制恢復到運行狀態。
interrupt()
方法是在清除線程的凍結狀態。示例代碼:
package com.heisejiuhuche;
public class InterruptTest {
public static void main(String[] args) {
int x = 0;
Interrupt inter = new Interrupt();
Thread t1 = new Thread(inter);
Thread t2 = new Thread(inter);
t1.start();
t2.start();
while(true) {
System.out.println(Thread.currentThread().getName() + "run....");
if(x++ == 60) {
//強制t1 t2恢復運行狀態,拋出異常
t1.interrupt();
t2.interrupt();
break;
}
}
System.out.println("over");
}
}
class Interrupt implements Runnable {
//循環控制變量
private boolean flag = true;
public synchronized void run() {
while(flag) {
try {
//讓t1 t2進入凍結狀態
this.wait();
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "Interrupt Exception.....");
//處理完異常,改變flag的值,下次判斷時,結束循環
changeFlag();
}
System.out.println(Thread.currentThread().getName() + " Interrupt run.....");
}
}
public void changeFlag() {
flag = false;
}
}
如果不調用t1
和t2
線程的interrupt()
方法,程序會無響應,因爲兩個線程都處於凍結狀態,無法繼續運行。
上述程序運行結果:
mainrun....
over
Thread-1Interrupt Exception.....
Thread-1 Interrupt run.....
Thread-0Interrupt Exception.....
Thread-0 Interrupt run.....
四、Thread類其他方法
- 1.setDaemon()方法
- 將該線程標記爲守護線程或用戶線程。當正在運行的線程都是守護線程時,Java虛擬機退出。該方法必須在啓動線程前調用。守護線程可以理解爲後臺線程。後臺線程開啓後,會和前臺線程(一般線程)一起搶奪CPU資源;當所有前臺線程結束運行後,後臺線程自動結束。可以理解爲,後臺線程依賴前臺線程的運行。
示例代碼:
package com.heisejiuhuche;
public class InterruptTest {
public static void main(String[] args) {
int x = 0;
Interrupt inter = new Interrupt();
Thread t1 = new Thread(inter);
Thread t2 = new Thread(inter);
t1.setDaemon(true);
t2.setDaemon(true);
t1.start();
t2.start();
}
在啓動兩個線程前,將兩個線程設置爲守護線程,其他代碼不變;那麼這兩個線程依賴主線程運行;雖然這兩個線程都處於凍結狀態,但是當主線程運行完畢,這兩個守護進程隨之結束。
- 2.join()方法
- 調用
join()
方法的線程,在申請CPU執行權。之前擁有CPU執行權的線程,將轉入凍結狀態,等調用join()
方法的線程執行完畢,再轉回運行狀態。示例代碼:
package com.heisejiuhuche;
public class JoinTest {
public static void main(String[] args) {
Join j = new Join();
Thread t1 = new Thread(j);
Thread t2 = new Thread(j);
t1.start();
try {
//主線程將CPU執行權交給t1線程,自己轉入凍結
//等待t1線程執行完畢,主線程再運行
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
for(int x = 0; x < 100; x++) {
System.out.println(Thread.currentThread().getName() + "***" + x);
}
}
}
class Join implements Runnable {
public void run() {
for(int x = 0; x < 100; x++) {
System.out.println(Thread.currentThread().getName() + "---" + x);
}
}
}
程序在啓動t1
線程之後,主線程先等待t1
線程打印完100
個數;主線程再繼續和t2
線程交替打印100
個數。
- 3.yield()方法
- 調用
yield()
方法的線程,會臨時釋放執行權,可以達到線程均衡運行的效果。示例代碼:
package com.heisejiuhuche;
public class YieldTest {
public static void main(String[] args) {
Yield j = new Yield();
Thread t1 = new Thread(j);
Thread t2 = new Thread(j);
t1.start();
t2.start();
for(int x = 0; x < 100; x++) {
System.out.println(Thread.currentThread().getName() + "***" + x);
}
}
}
class Yield implements Runnable {
public void run() {
for(int x = 0; x < 100; x++) {
System.out.println(Thread.currentThread().getName() + "---" + x);
Thread.yield();
}
}
}
程序運行部分結果:
Thread-1---51
main***79
Thread-0---46
main***80
Thread-1---52
main***81
Thread-0---47
三個線程均衡執行。
五、多線程開發應用
- 多線程應用在程序中的運算需要同時進行的時候,可以提高程序運行的效率。例如,
main()
方法中有三個循環需要執行,如果是單線程,第二個循環要等待第一個循環執行完才能執行,第三個循環要等第二個循環執行完,如此一來,程序運行效率低下。此時,就可以運用多線程,讓三個循環同時運行。示例代碼:
package com.heisejiuhuche;
public class ThreadApplycation {
public static void main(String[] args) {
//主線程執行
for(int x = 0; x < 100; x ++) {
System.out.println("Main thread running ...");
}
//匿名線程執行
new Thread() {
public void run() {
for(int x = 0; x < 100; x++) {
System.out.println("Anonymous thread running ...");
}
}
}.start();
//線程r執行
Runnable r = new Runnable() {
public void run() {
for(int x = 0; x < 100; x ++) {
System.out.println("r thread running ...");
}
}
};
new Thread(r).start();
}
}
讓主線程,匿名線程和r
線程,同時開始運算。