書接上文。上文主要講了下線程的基本概念,三種建立線程的方式與區別,還介紹了線程的狀態,線程通知和等待,join等,本篇繼續介紹併發編程的基礎知識。java
當一個執行的線程調用了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
咱們知道線程是以時間片的機制來佔用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(); } }
運行結果:
線程
固然因爲線程的特性,因此每次運行結果可能都不太相同,可是當咱們運行屢次後,會發現絕大多數的時候,兩個線程的打印都是比較平均的,我用完時間片了,你用,你用完了時間片了,我再用。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(); } }
運行結果:
固然在通常狀況下,可能永遠也不會用到yield,可是仍是要對這個方法有必定的瞭解。
當線程調用sleep後,會阻塞當前線程指定的時間,在這段時間內,線程調度器不會調用此線程,當指定的時間結束後,該線程的狀態爲「就緒」,等待分配CPU資源。
當線程調用yield後,不會阻塞當前線程,只是讓出時間片,回到「就緒」的狀態,等待分配CPU資源。
死鎖是指多個線程在執行的過程當中,由於爭奪資源而形成的相互等待的現象,並且沒法打破這個「僵局」。
死鎖的四個必要條件:
要想打破「死鎖」僵局,只須要破壞以上四個條件中的任意一個,可是程序員能夠干預的只有「請求並持有」,「環路等待」兩個條件,其他兩個條件是鎖的特性,程序員是沒法干預的。
聰明的你,必定看出來了,所謂「死鎖」就是「悲觀鎖」形成的,相對於「死鎖」,還有一個「活鎖」,就是「樂觀鎖」形成的。
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方法。
中斷線程。當線程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,就沒法退出死循環了。
這個方法在上面已經出現過了,就是 獲取對象線程的「中斷標記」。
獲取當前線程的「中斷標記」,若是發現當前線程被中斷,會重置中斷標記爲false,該方法是static方法,經過Thread類直接調用。
併發編程基礎到這裏就結束了,能夠看到內容仍是至關多的,雖然說是基礎,可是每個知識點,若是要深究的話,均可以牽扯到「操做系統」,因此只有深刻到了「操做系統」,才能夠說真的懂了,如今仍是僅僅停留在Java的層面,唉。