併發程式設計基礎(下)

CoderBear發表於2019-05-05

書接上文。上文主要講了下執行緒的基本概念,三種建立執行緒的方式與區別,還介紹了執行緒的狀態,執行緒通知和等待,join等,本篇繼續介紹併發程式設計的基礎知識。

sleep

當一個執行的執行緒呼叫了Thread的sleep方法,呼叫執行緒會暫時讓出指定時間的執行權,在這期間不參與CPU的排程,不佔用CPU,但是不會釋放該執行緒鎖持有的監視器鎖。指定的時間到了後,該執行緒會回到就緒的狀態,再次等待分配CPU資源,然後再次執行。

我們有時會看到sleep(1),甚至還有sleep(0)這種寫法,肯定會覺得非常奇怪,特別是sleep(0),睡0秒鐘,有意義嗎?其實是有的,sleep(1),sleep(0)的意義就在於告訴作業系統立刻觸發一次CPU競爭。

讓我們來看看正在sleep的程式被中斷了,會發生什麼事情:

class MySleepTask implements Runnable{
    @Override
    public void run() {
        System.out.println("MyTask1");
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            System.out.println("中斷");
            e.printStackTrace();
        }
        System.out.println("MyTask2");
    }
}

public class Sleep {
    public static void main(String[] args) {
        MySleepTask mySleepTask=new MySleepTask();
        Thread thread=new Thread(mySleepTask);
        thread.start();
        thread.interrupt();
    }
}
複製程式碼

執行結果:

MyTask1
中斷
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at java.lang.Thread.sleep(Thread.java:340)
	at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
	at com.codebear.MySleepTask.run(Sleep.java:10)
	at java.lang.Thread.run(Thread.java:748)
MyTask2
複製程式碼

yield

我們知道執行緒是以時間片的機制來佔用CPU資源並執行的,正常情況下,一個執行緒只有把分配給自己的時間片用完之後,執行緒排程器才會進行下一輪的執行緒排程,當執行了Thread的yield後,就告訴作業系統“我不需要CPU了,你現在就可以進行下一輪的執行緒排程了 ”,但是作業系統可以忽略這個暗示,也有可能下一輪還是把時間片分配給了這個執行緒。

我們來寫一個例子加深下印象:

class MyYieldTask implements Runnable {
    @Override
    public void run() {
        for (int i = 10; i > 0; i--) {
            System.out.println("我是" + Thread.currentThread().getName() + ",我分配到了時間片");
        }
    }
}


public class MyYield {
    public static void main(String[] args) {
        Thread thread1 = new Thread(new MyYieldTask());
        thread1.start();

        Thread thread2 = new Thread(new MyYieldTask());
        thread2.start();
    }
}
複製程式碼

執行結果:

image.png

當然由於執行緒的特性,所以每次執行結果可能都不太相同,但是當我們執行多次後,會發現絕大多數的時候,兩個執行緒的列印都是比較平均的,我用完時間片了,你用,你用完了時間片了,我再用。

當我們呼叫yield後:

class MyYieldTask implements Runnable {
    @Override
    public void run() {
        for (int i = 10; i > 0; i--) {
            System.out.println("我是" + Thread.currentThread().getName() + ",我分配到了時間片");
            Thread.yield();
        }
    }
}


public class MyYield {
    public static void main(String[] args) {
        Thread thread1 = new Thread(new MyYieldTask());
        thread1.start();

        Thread thread2 = new Thread(new MyYieldTask());
        thread2.start();
    }
}
複製程式碼

執行結果:

image.png

當然在一般情況下,可能永遠也不會用到yield,但是還是要對這個方法有一定的瞭解。

sleep 和 yield 區別

當執行緒呼叫sleep後,會阻塞當前執行緒指定的時間,在這段時間內,執行緒排程器不會呼叫此執行緒,當指定的時間結束後,該執行緒的狀態為“就緒”,等待分配CPU資源。 當執行緒呼叫yield後,不會阻塞當前執行緒,只是讓出時間片,回到“就緒”的狀態,等待分配CPU資源。

死鎖

死鎖是指多個執行緒在執行的過程中,因為爭奪資源而造成的相互等待的現象,而且無法打破這個“僵局”。

死鎖的四個必要條件:

  • 互斥:指執行緒對於已經獲取到的資源進行排他性使用,即該資源只能被一個執行緒佔有,如果還有其他執行緒也想佔有,只能等待,直到佔有資源的執行緒釋放該資源。
  • 請求並持有:指一個執行緒已經佔有了一個資源,但是還想佔有其他的資源,但是其他資源已經被其他執行緒佔有了,所以當前執行緒只能等待,等待的同時並不釋放自己已經擁有的資源。
  • 不可剝奪:當一個執行緒獲取資源後,不能被其他執行緒佔有,只有在自己使用完畢後自己釋放資源。
  • 環路等待:即 T1執行緒正在等待T2佔有的資源,T2執行緒正在等待T3執行緒佔有的資源,T3執行緒又在等待T1執行緒佔有的資源。

要想打破“死鎖”僵局,只需要破壞以上四個條件中的任意一個,但是程式設計師可以干預的只有“請求並持有”,“環路等待”兩個條件,其餘兩個條件是鎖的特性,程式設計師是無法干預的。

聰明的你,一定看出來了,所謂“死鎖”就是“悲觀鎖”造成的,相對於“死鎖”,還有一個“活鎖”,就是“樂觀鎖”造成的。

守護執行緒與使用者執行緒

Java中的執行緒分為兩類,分別為 使用者執行緒和守護執行緒。在JVM啟動時,會呼叫main函式,這個就是使用者執行緒,JVM內部還會啟動一些守護執行緒,比如垃圾回收執行緒。那麼守護執行緒和使用者執行緒到底有什麼區別呢?當最後一個使用者執行緒結束後,JVM就自動退出了,而不管當前是否有守護執行緒還在執行。 如何建立一個守護執行緒呢?

public class Daemon {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
        });
        thread.setDaemon(true);
        thread.start();
    }
}
複製程式碼

只需要設定執行緒的daemon為true就可以。 下面來演示下使用者執行緒與守護執行緒的區別:

public class Daemon {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            while (true){}
        });

        thread.start();
    }
}
複製程式碼

當我們執行後,可以發現程式一直沒有退出:

image.png
因為這是使用者執行緒,只要有一個使用者執行緒還沒結束,程式就不會退出。

再來看看守護執行緒:

public class Daemon {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            while (true){}
        });
        thread.setDaemon(true);
        thread.start();
    }
}
複製程式碼

當我們執行後,發現程式立刻就停止了:

image.png
因為這是守護執行緒,當使用者執行緒結束後,不管有沒有守護執行緒還在執行,程式都會退出。

執行緒中斷

之所以把執行緒中斷放在後面,是因為它是併發程式設計基礎中最難以理解的一個,當然這也與不經常使用有關。現在就讓我們好好看看執行緒中斷。 Thread提供了stop方法,用來停止當前執行緒,但是已經被標記為過期,應該用執行緒中斷方法來代替stop方法。

interrupt

中斷執行緒。當執行緒A執行(非阻塞)時,執行緒B可以呼叫執行緒A的interrupt方法來設定執行緒A的中斷標記為true,這裡要特別注意,呼叫interrupt方法並不會真的去中斷執行緒,只是設定了中斷標記為true,執行緒A還是活的好好的。如果執行緒A被阻塞了,比如呼叫了sleep、wait、join,執行緒A會在呼叫這些方法的地方丟擲“InterruptedException”。 我們來做個試驗,證明下interrupt方法不會中斷正在執行的執行緒:

class InterruptTask implements Runnable {
    @Override
    public void run() {
        CopyOnWriteArrayList copyOnWriteArrayList = new CopyOnWriteArrayList();
        try {
            long start = System.currentTimeMillis();
            for (int i = 0; i < 150000; i++) {
                copyOnWriteArrayList.add(i);
            }
            System.out.println("結束了,時間是" + (System.currentTimeMillis() - start));
            System.out.println(Thread.currentThread().isInterrupted());
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}

public class InterruptTest {
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new InterruptTask());
        thread1.start();
        thread1.interrupt();
    }
}
複製程式碼

執行結果:

結束了,時間是7643
true
複製程式碼

在子執行緒中,我們通過一個迴圈往copyOnWriteArrayList裡面新增資料來模擬一個耗時操作。這裡要特別要注意,一般來說,我們模擬耗時操作都是用sleep方法,但是這裡不能用sleep方法,因為呼叫sleep方法會讓當前執行緒阻塞,而現在是要讓執行緒處於執行的狀態。我們可以很清楚的看到,雖然子執行緒剛執行,就被interrupt了,但是卻沒有丟擲任何異常,也沒有讓子執行緒終止,子執行緒還是活的好好的,只是最後列印出的“中斷標記”為true。

如果沒有呼叫interrupt方法,中斷標記為false:

class InterruptTask implements Runnable {
    @Override
    public void run() {
        CopyOnWriteArrayList copyOnWriteArrayList = new CopyOnWriteArrayList();
        try {
            long start = System.currentTimeMillis();
            for (int i = 0; i < 500; i++) {
                copyOnWriteArrayList.add(i);
            }
            System.out.println("結束了,時間是" + (System.currentTimeMillis() - start));
            System.out.println(Thread.currentThread().isInterrupted());
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}

public class InterruptTest {
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new InterruptTask());
        thread1.start();
    }
}
複製程式碼

執行結果:

結束了,時間是1
false
複製程式碼

在介紹sleep,wait,join方法的時候,大家已經看到了,如果中斷呼叫這些方法而被阻塞的執行緒會丟擲異常,這裡就不再演示了,但是還有一點需要注意,當我們catch住InterruptedException異常後,“中斷標記”會被重置為false,我們繼續做實驗:

class InterruptTask implements Runnable {
    @Override
    public void run() {
        CopyOnWriteArrayList copyOnWriteArrayList = new CopyOnWriteArrayList();
        try {
            long start = System.currentTimeMillis();
            TimeUnit.SECONDS.sleep(3);
            System.out.println("結束了,時間是" + (System.currentTimeMillis() - start));
        } catch (Exception ex) {
            System.out.println(Thread.currentThread().isInterrupted());
            ex.printStackTrace();
        }
    }
}

public class InterruptTest {
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new InterruptTask());
        thread1.start();
        thread1.interrupt();
    }
}
複製程式碼

執行結果:

false
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at java.lang.Thread.sleep(Thread.java:340)
	at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
	at com.codebear.InterruptTask.run(InterruptTest.java:20)
	at java.lang.Thread.run(Thread.java:748)
複製程式碼

可以很清楚的看到,“中斷標記”被重置為false了。

還有一個問題,大家可以思考下,程式碼的本意是當前執行緒被中斷後退出死迴圈,這段程式碼有問題嗎?

Thread th = Thread.currentThread();
while(true) {
  if(th.isInterrupted()) {
    break;
  }
 
  try {
    Thread.sleep(100);
  }catch (InterruptedException e){
    e.printStackTrace();
  }
}
複製程式碼

本題來自 極客時間 王寶令 老師的 《Java併發程式設計實戰》

程式碼是有問題的,因為catch住異常後,會把“中斷標記”重置。如果正好在sleep的時候,執行緒被中斷了,又重置了“中斷標記”,那麼下一次迴圈,檢測中斷標記為false,就無法退出死迴圈了。

isInterrupted

這個方法在上面已經出現過了,就是 獲取物件執行緒的“中斷標記”。

interrupted

獲取當前執行緒的“中斷標記”,如果發現當前執行緒被中斷,會重置中斷標記為false,該方法是static方法,通過Thread類直接呼叫。

併發程式設計基礎到這裡就結束了,可以看到內容還是相當多的,雖說是基礎,但是每一個知識點,如果要深究的話,都可以牽扯到“作業系統”,所以只有深入到了“作業系統”,才可以說真的懂了,現在還是僅僅停留在Java的層面,唉。

相關文章