執行緒最最基礎的知識
- 2019 年 10 月 3 日
- 筆記
Java 多執行緒系列文章第 5 篇。
什麼是執行緒
試想一下沒有執行緒的程式是怎麼樣的?百度網盤在上傳文件時就無法下載文件了,得等文件上傳完成後才能下載文件。這個我們現在看起來很反人性,因為我們習慣了一個程式同時可以進行運行多個功能,而這些都是執行緒的功勞。
之前的文章 進程知多少 中講到,為了實現多個程式並行執行,引入了進程概念。現在引入執行緒是為了讓一個程式能夠並發執行。
執行緒的組成
執行緒ID:執行緒標識符。
當前指令指針(PC):指向要執行的指令。
暫存器集合:存儲單元暫存器的集合。
堆棧:暫時存放數據和地址,一般用來保護斷點和現場。
執行緒與進程區別
執行緒和進程之間的區別,我覺得可以用這個例子來看出兩者的不同,進程就是一棟房子,房子住著 3 個人,執行緒就是住在房子里的人。進程是一個獨立的個體,有自己的資源,執行緒是在進程里的,多個執行緒共享著進程的資源。
執行緒狀態
我們看到 Java 源程式碼裡面,執行緒狀態的枚舉有如下 6 個。
public enum State { //新建狀態 NEW, //運行狀態 RUNNABLE, //阻塞狀態 BLOCKED, //等待狀態 WAITING, //等待狀態(區別在於這個有等待的時間) TIMED_WAITING, //終止狀態 TERMINATED; }
下面給這 6 個狀態一一做下解釋。
NEW:新建狀態。在創建完 Thread ,還沒執行 start() 之前,執行緒的狀態一直是 NEW。可以說這個時候還沒有真正的一個執行緒映射著,只是一個對象。
RUNNABLE:運行狀態。執行緒對象調用 start() 之後,就進入 RUNNABLE 狀態,該狀態說明在 JVM 中有一個真實的執行緒存在。
BLOCKED:阻塞狀態。執行緒在等待鎖的釋放,也就是等待獲取 monitor 鎖。
WAITING:等待狀態。執行緒在這個狀態的時候,不會被分配 CPU,而且需要被顯示地喚醒,否則會一直等待下去。
TIMED_WAITING:超時等待狀態。這個狀態的執行緒也一樣不會被分配 CPU,但是它不會無限等待下去,有時間限制,時間一到就停止等待。
TERMINATED:終止狀態。執行緒執行完成結束,但不代表這個對象已經沒有了,對象可能還是存在的,只是執行緒不存在了。
執行緒既然有這麼多個狀態,那肯定就有狀態機,也就是在什麼情況下 A 狀態會變成 B 狀態。下面就來簡單描述一下。
結合下圖,我們 new 出執行緒類的時候,就是 NEW
狀態,調用 start() 方法,就進入了 RUNNABLE
狀態,這時如果觸發等待,則進入了 WAITING
狀態,如果觸發超時等待,則進入 TIMED_WAITING
狀態,當訪問需要同步的資源時,則只有一個執行緒能訪問,其他執行緒就進入 BLOCKED
狀態,當執行緒執行完後,進入 TERMINATED
狀態。
其實在 JVM 中,執行緒是有 9 個狀態,如下所示,有興趣的同學可以深入了解一下。
javaClasses.hpp enum ThreadStatus { NEW = 0, RUNNABLE = JVMTI_THREAD_STATE_ALIVE + // runnable / running JVMTI_THREAD_STATE_RUNNABLE, SLEEPING = JVMTI_THREAD_STATE_ALIVE + // Thread.sleep() JVMTI_THREAD_STATE_WAITING + JVMTI_THREAD_STATE_WAITING_WITH_TIMEOUT + JVMTI_THREAD_STATE_SLEEPING, IN_OBJECT_WAIT = JVMTI_THREAD_STATE_ALIVE + // Object.wait() JVMTI_THREAD_STATE_WAITING + JVMTI_THREAD_STATE_WAITING_INDEFINITELY + JVMTI_THREAD_STATE_IN_OBJECT_WAIT, IN_OBJECT_WAIT_TIMED = JVMTI_THREAD_STATE_ALIVE + // Object.wait(long) JVMTI_THREAD_STATE_WAITING + JVMTI_THREAD_STATE_WAITING_WITH_TIMEOUT + JVMTI_THREAD_STATE_IN_OBJECT_WAIT, PARKED = JVMTI_THREAD_STATE_ALIVE + // LockSupport.park() JVMTI_THREAD_STATE_WAITING + JVMTI_THREAD_STATE_WAITING_INDEFINITELY + JVMTI_THREAD_STATE_PARKED, PARKED_TIMED = JVMTI_THREAD_STATE_ALIVE + // LockSupport.park(long) JVMTI_THREAD_STATE_WAITING + JVMTI_THREAD_STATE_WAITING_WITH_TIMEOUT + JVMTI_THREAD_STATE_PARKED, BLOCKED_ON_MONITOR_ENTER = JVMTI_THREAD_STATE_ALIVE + // (re-)entering a synchronization block JVMTI_THREAD_STATE_BLOCKED_ON_MONITOR_ENTER, TERMINATED = JVMTI_THREAD_STATE_TERMINATED };
Java 執行緒實現
下面講一講在 Java 中如何創建一個執行緒。眾所周知,實現 Java 執行緒有 2 種方式:繼承 Thread 類和實現 Runnable 介面。
繼承 Thread 類
繼承 Thread 類,重寫 run()
方法。
class MyThread extends Thread { @Override public void run() { System.out.println("MyThread"); } }
實現 Runnable 介面
實現 Runnable 介面,實現 run()
方法。
class MyRunnable implements Runnable { public void run() { System.out.println("MyRunnable"); } }
這 2 種執行緒的啟動方式也不一樣。MyThread
是一個執行緒類,所以可以直接 new
出一個對象出來,接著調用 start()
方法來啟動執行緒;而 MyRunnable
只是一個普通的類,需要 new
出執行緒基類 Thread
對象,將 MyRunnable
對象傳進去。
下面是啟動執行緒的方式。
public class ThreadImpl { public static void main(String[] args) { MyThread myThread = new MyThread(); Thread myRunnable = new Thread(new MyRunnable()); System.out.println("main Thread begin"); myThread.start(); myRunnable.start(); System.out.println("main Thread end"); } }
列印結果如下:
main Thread begin main Thread end MyThread MyRunnable
看這結果,不像咱們之前的串列執行依次列印,主執行緒不會等待子執行緒執行完。
敲重點:不能直接調用 run()
,直接調用 run()
不會創建執行緒,而是主執行緒直接執行 run()
的內容,相當於執行普通函數。這時就是串列執行的。看下面程式碼。
public class ThreadImpl { public static void main(String[] args) { MyThread myThread = new MyThread(); Thread myRunnable = new Thread(new MyRunnable()); System.out.println("main Thread begin"); myThread.run(); myRunnable.run(); System.out.println("main Thread end"); } }
列印結果:
main Thread begin MyThread MyRunnable main Thread end
從結果看出只是串列的,但看不出沒有執行緒,我們看下面例子來驗證直接調用 run()
方法沒有創建新的執行緒,使用 VisualVM 工具來觀察執行緒情況。
我們對程式碼做一下修改,加上 Thread.sleep(1000000)
讓它睡眠一段時間,這樣方便用工具查看執行緒情況。
調用 run()
的程式碼:
public class ThreadImpl { public static void main(String[] args) { MyThread myThread = new MyThread(); myThread.setName("MyThread"); Thread myRunnable = new Thread(new MyRunnable()); myRunnable.setName("MyRunnable"); System.out.println("main Thread begin"); myThread.run(); myRunnable.run(); System.out.println("main Thread end"); try { Thread.sleep(1000000); } catch (InterruptedException e) { e.printStackTrace(); } } } class MyThread extends Thread { @Override public void run() { System.out.println("MyThread"); try { Thread.sleep(1000000); } catch (InterruptedException e) { e.printStackTrace(); } } } class MyRunnable implements Runnable { public void run() { System.out.println("MyRunnable"); try { Thread.sleep(1000000); } catch (InterruptedException e) { e.printStackTrace(); } } }
運行結果:
main Thread begin MyThread
只列印出 2 句日誌,觀察執行緒時也只看到 main
執行緒,沒有看到 MyThread
和 MyRunnable
執行緒,印證了上面咱們說的:直接調用 run()
方法,沒有創建執行緒。
下面我們來看看有
調用 start()
的程式碼:
public class ThreadImpl { public static void main(String[] args) { MyThread myThread = new MyThread(); myThread.setName("MyThread"); Thread myRunnable = new Thread(new MyRunnable()); myRunnable.setName("MyRunnable"); System.out.println("main Thread begin"); myThread.start(); myRunnable.start(); System.out.println("main Thread end"); try { Thread.sleep(1000000); } catch (InterruptedException e) { e.printStackTrace(); } } }
運行結果:
main Thread begin main Thread end MyThread MyRunnable
所有日誌都列印出來了,並且通過 VisualVM 工具可以看到 MyThread
和 MyRunnable
執行緒。看到了這個結果,切記創建執行緒要調用 start()
方法。
今天就先講到這,繼續關注後面的內容。
推薦閱讀
後台回復『設計模式』可以獲取《一故事一設計模式》電子書
覺得文章有用幫忙轉發&點贊,多謝朋友們!