偉大的理想只有通過忘個人鬥爭和犧牲才能勝利實現。
本篇爲【Dali王的技術博客】Java併發編程系列第二篇,講講有關線程的那些事兒。主要內容是以下這些:java
進程表明了運行中的程序,一個運行的Java程序就是一個進程。在Java中,當咱們啓動main函數時就啓動了一個JVM的進程,而main函數所在的線程就是這個進程中的一個線程,稱爲主線程。
進程和線程的關係以下圖所示:
面試
由上圖能夠看出來,一個進程中有多個線程,多個線程共享進程的堆的方法區資源,可是每一個線程有本身的程序計數器和棧區域。編程
Java中有三種線程建立方式,分別爲:繼承Thread類並重寫run方法,實現Runnable接口的run方法,使用FutureTask方式。
先看繼承Thread方式的實現,代碼示例以下:json
public class ThreadDemo { public static class DemoThread extends Thread { @Override public void run() { System.out.println("this is a child thread."); } } public static void main(String[] args) { System.out.println("this is main thread.") DemoThread thread = new DemoThread(); thread.start(); } }
上面代碼中DemoThread類繼承了Thread類,並重寫了run方法。在main函數裏建立了一個DemoThread的實例,而後調用其start方法啓動了線程。微信
tips:調用start方法後線程並無立刻執行,而是處於就緒狀態,也就是這個線程已經獲取了除CPU資源外的其餘資源,等待獲取CPU資源後纔會真正處於運行狀態。
使用繼承方式,好處在於經過this就能夠獲取當前線程,缺點在於Java不支持多繼承,若是繼承了Thread類,那麼就不能再繼承其餘類。並且任務與代碼耦合嚴重,一個線程類只能執行一個任務,使用Runnable則沒有這個限制。
來看實現Runnable接口的run方法的方式,代碼示例以下:
public class RunnableDemo { public static class DemoRunnable implements Runnable { @Override public void run() { System.out.println("this is a child thread."); } } public static void main(String[] args) { System.out.println("this is main thread."); DemoRunnable runnable = new DemoRunnable(); new Thread(runnable).start(); new Thread(runnable).start(); } }
上面代碼兩個線程共用一個Runnable邏輯,若是須要,能夠給RunnableTask添加參數進行任務區分。在Java8中,可使用Lambda表達式對上述代碼進行簡化:併發
public static void main(String[] args) { System.out.println("this is main thread."); Thread t = new Thread(() -> System.out.println("this is child thread")); t.start(); }
上面兩種方式都有一個缺點,就是任務沒有返回值,下面看第三種,使用FutureTask的方式。代碼示例以下:ide
public class CallableDemo implements Callable<JsonObject> { @Override public JsonObject call() throws Exception { return new JsonObject(); } public static void main(String[] args) { System.out.println("this is main thread."); FutureTask<JsonObject> futureTask = new FutureTask<>(new CallableDemo()); // 1. 可複用的FutureTask new Thread(futureTask).start(); try { JsonObject result = futureTask.get(); System.out.println(result.toString()); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } // 2. 一次性的FutureTask FutureTask<JsonObject> innerFutureTask = new FutureTask<>(() -> { JsonObject jsonObject = new JsonObject(); jsonObject.addProperty("name", "Dali"); return jsonObject; }); new Thread(innerFutureTask).start(); try { JsonObject innerResult = innerFutureTask.get(); System.out.println(innerResult.toString()); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } } }
如上代碼,CallableDemo實現了Callable接口的call方法,在main函數中使用CallableDemo的實例建立了一個FutureTask,而後使用建立的FutureTask對象做爲任務建立了一個線程並啓動它,最後經過FutureTask等待任務執行完畢並返回結果。
一樣的,上面的操做過程適合於須要複用的任務,若是對於一次性的任務,大能夠經過Lambda來簡化代碼,如註釋2處。函數
在項目中常常會遇到一個場景,就是須要等待某幾件事情完成後才能繼續往下執行。Thread類中有一個join方法就能夠用來處理這種場景。直接上代碼示例:學習
public static void main(String[] args) throws InterruptedException { System.out.println("main thread starts"); Thread t1 = new Thread(() -> System.out.println("this is thread 1")); Thread t2 = new Thread(() -> System.out.println("this is thread 2")); t1.start(); t2.start(); System.out.println("main thread waits child threads to be over"); t1.join(); t2.join(); System.out.println("child threads are over"); }
上面代碼在主線程裏啓動了兩個線程,而後分別調用了它們的join方法,主線程會在調用t1.join()後被阻塞,等待其執行完畢後返回;而後主線程調用t2.join()後再次被阻塞,等待t2執行完畢後返回。上面代碼的執行結果以下:this
main thread starts main thread waits child threads to be over this is thread 1 this is thread 2 child threads are over
須要注意的是,線程1調用線程2的join方法後會被阻塞,當其餘線程調用了線程1的interrupt方法中斷了線程1時,線程1會拋出一個InterruptedException異常而返回。
Thread類中有一個static的sleep方法,當一個執行中的線程調用了Thread的sleep方法後,調用線程會暫時讓出指定時間的執行權,也就是在這期間不參與CPU的調度,可是該線程所擁有的監視器資源,好比鎖仍是不讓出的。指定的睡眠時間到了後該函數會正常返回,線程就處於就緒狀態,而後等待CPU的調度執行。
tips:面試當中wait和sleep常常會被用來比較,須要多加體會兩者的區別。
調用某個對象的wait()方法,至關於讓當前線程交出此對象的monitor,而後進入等待狀態,等待後續再次得到此對象的鎖;notify()方法可以喚醒一個正在等待該對象的monitor的線程,當有多個線程都在等待該對象的monitor的話,則只能喚醒其中一個線程,具體喚醒哪一個線程則不得而知。
調用某個對象的wait()方法和notify()方法,當前線程必須擁有這個對象的monitor,所以調用wait()方法和notify()方法必須在同步塊或者同步方法中進行(synchronized塊或者synchronized方法)。
看一個線程睡眠的代碼示例:
private static final Lock lock = new ReentrantLock(); public static void main(String[] args) { Thread t1 = new Thread(() -> { // 獲取獨佔鎖 lock.lock(); System.out.println("thread1 get to sleep"); try { Thread.sleep(1000); System.out.println("thread1 is awake"); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } }); Thread t2 = new Thread(() -> { // 獲取獨佔鎖 lock.lock(); System.out.println("thread2 get to sleep"); try { Thread.sleep(1000); System.out.println("thread2 is awake"); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } }); t1.start(); t2.start(); }
上面的代碼建立了一個獨佔鎖,而後建立了兩個線程,每一個線程在內部先獲取鎖,而後睡眠,睡眠結束後會釋放鎖。執行結果以下:
thread1 get to sleep thread1 is awake thread2 get to sleep thread2 is awake
從執行結果來看,線程1先獲取鎖,而後睡眠,再被喚醒,以後才輪到線程2獲取到鎖,也即在線程1sleep期間,線程1並無釋放鎖。
須要注意的是,若是子線程在睡眠期間,主線程中斷了它,子線程就會在調用sleep方法處拋出了InterruptedException異常。
Thread類中有一個static的yield方法,當一個線程調用yield方法時,實際就是暗示線程調度器當前線程請求讓出本身的CPU使用,若是該線程還有沒用完的時間片也會放棄,這意味着線程調度器能夠進行下一輪的線程調度了。
當一個線程調用yield方法時,當前線程會讓出CPU使用權,而後處於就緒狀態,線程調度器會從線程就緒隊列裏面獲取一個線程優先級最高的線程,固然也有可能會調度到剛剛讓出CPU的那個線程來獲取CPU執行權。
請看代碼示例:
public static void main(String[] args) { Thread t1 = new Thread(() -> { for (int i = 0; i < 10; i++) { if (i == 8) { System.out.println("current thread: " + Thread.currentThread() + " yield cpu"); } Thread.yield(); // 2 } System.out.println("current thread: " + Thread.currentThread() + " is over"); }); Thread t2 = new Thread(() -> { for (int i = 0; i < 10; i++) { if (i == 8) { System.out.println("current thread: " + Thread.currentThread() + " yield cpu"); } Thread.yield(); // 1 } System.out.println("current thread: " + Thread.currentThread() + " is over"); }); t1.start(); t2.start(); }
在如上的代碼中,兩個線程的功能同樣,運行屢次,同一線程的兩行輸出是順序的,可是總體順序是不肯定的,取決於線程調度器的調度狀況。
當把上面代碼中1和2處代碼註釋掉,會發現結果只有一個,以下:
current thread: Thread[Thread-1,5,main] yield cpu current thread: Thread[Thread-0,5,main] yield cpu current thread: Thread[Thread-1,5,main] is over current thread: Thread[Thread-0,5,main] is over
從結果可知,Thread.yiled方法生效使得兩個線程分別在執行過程當中放棄CPU,而後在調度另外一個線程,這裏的兩個線程有點互相謙讓的感受,最終是因爲只有兩個線程,最終仍是執行完了兩個任務。
tips:sleep和yield的區別:
當線程調用sleep方法時,調用線程會阻塞掛起指定的時間,在這期間線程調度器不會去調度該線程。而調用yield方法時,線程只是讓出本身剩餘的時間片,並無被阻塞掛起,而是出於就緒狀態,線程調度器下一次調度時就可能調度到當前線程執行。
Java中的線程中斷是一種線程間的協做模式。每一個線程對象裏都有一個boolean類型的標識(經過isInterrupted()方法返回),表明着是否有中斷請求(interrupt()方法)。例如,當線程t1想中斷線程t2,只須要在線程t1中將線程t2對象的中斷標識置爲true,而後線程2能夠選擇在合適的時候處理該中斷請求,甚至能夠不理會該請求,就像這個線程沒有被中斷同樣。
在上面章節中也講到了線程中斷的一些內容,此處就再也不用代碼來展開了。
繼續附上Java編程的系統學習大綱以供參考:
Java併發編程.png
【參考資料】
本文由微信公衆號【Dali王的技術博客】原創,掃碼關注獲取更多原創技術文章。