從零開始學習Java多線程(一)
1. 什麽是進程?
對其概念需要自行goole,簡單理解就是:進程是計算機系統進行資源分配和調度的基本單位,是正在運行程序的實體;每一個進程都有它自己的內存空間和系統資源;進程是線程的容器。如:打開IDEA寫代碼是一個進程,打開有道詞典也是一個獨立的進程。
如果我們在用IDEA寫代碼的同時打開有道詞典那就是多進程,多進程具有獨立性,動態性,並發性,異步性。鑒於多數人混淆並行和並發,在此簡單介紹:
- 並發:多個CPU實例同時執行一段代碼或處理邏輯,具有物理意義上的同時發生。
- 並行:計算機通過調度算法爭奪CPU時間片繼而執行屬於自己的執行計劃,CPU的高效切換在轉瞬間完成,讓用戶感覺像是同時發生,實際上只是邏輯上的同時發生。
那麽QQ和網易雲是同時進行的嗎?取決於CPU的個數,單個CPU在某個時間點上只能做一件事情,而多核(多個CPU)可以做到同時進行。多進程的意義在於,提高了CPU使用率。值得一提的是,Java是不能夠通過調用系統資源來開啟一個進程的,例如在windows系統中,Java通過調用C語言底層代碼來開啟進程。
2. 什麽是線程?
線程:是進程中的單個順序控制流,計算機最小的執行單元,一條執行路徑。一個進程如果只有一條執行路徑,成為單線程程序;如果有多條執行路徑,則成為多線程程序;多線程共享該進程的全部資源。如:打開QQ後,好友聊天屬於一條線程,瀏覽QQ空間又屬於一條線程。
假如我們的計算機只有一個CPU,那麽CPU在某一個時刻只能執行一條指令,線程只有得到CPU時間片才能擁有使用權,才可以執行指令,那麽Java是如何對線程進行調用的呢?
線程調用的兩種模型:
- 分時調度模型 : 所有的線程輪流獲得CPU的使用權,平均分配每個線程占用CPU的時間片
- 搶占式調度模型:優先讓優先級高的線程使用CPU,如果優先級相同,那麽會從中隨機選取一個,優先級高的線程獲取的CPU時間片相對多一些。
- Java使用的是搶占式調度模型。
- 可利用API設置和獲取線程優先級。
public final int getPriority()
public final void setPriority(int newPriority)
現在大致了解進程和線程之間的關系後,再來看Java程序運行原理。
Java命令會啟動Java虛擬機,啟動JVM,等於啟動了一個進程。該進程會自動啟動一個"主線程",然後主線程去調用某個類的main方法,所有main方法運行在主線程中,在此之前的所有程序都是單線程的。Java虛擬機的啟動是多線程的,因為JVM啟動至少啟動了垃圾回收線程和主線程。
3. 多線程的意義
進程具有獨立性,多進程之間是沒有共享資源的,但是多線程可以共享內存資源,而且十分簡單。系統創建進程是需要為該進程重新分配系統資源,浪費了大量資源,但創建線程的代價要小很多,因此多線程實現多任務的並發要比多進程的效率高。
總結起來:
- 共享內存資源
- 並發效率高
- 多線程的作用不是提高執行速度,而是提高應用程序的使用率
而多線程的實際應用包括:
-
- 瀏覽器必須能同時下載多個圖片
- 一臺服務器必須n能同時響應多個用戶請求
- JVM本身就在後臺提高了一個超級線程進行垃圾回收
4. Java多線程實現
(一)繼承Thread類,復寫run()方法
1 /** 2 * @author supiaol 3 * @date 2019/3/7 4 * @time 9:26 5 */ 6 public class MyThread extends Thread { 7 8 //多線程運行的代碼塊 9 public void run() { 10 System.out.println("Thread is running"); 11 } 12 13 public static void main(String[] args) { 14 15 MyThread myThread1 = new MyThread(); 16 MyThread myThread2 = new MyThread(); 17 18 //運行多線程 19 myThread1.start(); 20 myThread2.start(); 21 22 } 23 }
Thread類本質上是實現Runable接口的一個實例。Thread 類中有一些關鍵屬性,如:name屬性代表線程的名稱,可以通過Thread類的構造器中參數來指定線程名稱;priority屬性代表線程優先級,上文提高優先級高的線程搶占CPU時間可能性越大,默認優先級為5,最小值為1,最大值為10;daemon屬性表示線程是否是守護線程,target屬性代表要執行的任務。
下面是Thread類中常用的api:
1. run()方法 新建線程(新建狀態)
需要明確的是run()方法不是用來運行線程的,也不需要用戶調用,當線程獲得CPU執行時間,會進入run()方法執行代碼塊。
2. start()方法 啟動線程(就緒狀態)
線程啟動的方法,調用start()方法後,系統會開啟一個新的線程用來執行用戶定義的任務,在此過程中,為線程分配系統資源。需要註意的是,調用start()方法後,並不會立即執行定義的任務,而是賦 予線程可以搶占CPU時間片的資格,只有得到CPU時間片才能執行計劃任務。
3. sleep()方法 睡眠線程(堵塞狀態)
線程睡眠,必須指定睡眠時間,在適當的位置調用sleep(),讓該線程睡眠,也就是交出CPU,讓CPU來執行其它任務。特別需要關註的是,sleep()方法不會釋放鎖或者監視器,也就是說如果當前線程持有某個對象的鎖,那麽即使調用sleep()方法,其他線程也無法訪問該對象,關於該方法和鎖的關系會在後續詳細說明和演示。
4.yield()方法 禮讓線程(堵塞狀態)
調用yield()方法同樣可以讓該線程交出CPU時間片,失去執行權,類似於sleep()方法,同樣不會釋放鎖對象或者監視器,而區別之處在於,yield()不能控制具體交出CPU的時間,而且交出的CPU時間片只能允許相同優先級的線程獲取。
5.join()方法 線程加入(堵塞狀態)
join方法有三個重載版本:
join() join(long millis) //參數為毫秒 join(long millis,int nanoseconds) //第一參數為毫秒,第二個參數為納秒
它們的區別在於指定的參數,假如我們在main()所屬的主線程中調用另外一個從線程thread.join()方法,則main()方法失去執行權,只有等到thread線程執行完畢或者等待一定的時間後重新獲得執行權。如何調用無參join()方法,需要等待thread線程執行完畢,調用指定時間的帶參join()方法,則等到指定時間過後獲取執行權。
通過查看源碼發現,join實際上調用了wait()方法實現主線程等待,至於wait()方法,後面學習線程安全時候著重講述,在此先做了解。
1 public final synchronized void join(long millis) 2 throws InterruptedException { 3 long base = System.currentTimeMillis(); 4 long now = 0; 5 6 if (millis < 0) { 7 throw new IllegalArgumentException("timeout value is negative"); 8 } 9 //空參join 需要等待從線程執行完畢 10 if (millis == 0) { 11 while (isAlive()) { 12 wait(0); 13 } 14 } else { 15 //帶參join,等待指定時間後重新獲得執行權 16 while (isAlive()) { 17 long delay = millis - now; 18 if (delay <= 0) { 19 break; 20 } 21 wait(delay); 22 now = System.currentTimeMillis() - base; 23 } 24 } 25 }
6. interrupt() 線程中斷(堵塞狀態)
顧名思義,interrupt即中斷的意思。調用interrupt()方法能夠使處於堵塞狀態的線程拋出異常,其實質上就是用來中斷處於堵塞狀態的線程,通常配合isInterrupted()方法來停止正常運行的線程。
7. stop()方法 線程停止(線程中斷)
stop方法是一個已經被廢棄的方法,自身不安全。因為調用stop方法會直接終止run方法的調用,並且拋出ThreadDeath異常,如果該線程調用stop方法之前持有某個對象鎖,之後會完全釋放鎖對象,導致對象狀態不一致。
8.destory() 方法 已被廢棄,不會用到。
(二) 實現Runnable接口,重寫run()方法。
1 /** 2 * @author supiaol 3 * @date 2019/3/7 4 * @time 14:49 5 */ 6 public class MyThread extends OtherClass implements Runnable { 7 @Override 8 public void run() { 9 System.out.println("Runnable realize multithreading"); 10 } 11 12 public static void main(String[] args) { 13 MyThread myThread1 = new MyThread(); 14 MyThread myThread2 = new MyThread(); 15 16 new Thread(myThread1).start(); 17 new Thread(myThread2).start(); 18 19 } 20 }
實現Runnable接口實現多現成的好處就在於彌補Java單繼承的缺陷。更適合多個相同程序的代碼去處理一個資源的情況,這樣線程同程序的代碼,數據有效分離,較好的體現了面向對象的設計思想。區別於繼承Thread類啟動線程,實現Runnable接口啟動線程時,需要將實現Runnable接口的實例作為target目標傳入Thread實例,然後調用start()方法啟動線程。
如果需要對線程設置名稱,可以通過線程對象調用setName方法進行設置,也可以通過Thread的構造方法設置,而getName()方法可以獲取線程名稱,也可以通過Thread.currentThread().getName()方法獲取當前線程的名稱。
(三) 基於線程池實現多線程,用到不多,在此不多介紹
5. 線程的生命周期
- 新建:創建線程對象,從new一個線程對象到調用start()方法之間都是新建狀態
- 就緒:調用start()方法後,線程對象已經啟動,但是還沒有獲取到CPU的執行權
- 運行:獲取到CPU時間片,開始執行run()方法中的代碼
- 堵塞:失去執行權,回到就緒狀態。
- 結束:代碼運行完畢,或者main方法執行完畢,線程消亡
以上就是一個線程完整的生命周期,一個線程最基本的生命周期包括:新建,就緒,運行,結束。
從零開始學習Java多線程(一)