併發和並行併發併發指的是在同一個時間段發生,即交替執行。例如單核CPU一次只能處理一個程式,但是為什麼我們可以在使用IntelliJ IDEA寫程式碼的同時又可以使用QQ音樂播放音樂呢?因為這兩個程式微觀上在執行時是交替執行,只不過交替的時間非常短(毫秒級),我們感覺是同時在執行,實際上是併發。單核CPU不能並行處理多個任務,只能是多個任務在單個CPU上併發執行。並行並行指的兩個或者多個事件在同一時刻發生,即同時執行 。在多核CPU系統中,併發的程式可以分配到不同的CPU上,實現多工同時執行,即利用每個處理器來處理一個可以併發執行的程式,這樣多個程式便可以同時執行。目前的計算機絕大多數都是多核CPU,核心越多,並行處理的程式越多,能大大提高電腦的執行效率。
CPU資訊
程序和執行緒程序程序是程式的一次執行過程,是系統執行程式的基本單元,系統執行一個程式既是一個程序從建立、執行到消亡的過程。每個進行都擁有一個獨立的記憶體空間,一個應用程式可以執行多個程序(例如QQ就可以登入多個賬號)Windows任務管理器中可以檢視到當前系統執行的程序執行緒執行緒是程序的執行單元,負責當前程序中的任務執行,一個程序中至少會有一個執行緒。但是通常都是一個程序有多個執行緒,這樣的應用程式稱為多執行緒應用程式。每個執行緒都有獨立的棧空間一個Java程式就是一個程序,而一個程序一次只能執行一個執行緒,因此Java程式只有高併發,沒有高並行。即多個執行緒交替執行,沒有同時執行。Java程式執行時,內部同時有多條執行緒,而此時需要一個執行緒排程機制,Java執行緒的排程機制是搶佔式排程:優先讓優先順序高的執行緒使用CPU,如果執行緒的優先順序相同,那麼會隨機選擇一個執行緒執行。
建立執行緒的兩種方式在Java中java.lang.Thread類表示執行緒。如果想要建立執行緒就需要建立Thread類的物件
繼承Thread類,重寫run()方法PrintThread繼承了java.lang.Thread,並且重寫任務的方法@Log4j2註解主要用於日誌記錄,透過由於log42.xml檔案中配置了控制檯輸出的日誌會列印執行緒資訊,因此在程式執行時可以看到當前執行的執行緒名稱。
/** 自定義執行緒類PrintThread */@Log4j2class PrintThread extends Thread { /** 重寫父類的run()方法 run()方法就是執行緒執行的任務 */ @Override public void run() { for (int i = 0; i < 10; i++) { //PrintThread的run方法列印i的值 log.info("Print Thread i = "+i); } }}
然後在單元測試方法中建立執行緒物件,呼叫start()方法啟動執行緒執行執行緒的任務。而且主執行緒中也有和PrintThread一樣的任務在執行。
/** * 執行緒建立的方式1 繼承Thread類重寫run()方法 */ @Test public void testCreateThreadInherit() { //建立執行緒物件 PrintThread printThread=new PrintThread(); //啟動執行緒執行任務 printThread.start(); //主執行緒中列印0-9 for (int i = 0; i < 10; i++) { //主執行緒中列印i的值 log.info("Main Thread i = "+i); } }
程式執行結果從控制檯列印輸出的結果看出兩條執行緒是相互交替執行的。不過交替的時間非常短暫,給人的感覺幾乎同時執行。但是其本質還是序列的。
程式執行時會同時啟動至少兩條執行緒 main方法所在的執行緒是main執行緒,而PrintThread由於在建立的時沒有傳遞執行緒名引數,因此預設的執行緒名是Thread-0當主執行緒執行到printThread.start();時Thread-0執行緒啟動,並等待執行緒排程搶佔CPU資源,一旦搶佔到資源就會由JVM呼叫run方法執行任務,執行緒的run()方法執行完畢後表示執行緒的任務執行完畢,執行緒就會被回收。由於Java執行緒採用的排程機制是搶佔機制,因此main執行緒和Thread-0執行緒兩條執行緒會同時搶佔CPU資源,同一時刻只能有一條執行緒執行,而且每條執行緒有自己的單獨棧空間。當
實現java.lang.Runnable介面,實現run()方法自定義實現類CustomizationRunnable 實現Runnable介面
@Log4j2class CustomizationRunnable implements Runnable{ @Override public void run() { for (int i = 0; i < 10; i++) { //PrintThread的run方法列印i的值 log.info("CustomizationRunnable Thread i = "+i); } }}
然後在測試方法中建立Thread物件,並在Thread構造器中傳入CustomizationRunnable的匿名物件
/** * 執行緒建立的方式2:實現Runnable介面,重寫run方法 */ @Test public void testCreateThreadImplRunnable(){ Thread thread=new Thread(new CustomizationRunnable()); thread.start(); for (int i = 0; i < 10; i++) { //主執行緒中列印i的值 log.info("Main Thread i = "+i); } } }
程式執行結果和之前繼承Thread類重寫run()方法一樣,程式也是交替執行的。
Thread類提供了接收java.lang.Runnable介面的構造器去構造Thread物件,我們可以使用匿名內部類的方式,方便的實現每個執行緒執行不同的任務操作。
例如某個網站要統計年度使用者訪問總人數和統計年度使用者平均訪問時間,此時可以使用兩個Runnable介面的匿名內部類作為引數傳入Thread來建立Thread物件
/** * 使用匿名內部類構建Runnable物件建立執行緒 */ @Test public void testCreateThreadNonInnerClassRunnable(){ //建立Thread物件,直接傳入Runnable介面的匿名內部類作為引數傳入 Thread sumThread=new Thread(new Runnable() { @Override public void run() { log.info("統計年度使用者訪問總人數"); } }); sumThread.start(); Thread avgThread=new Thread(new Runnable() { @Override public void run() { log.info("統計年度使用者平均訪問時間"); } }); avgThread.start(); for (int i = 0; i < 10; i++) { // 主執行緒中列印i的值 log.info("Main Thread i = " + i); } } }
程式執行結果
繼承Thread類重寫run()和實現Runnable介面重寫run()方法兩種方式建立執行緒的區別
Java的類只能支援單繼承,但是可以實現多個介面。透過實現介面擴充套件性比較強,因此推薦使用實現Runnable介面,實現run()方法的方式建立執行緒繼承Thread類重寫run()方法的執行緒和任務繫結在一塊,而實現Runnable介面重寫run()方法的任務和執行緒是獨立的,增強程式健壯性,實現解耦操作。使用實現Runnable介面,實現run()方法建立執行緒,還可以適合多個相同業務執行緒共享同一個(Runnable)任務物件。執行緒池(ThreadPool)只會接收實現Runnable或者Callable介面的執行緒物件,不能直接放入繼承Thread類的物件Thread類常用方法Thread類常用的構造方法
public Thread(): 分配一個新的執行緒物件,執行緒名由系統給出public Thread(String name):分配一個新的執行緒物件,執行緒名由引數傳入public Thread(Runnable target):分配一個帶有指定目標的新的執行緒物件,Runnable是任務介面,執行緒名由系統給出。public Thread(Runnable target,String name):分配一個帶有指定目標的新的執行緒物件並指定名字Thread類的常用方法
public void start() :啟動執行緒,JVM呼叫該執行緒的run方法public void run():執行緒執行的任務public String getName(): 獲取當前執行緒名稱public static void sleep(long millis):使當前執行的執行緒以指定的毫秒數暫停,暫停任務執行public static Thread currentThread() :返回當前正在執行執行緒物件的引用我們可以使用Thread提供的API把之前寫的程式改造下
相比之前的PrintThread
增加了兩個構造器,其中一個有參構造器可以在建立Thread物件時可以傳遞執行緒的名稱run方法中使用super.getName()獲取當前的執行緒名稱run方法中使用Thread.sleep(1000)讓PrintThread在每次列印i的值後暫停1秒,該方法有個編譯異常,異常型別是InterruptedException,這裡使用try/catch捕獲異常,並在catch處理異常,主要是列印異常堆疊資訊/** 自定義執行緒類PrintThread */@Log4j2class PrintThread extends Thread { PrintThread(){} PrintThread(String threadName){ super(threadName); } /** 重寫父類的run()方法 run()方法就是執行緒執行的任務 */ @Override public void run() { for (int i = 0; i < 10; i++) { // PrintThread的run方法列印i的值 //getName()獲取當前執行的執行緒名稱 log.info(super.getName()+"i = " + i); try { //當前執行緒暫停1秒 System.out.println(Thread.currentThread().getName()+"暫停1秒"); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }}
相比之前的testCreateThreadInherit()方法
在建立執行緒物件時指定了執行緒的名稱在main執行緒中列印i的值後也使用Thread.sleep(1000)暫停1秒透過Thread.currentThread()獲取當前執行執行緒的物件 /** 執行緒建立的方式1 繼承Thread類重寫run()方法 */ @Test public void testCreateThreadInherit() { // 建立執行緒物件 PrintThread printThread = new PrintThread("PrintThread"); // 啟動執行緒執行任務 printThread.start(); // 主執行緒中列印0-9 for (int i = 0; i < 10; i++) { // 主執行緒中列印i的值 log.info(Thread.currentThread().getName()+" i = " + i); try { System.out.println(Thread.currentThread().getName()+"暫停1秒"); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }
程式執行結果