併發編程基礎(下)

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

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();
    }
}

運行結果:ide

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線程

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

當咱們調用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的層面,唉。

相關文章
相關標籤/搜索