併發編程基礎(下)

書接上文。上文主要講了下線程的基本概念,三種創建線程的方式與區別,還介紹了線程的狀態,線程通知和等待,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();
    }
}

運行結果:


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

當我們調用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();
    }
}

運行結果:


當然在一般情況下,可能永遠也不會用到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();
    }
}

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



因爲這是用戶線程,只要有一個用戶線程還沒結束,程序就不會退出。

再來看看守護線程:

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

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



因爲這是守護線程,當用戶線程結束後,不管有沒有守護線程還在運行,程序都會退出。

線程中斷

之所以把線程中斷放在後面,是因爲它是併發編程基礎中最難以理解的一個,當然這也與不經常使用有關。現在就讓我們好好看看線程中斷。
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的層面,唉。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章