【搞定Java併發程式設計】第4篇:多執行緒概述~下篇
上一篇:多執行緒上篇:https://blog.csdn.net/pcwl1206/article/details/84837530
目 錄:
7、sleep()、wait()、yield()和join()方法的區別
1、等待/喚醒機制
一個執行緒修改了一個物件的值,而另一個執行緒感知到了變化,然後進行相應的操作,整個過程開始於一個執行緒,而最終執行又是另一個執行緒。前者是“生產者”,後者是“消費者”,在功能層面實現瞭解耦。
1.1、等待/喚醒機制相關的方法
在Object.java中,定義了wait()、notify()和notifyAll()等方法。wait()的作用是讓當前執行緒進入等待狀態,同時,wait()也會讓當前執行緒釋放它所持有的鎖。而notify()和notifyAll()的作用,則是喚醒當前物件上的等待執行緒;notify()是喚醒單個執行緒,而notifyAll()是喚醒所有的執行緒。
Object類中關於等待/喚醒的API詳細資訊如下:
- notify():喚醒在此物件監視器上等待的單個執行緒,使其從wait()方法返回,而返回的前提是該執行緒獲取到了物件的鎖;
- notifyAll():
喚醒在此物件監視器上等待的所有執行緒;- wait():讓當前執行緒處於“等待(阻塞)狀態”,“直到其他執行緒呼叫此物件的 notify() 方法或 notifyAll() 方法”,當前執行緒被喚醒(進入“就緒狀態”)。需要注意的是:呼叫wait()方法後,會釋放物件的鎖;
- wait(long timeout) :讓當前執行緒處於“等待(阻塞)狀態”,“直到其他執行緒呼叫此物件的 notify() 方法或 notifyAll() 方法,或者超過指定的時間量”,當前執行緒被喚醒(進入“就緒狀態”)。
- wait(long timeout, int nanos):讓當前執行緒處於“等待(阻塞)狀態”,“直到其他執行緒呼叫此物件的 notify() 方法或 notifyAll() 方法,或者其他某個執行緒中斷當前執行緒,或者已超過某個實際時間量”,當前執行緒被喚醒(進入“就緒狀態”)。其實就是對於超時時間更細粒度的控制;
- 等待/喚醒機制的含義:
等待/喚醒機制是指一個執行緒A呼叫了物件O的wait()方法進入等待狀態,而另一個執行緒B呼叫了物件O的notify()或者notifyAll()方法,執行緒A收到通知後從物件O的wait()方法返回,進而執行後續操作。
上訴兩個執行緒通過物件O來完成互動,而物件上的wait()和notify/notifyAll()的關係就如同開關訊號一樣,用來完成等待方和通知方之間的互動工作。
1.2、等待/喚醒機制的案例
說明:該案例轉發自:等待喚醒機制
任務:
- 當input發現Resource中沒有資料時,開始輸入,輸入完成後,叫output來輸出。如果發現有資料,就wait();
- 當output發現Resource中沒有資料時,就wait() ;當發現有資料時,就輸出,然後,叫醒input來輸入資料。
這個問題的關鍵在於,如何實現交替進行。
public class Resource{
private String name;
private String sex;
private boolean flag = false;
public synchronized void set(String name, String sex){
// 如果flag為true,證明Resource還沒有輸出,則進入等待狀態
if(flag){
try {
wait(); // 等待消費者消費
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 設定成員變數
this.name = name;
this.sex = sex;
System.out.println("輸入的是:The name is : " + name + "&& The sex is :" + sex);
// 設定之後,Resource中有值,相當於生產出了新的Resource物件,將flag設定為true
flag = true;
// 喚醒output執行緒,進行資料的寫出,即消費
this.notify();
}
public synchronized void get(){
// 如果沒有了Resource物件,則進入等待狀態
if(!flag){
try {
wait(); // 等待生產者生產
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// out執行緒將資料輸出
System.out.println("輸出的是:The name is : " + name + "&& The sex is :" + sex);
// 改變標記,以便input執行緒輸入資料
flag = false;
// 喚醒input執行緒,進行資料的寫入,即生產
this.notify();
}
}
public class Input implements Runnable {
private Resource res;
public Input(Resource res){
this.res = res;
}
@Override
public void run() {
int count = 0;
while(true){
if(count == 0){
res.set("Tom", "man"); // 生產資料
}else{
res.set("Lily", "woman");
}
// 在兩個資料之間進行切換
count = (count + 1) % 2;
}
}
}
public class Output implements Runnable {
private Resource res;
public Output(Resource res){
this.res = res;
}
@Override
public void run() {
while(true){
res.get(); // 消費資料
}
}
}
public class ResourceTest {
public static void main(String[] args) {
// 資源物件
Resource res = new Resource();
// 任務物件,同一個res
Input in = new Input(res);
Output out = new Output(res);
// 執行緒物件
Thread t1 = new Thread(in); // 輸入執行緒
Thread t2 = new Thread(out); // 輸出執行緒
// 開啟執行緒
t1.start();
t2.start();
}
}
執行結果:
上面的這個例子是典型的生產者和消費者案例。只不過是單生產者和單消費者案例。
- 幾點細節:
1、呼叫wait()、notify()和notifyAll()時需要先對呼叫物件加鎖;
2、呼叫wait()方法後,執行緒狀態由RUNNING變為WAITING,並將當前執行緒放置到物件的等待佇列;
3、notify()或notifyAll()方法呼叫後,等待執行緒依舊不會從wait()返回,需要呼叫notify()或notifyAll()的執行緒釋放鎖之後,等待執行緒才有機會從wait()返回;
4、notify()方法將等待佇列中的一個等待執行緒從等待佇列中移動到同步佇列中,而notifyAll()方法則是將等待佇列中所有的執行緒全部移動到同步佇列,被移動的執行緒狀態由WAITING變為BLOCKED;
5、從wait()方法返回的前提是獲得了呼叫物件的鎖。
6、等待/喚醒機制依託於同步機制,其目的就是確保等待執行緒從wait()方法返回時能夠感知到通知執行緒對變數做出的修改。
- 為什麼notify()、wait()等方法定義在了Object類中,而不是Thread類中呢?
Object中的wait(), notify()等函式,和synchronized一樣,會對“物件的同步鎖”進行操作。wait()會使“當前執行緒”等待,因為執行緒進入等待狀態,所以執行緒應該釋放它鎖持有的“同步鎖”,否則其它執行緒獲取不到該“同步鎖”而無法執行!
執行緒呼叫wait()之後,會釋放它鎖持有的“同步鎖”;而且,根據前面的介紹,我們知道:等待執行緒可以被notify()或notifyAll()喚醒。現在,請思考一個問題:notify()是依據什麼喚醒等待執行緒的?或者說,wait()等待執行緒和notify()之間是通過什麼關聯起來的?答案是:依據“物件的同步鎖”。
負責喚醒等待執行緒的那個執行緒(我們稱為“喚醒執行緒”),它只有在獲取“該物件的同步鎖”(這裡的同步鎖必須和等待執行緒的同步鎖是同一個),並且呼叫notify()或notifyAll()方法之後,才能喚醒等待執行緒。雖然,等待執行緒被喚醒;但是,它不能立刻執行,因為喚醒執行緒還持有“該物件的同步鎖”。必須等到喚醒執行緒釋放了“物件的同步鎖”之後,等待執行緒才能獲取到“物件的同步鎖”進而繼續執行。
總之,notify(), wait()依賴於“同步鎖”,而“同步鎖”是物件鎖持有的,並且每個物件有且僅有一個!這就是為什麼notify(), wait()等函式定義在Object類,而不是Thread類中的原因。
2、執行緒中斷
2.1、什麼是執行緒中斷?
執行緒中斷是執行緒的標誌位屬性。而不是真正終止執行緒,和執行緒的狀態無關。執行緒中斷過程表示一個執行中的執行緒,通過其他執行緒呼叫了該執行緒的 interrupt()
方法,使得該執行緒中斷標誌位屬性改變。
深入思考下,執行緒中斷不是去中斷了執行緒,恰恰是用來通知該執行緒應該被中斷了。具體是一個標誌位屬性,到底該執行緒生命週期是去終止,還是繼續執行,由執行緒根據標誌位屬性自行處理。
2.2、執行緒中斷操作
呼叫執行緒的 interrupt()
方法,根據執行緒不同的狀態會有不同的結果。
下面新建 InterruptedThread 物件,程式碼如下:
public class InterruptThread implements Runnable {
@Override
public void run() {
// 一直run
while(true){
// ...
}
}
public static void main(String[] args) throws InterruptedException {
Thread interruptedThread = new Thread(new InterruptThread(), "InterruptedThread");
interruptedThread.start();
TimeUnit.SECONDS.sleep(2);
interruptedThread.interrupt();
System.out.println("InterruptedThread interrupted is " + interruptedThread.isInterrupted());
TimeUnit.SECONDS.sleep(2);
}
}
執行結果:
程式碼詳解:
- 執行緒一直在執行狀態,沒有停止或者阻塞等
- 呼叫了interrupt()方法,中斷狀態置為 true,但不會影響執行緒的繼續執行
另一種情況,新建 InterruptedException 物件,程式碼如下:
public class InterruptException implements Runnable {
@Override
public void run() {
// 一直sleep
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
Thread interruptedThread = new Thread(new InterruptException(), "InterruptedThread");
interruptedThread.start();
TimeUnit.SECONDS.sleep(2);
// 中斷被阻塞狀態(sleep、wait、join 等狀態)的執行緒,會丟擲異常 InterruptedException
// 在丟擲異常 InterruptedException 前,JVM 會先將中斷狀態重置為預設狀態 false
interruptedThread.interrupt();
System.out.println("InterruptedThread interrupted is " + interruptedThread.isInterrupted());
TimeUnit.SECONDS.sleep(2);
}
}
執行結果:
程式碼詳解:
- 中斷被阻塞狀態(sleep、wait、join 等狀態)的執行緒,會丟擲異常 InterruptedException
- 丟擲異常 InterruptedException 前,JVM 會先將中斷狀態重置為預設狀態 false
執行緒中斷總結:
- 執行緒中斷,不是停止執行緒,只是改變一個執行緒的標誌位屬性;
- 如果執行緒狀態為被阻塞狀態(sleep、wait、join 等狀態),執行緒狀態退出被阻塞狀態,丟擲異常 InterruptedException,並重置中斷狀態為預設狀態 false;
- 如果執行緒狀態為執行狀態,執行緒狀態不變,繼續執行,中斷狀態置為 true。
3、執行緒終止
比如在 IDEA 中強制關閉程式,立即停止程式,不給程式釋放資源等操作,肯定是不正確的。執行緒終止也存在類似的問題,所以需要考慮如何終止執行緒?
上面提到的中斷狀態是執行緒的一個標識位,而中斷操作是一種簡便的執行緒間互動方式,而這種互動方式最適合用來取消或停止任務。除了中斷以外,還可以利用一個boolean變數來控制是否需要停止任務並終止該執行緒。
案例如下程式碼所示:
public class ThreadSafeStop {
public static void main(String[] args) throws Exception {
Runner run1 = new Runner();
Thread countThread = new Thread(run1, "CountThread");
countThread.start();
// 睡眠1秒後,通知CountThread中斷,並終止執行緒
TimeUnit.SECONDS.sleep(1);
countThread.interrupt();
Runner run2 = new Runner();
countThread = new Thread(run2,"CountThread");
countThread.start();
// 睡眠 1 秒,然後設定執行緒停止狀態,並終止執行緒
TimeUnit.SECONDS.sleep(1);
run2.stopSafely();
}
// Runner:靜態內部類
private static class Runner implements Runnable{
private long i;
// 執行緒狀態變數
private volatile boolean on = true;
@Override
public void run() {
while(on && !Thread.currentThread().isInterrupted()){
// 執行緒執行的具體邏輯
i++;
}
System.out.println("Count i = " + i);
}
public void stopSafely(){
on = false;
}
}
}
從上面程式碼可以看出,通過while(on && !Thread.currentThread().isInterrupted())程式碼來實現執行緒是否跳出執行邏輯,並終止。但是疑問點就來了,為啥需要on和isInterrupted()兩項一起呢?用其中一個方式不就行了嗎?答案是:
- 執行緒成員變數on通過 volatile 關鍵字修飾,達到執行緒之間可見,從而實現執行緒的終止。但當執行緒狀態為被阻塞狀態(sleep、wait、join 等狀態)時,對成員變數操作也阻塞,進而無法執行安全終止執行緒;
- 為了處理上面的問題,引入了isInterrupted()
;
只去解決阻塞狀態下的執行緒安全終止; - 兩者結合是真的沒問題了嗎?不是的,如果是網路 io 阻塞,比如一個 websocket 一直再等待響應,那麼直接使用底層的 close 。
4、執行緒休眠sleep
sleep() 的作用是讓當前執行緒休眠,即當前執行緒會從“執行狀態”進入到“休眠(阻塞)狀態”。sleep()會指定休眠時間,執行緒休眠的時間會大於/等於該休眠時間;線上程重新被喚醒時,它會由“阻塞狀態”變成“就緒狀態”,從而等待cpu的排程執行。
public class ThreadA extends Thread {
public ThreadA(String name) {
super(name);
}
public synchronized void run() {
try {
for (int i = 0; i < 6; i++) {
System.out.printf("%s: %d\n", this.getName(), i);
// i能被4整除時,休眠1000毫秒
if (i % 4 == 0)
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
ThreadA t1 = new ThreadA("t1");
t1.start();
}
}
執行結果:
結果說明:
程式比較簡單,在主執行緒main中啟動執行緒t1。t1啟動之後,當t1中的計算i能被4整除時,t1會通過Thread.sleep(1000)休眠1000毫秒。
5、執行緒讓步yield()
yield()的作用是讓步。它能讓當前執行緒由“執行狀態”進入到“就緒狀態”,從而讓其它具有相同優先順序的等待執行緒獲取執行權。但是,並不能保證在當前執行緒呼叫yield()之後,其它具有相同優先順序的執行緒就一定能獲得執行權;也有可能是當前執行緒又進入到“執行狀態”繼續執行!
public class Thread_Yield extends Thread {
public Thread_Yield(String name){
super(name);
}
public synchronized void run(){
for(int i = 0; i < 10; i++){
System.out.printf("%s [%d]:%d\n", this.getName(), this.getPriority(), i);
// i整除4時,呼叫yield()
if(i % 4 == 0){
Thread.yield();
}
}
}
public static void main(String[] args) {
Thread_Yield t1 = new Thread_Yield("t1");
Thread_Yield t2 = new Thread_Yield("t2");
t1.start();
t2.start();
}
}
某一次的執行結果:
結果說明:
“執行緒t1”在能被4整數的時候,並沒有切換到“執行緒t2”。這表明,yield()雖然可以讓執行緒由“執行狀態”進入到“就緒狀態”;但是,它不一定會讓其它執行緒獲取CPU執行權(即,其它執行緒進入到“執行狀態”),即使這個“其它執行緒”與當前呼叫yield()的執行緒具有相同的優先順序。
6、join()方法
join() 的作用是:讓“主執行緒”等待“子執行緒”結束之後才能繼續執行。
// 主執行緒
public class Father extends Thread {
public void run() {
Son s = new Son();
s.start();
s.join();
...
}
}
// 子執行緒
public class Son extends Thread {
public void run() {
...
}
}
說明:
上面的有兩個類Father(主執行緒類)和Son(子執行緒類)。因為Son是在Father中建立並啟動的,所以,Father是主執行緒類,Son是子執行緒類。
在Father主執行緒中,通過new Son()新建“子執行緒s”。接著通過s.start()啟動“子執行緒s”,並且呼叫s.join()。在呼叫s.join()之後,Father主執行緒會一直等待,直到“子執行緒s”執行完畢;在“子執行緒s”執行完畢之後,Father主執行緒才能接著執行。 這也就是我們所說的“join()的作用,是讓主執行緒會等待子執行緒結束之後才能繼續執行”!
- join()的原始碼(JDK1.7)
public final void join() throws InterruptedException {
join(0);
}
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
// 當millis等於0的時候,判斷子執行緒是否是活的
if (millis == 0) {
while (isAlive()) {
wait(0); // 如果是活的,就無限等待下去
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
7、sleep()、wait()、yield()和join()方法的區別
1.sleep()方法
1、在指定時間內讓當前正在執行的執行緒暫停執行,但不會釋放“鎖標誌”。不推薦使用。
2、sleep()使當前執行緒進入阻塞狀態,在指定時間內不會執行。
2.wait()方法
1、在其他執行緒呼叫物件的notify或notifyAll方法前,導致當前執行緒等待。執行緒會釋放掉它所佔有的“鎖標誌”,從而使別的執行緒有機會搶佔該鎖。
2、當前執行緒必須擁有當前物件鎖。如果當前執行緒不是此鎖的擁有者,會丟擲IllegalMonitorStateException異常。
3、喚醒當前物件鎖的等待執行緒使用notify或notifyAll方法,也必須擁有相同的物件鎖,否則也會丟擲IllegalMonitorStateException異常。
4、wait()和notify()必須在synchronized函式或synchronized block中進行呼叫。如果在non-synchronized函式或non-synchronized block中進行呼叫,雖然能編譯通過,但在執行時會發生IllegalMonitorStateException的異常。
3.yield方法
1、暫停當前正在執行的執行緒物件。
2、yield()只是使當前執行緒重新回到可執行狀態,所以執行yield()的執行緒有可能在進入到可執行狀態後馬上又被執行。
3、yield()只能使同優先順序或更高優先順序的執行緒有執行的機會。
4.join方法
1、讓“主執行緒”等待“子執行緒”結束之後才能繼續執行,即等待呼叫join方法的執行緒結束。
8、Daemon執行緒
Daemon執行緒是一種支援型執行緒,因為它主要被用作程式中後臺排程以及支援性工作,被稱為守護執行緒。
當一個Java虛擬機器中不存在非Daemon執行緒的時候,Java虛擬機器將會退出。但是需要說明的是虛擬機器退出時Daemon執行緒中的finally程式碼塊並不一定會執行。
可以通過呼叫Thread.setDemon(true)將執行緒設定為Daemon執行緒,但是必須線上程啟動前設定。
轉載宣告:
本文內容主要出自於以下文章:
1、Java多執行緒系列:https://www.cnblogs.com/skywang12345/p/java_threads_category.html
2、併發基礎與多執行緒基礎:https://blog.csdn.net/a724888/article/details/60867044
3、《Java併發程式設計的藝術》書中內容