從零開始創建一家公司
Java並發編程是Java的基礎之一,為了能在實踐中學習並發編程,我們跟着創建一家公司的旅途,一起來學習Java並發編程。
進程與線程
由於我們的目標是學習並發編程,所以我不會把很多時間放在底層原理和復雜的概念上。操作系統上的進程就像是全國各地的公司,而每個公司又都有許多員工--線程。關於進程與線程的關系先了解這么多。
創建一個線程
想象你現在成立了一個互聯網公司,你准備先設立一個總經理的崗位,而你自己是幕后Boss,Main線程。
public class App { public static void main(String[] args) throws Exception { Thread manager = new Thread(new manager()); manager.start(); } } class manager implements Runnable { @Override public void run() { System.out.println("我是總經理"); } }
讓我們一步步看上面的代碼,首先為了區分公司的員工和其他人,每個員工要有統一的標識,都屬於 Thread 類。這個Thread就是我們公司的員工標識,只要是這個類的對象都屬於你的公司。
雖然已經有了 Thread 標識,但公司的每個人的職責都不同,所以還要進一步的細分。向Thread構造函數傳入不同的實現Runnable接口的對象,可以獲得不同職責的員工。
不同員工的職責由不同的 run 方法實現區分。manager.start()方法就是默認調用Runnable接口中的run方法。同時Runnable之所以是個接口,是因為繼承只能單一,而接口可以實現多個,就像我們公司的員工,在社會上還可能有其他位置一樣。
終止線程
我們公司當然要有下班制度,重新實現如下。
public class App { public static void main(String[] args) throws Exception { Thread manager = new Thread(new manager()); manager.start(); manager.interrupt(); } } class manager implements Runnable { @Override public void run() { while(true) { if(Thread.currentThread().isInterrupted()) break; System.out.println("正在上班中"); }
} }
可以看到 manager 類中加了個循環表示持續的上班狀態。當到下班時間時,你(Main線程)調用經理的 interrupt 方法通知他該下班了。
注意,這里的 interrupt方法不會直接結束上班狀態,只是通知。而經理根據自己 run 方法的實現來決定到底怎么下班。
用 Thread.currentThread().isInterrupted() 方法來判斷是否收到通知。 Thread.currentThread()代表當前對象,之所以不是當前類,是因為你有時候會想只通知某些特定的員工下班,而不是每次通知都只能讓所有員工下班。
還有一個 interrupted 方法,和 isInterrupted 作用相同,都是查詢當前狀態,不過前一個方法查詢的同時會清除狀態。如果員工都是收到中斷請求就下班,那二者沒有什么區別。但對某些需要收到特定次數下班通知才會下班的員工來說,用 interrupted方法就特別合適。
線程休眠
當員工在上班時間卻感覺疲倦怎么辦,幸好我們有休息制度。
class manager implements Runnable { @Override public void run() { while(true) { try { Thread.sleep(1000); } catch (InterruptedException e) { break; } if(Thread.currentThread().isInterrupted()) break; System.out.println("我是總經理"); } } }
可以看到代碼中增加了靜態方法 Thread.sleep 和一個異常處理機制。線程會先休眠1秒后在執行下面的步驟。
雖然 sleep 是靜態方法,但是只對當前線程其作用。這樣設計的原因是為了防止別的線程調用該線程的休眠方法,也就是說,只有當前線程才能控制當前線程的休眠狀態。
由於線程休眠過程中無法處理中斷,所以當線程休眠時收到中斷請求,就會拋出異常,在異常處理中決定如何中斷。
總結
關於線程的基本情況基本這么多,可以看到麻雀雖小,五臟俱全。有唯一的標識Thread,可以實現自己的方法,可以響應中斷,可以進行休眠等待。一個員工的工作周期已經初步成型,接下來我們看看簡單的員工之間的合作。
等待(wait)和通知(notify)
等待與通知這兩個方法和前面介紹的最大的不同在於,由於要負責線程之間的協作,這兩個方法是屬於object的而不是Thread的。
為什么呢?如果這兩個方法是Thread的話兩個線程之間的協作就會收到極大的限制,而如果是object的話兩個線程可以通過任意一個對象進行通信。
具體過程如下 當一個線程調用等待方法時,它會加入一個等待隊列。由於有多個線程可能擁有該對象,當不同線程先后調用這個方法時,都會加入這個對象的等待隊列中。
當 notify 方法被某一線程調用時,就會在這個等待隊列中隨機挑出一個線程喚醒(並不是先到先得)。
要注意的是 使用 wai() 和 notify 的關鍵字必須要加鎖,代碼加粗部分
public class App { public static void main(String[] args) throws Exception { Thread manager = new Thread(new manager()); manager.start(); manager.interrupt(); synchronized(manager){ manager.notify(); } } } class manager implements Runnable { @Override public void run() { while(true) { synchronized(this) { try {
this.wait(); Thread.sleep(1000); } catch (InterruptedException e) { break; } if(Thread.currentThread().isInterrupted()) break; System.out.println("我是總經理"); } } } }
如果不加鎖編譯不會報錯,但執行會會有 current thread is not owner 異常,意思是當前線程沒有獲得對象的鎖就調用了 wait 方法,notify 的方法同理,也必須要先獲取鎖才能執行。
過程就是 加鎖---- 等待(wait)-----釋放鎖 -----加鎖------通知(notify)------ 繼續執行。但通知后不會釋放鎖,所以調用 wait 方法的線程要先等該線程執行完釋放鎖后才能繼續執行。
至於為什么要加鎖呢?如果沒有加鎖,就會出現丟失喚醒問題,既 notify 方法早於 wait 調用,導致 wait 的線程一直接收不到喚醒信號。
為什么加鎖就能避免,難道 notify 線程沒有可能先執行嗎?其實確實即使加鎖后 notify 也可能先於 wait 執行。因為這兩個方法是負責線程協作的,所以一般代碼邏輯是用戶來寫出,用戶來避免 notify 先於wait執行,但如果沒有加鎖,即使用戶的邏輯正確也可能導致 notify 先於 wait 執行,這也是個並發問題。
等待線程結束(join)和謙讓(yeild)
等待結束和謙讓是另外的一種線程間協作的方式,上文提到的等待和通知是基於線程內部方法的,而等待結束是等待線程整體的,可以說是線程協作的一種補充。
看個代碼
public class App { public static void main(String[] args) throws Exception { Thread manager = new Thread(new manager()); manager.start(); manager.join(); } } } class manager implements Runnable { @Override public void run() { Thread.sleep(1000); } } }
在這個代碼中 Main 線程會等待 manger 睡眠結束后才會繼續執行,期間一直處於阻塞狀態。
還有一點很有趣,join 本質是讓當前線程調用該對象的 wait 方法,比如上文代碼,本質是 Main 線程調用 manager 的 wait 方法,再此對象上等待,而該被等待的線程結束后,會調用 notifyAll 方法告訴所有被等待的線程結束等待。所以最好不要在線程上調用 wait 方法,因為可能被 join 的 notifyAll 意外喚醒。
最后思考一下 jion 和 wait notify 之間的使用場景是很有意思的,wait 和 notify 是 線程調用對象(由於join的存在,這個對象不能是線程!)的方法來進行協作,一個線程調用wait進入阻塞,另一個線程調用notify方法喚醒,一共三個對象(兩個線程,一個協作對象)。而 jion 的場景則是 一個線程調用 wait 方法等待一個線程,該線程調用notifyAll 方法喚醒該線程,沒有第三者。所以 jion 可以理解為兩個線程的互相協作,而 wait notify 是兩個線程通過一個對象進行協作,當然只是可以這樣理解,具體本質還需要好好在實踐生活中使用才能慢慢領會到。