再談多線程編程(一)——線程的概念、多線程的創建、守護線程、線程狀態的轉化
1、什麽是線程以及多線程與進程的區別
在現代操作在運行一個程序時,會為其創建一個進程。例如啟動一個QQ程序,操作系統就會為其創建一個進程。而操作系統中調度的最小單位元是線程,也叫輕量級進程,在一個進程裏可以創建多個線程,這些線程都擁有各自的計數器,堆棧和局部變量等屬性,並且能夠訪問共享的內存變量。處理器在這些線程上高速切換,讓使用者感覺到這些線程在同時執行。因此我們可以這樣理解:
進程:正在運行的程序,是系統進行資源分配和調用的獨立單位。每一個進程都有它自己的內存空間和系統資源。
線程:是進程中的單個順序控制流,是一條執行路徑。一個進程如果只有一條執行路徑,則稱為單線程程序。一個進程如果有多條執行路徑,則稱為多線程程序。
2、多線程的創建與啟動
創建多線程有兩種方法,一種是繼承Thread類重寫run方法,另一種是實現Runnable接口重寫run方法。下面我們分別給出代碼示例。 繼承Thread類重寫run方法:package com.zejian.test; public class ThreadByEx extends Thread{ /** * 重寫run方法 */ @Override public void run() { System.out.println("I‘m a thread that extends Thread!"); } }
實現Runnable接口重寫run方法:
package com.zejian.test; public class ThreadByRunnable implements Runnable{ /** * 實現run方法 */ @Override public void run() { System.out.println("I‘m a thread that implements Runnable !"); } }
怎麽啟動線程?
package com.zejian.test;public class MainTest { public static void main(String[] args) { //繼承Thread啟動的方法 ThreadByEx t1=new ThreadByEx(); t1.start();//啟動線程 //實現Runnable啟動線程的方法 ThreadByRunnable r = new ThreadByRunnable(); Thread t2 =new Thread(r); t2.start();//啟動線程 } }
運行結果:
I‘m a thread that extends Thread! I‘m a thread that implements Runnable !
這裏有點需要註意的是調用start()方法後並不是是立即的執行多線程的代碼,而是使該線程變為可運行態,什麽時候運行多線程代碼是由操作系統決定的。
3、守護線程以及線程優先級
守護線程:
首先,我們可以通過t.setDaemon(true)的方法將線程轉化為守護線程。而守護線程的唯一作用就是為其他線程提供服務。計時線程就是一個典型的例子,它定時地發送“計時器滴答”信號告訴其他線程去執行某項任務。當只剩下守護線程時,虛擬機就退出了,因為如果只剩下守護線程,程序就沒有必要執行了。另外JVM的垃圾回收、內存管理等線程都是守護線程。還有就是在做數據庫應用時候,使用的數據庫連接池,連接池本身也包含著很多後臺線程,監控連接個數、超時時間、狀態等等。最後還有一點需要特別註意的是在java虛擬機退出時Daemon線程中的finally代碼塊並不一定會執行,代碼示例:
package com.zejian.test; public class Demon { public static void main(String[] args) { Thread deamon = new Thread(new DaemonRunner(),"DaemonRunner"); //設置為守護線程 deamon.setDaemon(true); deamon.start();//啟動線程 } static class DaemonRunner implements Runnable{ @Override public void run() { try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); }finally{ System.out.println("這裏的代碼在java虛擬機退出時並不一定會執行哦!"); } } } }
因此在構建Daemon線程時,不能依靠finally代碼塊中的內容來確保執行關閉或清理資源的邏輯。
線程的優先級:
在現代操作系統中基本采用時分的形式調度運行的線程,操作系統會分出一個個時間片,線程會分配到若幹時間片,當線程的時間片用完了就會發生線程調度,並等待著下一次分配。線程分配到的時間片多少也決定了線程使用處理器資源的多少,而線程優先級就是決定線程需要多或者少分配一些處理器資源的線程屬性。在java線程中,通過一個整型的成員變量Priority來控制線程優先級。
每一個線程有一個優先級,默認情況下,一個線程繼承它父類的優先級。可以用setPriority方法提高或降低任何一個線程優先級。可以將優先級設置在MIN_PRIORITY(在Thread類定義為1)與MAX_PRIORITY(在Thread類定義為10)之間的任何值。線程的默認優先級為NORM_PRIORITY(在Thread類定義為5)。
盡量不要依賴優先級,如果確實要用,應該避免初學者常犯的一個錯誤。如果有幾個高優先級的線程沒有進入非活動狀態,低優先級線程可能永遠也不能執行。每當調度器決定運行一個新線程時,首先會在具有高優先級的線程中進行選擇,盡管這樣會使低優先級的線程可能永遠不會被執行到。因此我們在設置優先級時,針對頻繁阻塞(休眠或者I/O操作)的線程需要設置較高的優先級,而偏重計算(需要較多CPU時間或者運算)的線程則設置較低的優先級,這樣才能確保處理器不會被長久獨占。當然還有要註意就是在不同的JVM以及操作系統上線程的規劃存在差異,有些操作系統甚至會忽略對線程優先級的設定,如mac os系統或者Ubuntu系統........
4、線程的狀態轉化關系
(1)新建狀態(New):新創建了一個線程對象。
(2)就緒狀態(Runnable):線程對象創建後,其他線程調用了該對象的start()方法。該狀態的線程位於可運行線程池中,變得可運行,等待獲取CPU的使用權。
(3)運行狀態(Running):就緒狀態的線程獲取了CPU,執行程序代碼。
(4)阻塞狀態(Blocked):阻塞狀態是線程因為某種原因放棄CPU使用權,暫時停止運行。直到線程進入就緒狀態,才有機會轉到運行狀態。阻塞的情況分三種:
- 等待阻塞(WAITING):運行的線程執行wait()方法,JVM會把該線程放入等待池中。
- 同步阻塞(Blocked):運行的線程在獲取對象的同步鎖時,若該同步鎖被別的線程占用,則JVM會把該線程放入鎖池中。
- 超時阻塞(TIME_WAITING):運行的線程執行sleep(long)或join(long)方法,或者發出了I/O請求時,JVM會把該線程置為阻塞狀態。
(5)死亡狀態(Dead):線程執行完了或者因異常退出了run()方法,該線程結束生命周期。
圖中的方法解析如下:
Thread.sleep():在指定時間內讓當前正在執行的線程暫停執行,但不會釋放"鎖標誌"。不推薦使用。
Thread.sleep(long):使當前線程進入阻塞狀態,在指定時間內不會執行。
Object.wait()和Object.wait(long):在其他線程調用對象的notify或notifyAll方法前,導致當前線程等待。線程會釋放掉它所占有的"鎖標誌",從而使別的線程有機會搶占該鎖。 當前線程必須擁有當前對象鎖,如果當前線程不是此鎖的擁有者,會拋出IllegalMonitorStateException異常。 喚醒當前對象鎖的等待線程使用notify或notifyAll方法,也必須擁有相同的對象鎖,否則也會拋出IllegalMonitorStateException異常,wait()和notify()必須在synchronized函數或synchronized中進行調用。如果在non-synchronized函數或non-synchronized中進行調用,雖然能編譯通過,但在運行時會發生IllegalMonitorStateException的異常。
Object.notifyAll():從對象等待池中喚醒所有等待等待線程
Object.notify():從對象等待池中喚醒其中一個線程。
Thread.yield():暫停當前正在執行的線程對象,yield()只是使當前線程重新回到可執行狀態,所以執行yield()的線程有可能在進入到可執行狀態後馬上又被執行,yield()只能使同優先級或更高優先級的線程有執行的機會。
Thread.Join():把指定的線程加入到當前線程,可以將兩個交替執行的線程合並為順序執行的線程。比如在線程B中調用了線程A的Join()方法,直到線程A執行完畢後,才會繼續執行線程B。
再談多線程編程(一)——線程的概念、多線程的創建、守護線程、線程狀態的轉化