Java學習多執行緒第一天

  • 2019 年 10 月 3 日
  • 筆記

內容介紹

  • Thread
  • 執行緒創建
  • 執行緒池
  • 執行緒狀態圖

1 多執行緒

1.1     多執行緒介紹

學習多執行緒之前,我們先要了解幾個關於多執行緒有關的概念。

進程:進程指正在運行的程式。確切的來說,當一個程式進入記憶體運行,即變成一個進程,進程是處於運行過程中的程式,並且具有一定獨立功能。

 

執行緒:執行緒是進程中的一個執行單元,負責當前進程中程式的執行,一個進程中至少有一個執行緒。一個進程中是可以有多個執行緒的,這個應用程式也可以稱之為多執行緒程式。

簡而言之:一個程式運行後至少有一個進程,一個進程中可以包含多個執行緒

 

 什麼是多執行緒呢?即就是一個程式中有多個執行緒在同時執行。

通過下圖來區別單執行緒程式與多執行緒程式的不同:

  •  單執行緒程式:即,若有多個任務只能依次執行。當上一個任務執行結束後,下一個任務開始執行。如,去網吧上網,網吧只能讓一個人上網,當這個人下機後,下一個人才能上網。
  •  多執行緒程式:即,若有多個任務可以同時執行。如,去網吧上網,網吧能夠讓多個人同時上網。

 

1.2     程式運行原理

  •   分時調度

所有執行緒輪流使用 CPU 的使用權,平均分配每個執行緒佔用 CPU 的時間。

  •  搶佔式調度

優先讓優先順序高的執行緒使用 CPU,如果執行緒的優先順序相同,那麼會隨機選擇一個(執行緒隨機性),Java使用的為搶佔式調度。

1.2.1    搶佔式調度詳解

大部分作業系統都支援多進程並發運行,現在的作業系統幾乎都支援同時運行多個程式。比如:現在我們上課一邊使用編輯器,一邊使用錄屏軟體,同時還開著畫圖板,dos窗口等軟體。此時,這些程式是在同時運行,”感覺這些軟體好像在同一時刻運行著“。

 

實際上,CPU(中央處理器)使用搶佔式調度模式在多個執行緒間進行著高速的切換。對於CPU的一個核而言,某個時刻,只能執行一個執行緒,而 CPU的在多個執行緒間切換速度相對我們的感覺要快,看上去就是在同一時刻運行。

其實,多執行緒程式並不能提高程式的運行速度,但能夠提高程式運行效率,讓CPU的使用率更高。

1.3     主執行緒

回想我們以前學習中寫過的程式碼,當我們在dos命令行中輸入java空格類名回車後,啟動JVM,並且載入對應的class文件。虛擬機並會從main方法開始執行我們的程式程式碼,一直把main方法的程式碼執行結束。如果在執行過程遇到循環時間比較長的程式碼,那麼在循環之後的其他程式碼是不會被馬上執行的。如下程式碼演示:

class Demo{      String name;      Demo(String name){          this.name = name;      }      void show()    {          for (int i=1;i<=10000 ;i++ )        {              System.out.println("name="+name+",i="+i);          }      }  }    class ThreadDemo {      public static void main(String[] args)     {          Demo d = new Demo("小強");           Demo d2 = new Demo("旺財");          d.show();          d2.show();          System.out.println("Hello World!");      }  }

View Code

若在上述程式碼中show方法中的循環執行次數很多,這時在d.show();下面的程式碼是不會馬上執行的,並且在dos窗口會看到不停的輸出name=小強,i=值,這樣的語句。為什麼會這樣呢?

原因是:jvm啟動後,必然有一個執行路徑(執行緒)從main方法開始的,一直執行到main方法結束,這個執行緒在java中稱之為主執行緒。當程式的主執行緒執行時,如果遇到了循環而導致程式在指定位置停留時間過長,則無法馬上執行下面的程式,需要等待循環結束後能夠執行。

那麼,能否實現一個主執行緒負責執行其中一個循環,再由另一個執行緒負責其他程式碼的執行,最終實現多部分程式碼同時執行的效果?

能夠實現同時執行,通過Java中的多執行緒技術來解決該問題。

1.4     Thread類

該如何創建執行緒呢?通過API中搜索,查到Thread類。通過閱讀Thread類中的描述。Thread是程式中的執行執行緒。Java 虛擬機允許應用程式並發地運行多個執行執行緒。

 通過查看API可以發現創建新執行執行緒有兩種方法。

  •  一種方法是將類聲明為 Thread 的子類。該子類應重寫 Thread 類的 run 方法。創建對象,開啟執行緒。run方法相當於其他執行緒的main方法。
  •  另一種方法是聲明一個實現 Runnable 介面的類。該類然後實現 run 方法。然後創建Runnable的子類對象,傳入到某個執行緒的構造方法中,開啟執行緒。

1.5     創建執行緒方式一繼承Thread類

創建執行緒的步驟:

  1 定義一個類繼承Thread。

  2 重寫run方法。

  3 創建子類對象,就是創建執行緒對象。

  4 調用start方法,開啟執行緒並讓執行緒執行,同時還會告訴jvm去調用run方法。

  測試類

public class Demo01 {      public static void main(String[] args) {          //創建自定義執行緒對象          MyThread mt = new MyThread("新的執行緒!");          //開啟新執行緒          mt.start();          //在主方法中執行for循環          for (int i = 0; i < 10; i++) {              System.out.println("main執行緒!"+i);          }      }  }

  自定義執行緒類

public class MyThread extends Thread {      //定義指定執行緒名稱的構造方法      public MyThread(String name) {          //調用父類的String參數的構造方法,指定執行緒的名稱          super(name);      }      /**       * 重寫run方法,完成該執行緒執行的邏輯       */      @Override      public void run() {          for (int i = 0; i < 10; i++) {              System.out.println(getName()+":正在執行!"+i);          }      }  }

思考:執行緒對象調用 run方法和調用start方法區別?

執行緒對象調用run方法不開啟執行緒。僅是對象調用方法。執行緒對象調用start開啟執行緒,並讓jvm調用run方法在開啟的執行緒中執行。

1.5.1    繼承Thread類原理

我們為什麼要繼承Thread類,並調用其的start方法才能開啟執行緒呢?

繼承Thread類:因為Thread類用來描述執行緒,具備執行緒應該有功能。那為什麼不直接創建Thread類的對象呢?如下程式碼:

Thread t1 = new Thread();  t1.start();//這樣做沒有錯,但是該start調用的是Thread類中的run方法,而這個run方法沒有做什麼事情,更重要的是這個run方法中並沒有定義我們需要讓執行緒執行的程式碼。

創建執行緒的目的是什麼?

是為了建立程式單獨的執行路徑,讓多部分程式碼實現同時執行。也就是說執行緒創建並執行需要給定執行緒要執行的任務。

對於之前所講的主執行緒,它的任務定義在main函數中。自定義執行緒需要執行的任務都定義在run方法中。

Thread類run方法中的任務並不是我們所需要的,只有重寫這個run方法。既然Thread類已經定義了執行緒任務的編寫位置(run方法),那麼只要在編寫位置(run方法)中定義任務程式碼即可。所以進行了重寫run方法動作。

1.5.2    多執行緒的記憶體圖解

多執行緒執行時,到底在記憶體中是如何運行的呢?

以上個程式為例,進行圖解說明:

多執行緒執行時,在棧記憶體中,其實每一個執行執行緒都有一片自己所屬的棧記憶體空間。進行方法的壓棧和彈棧。

當執行執行緒的任務結束了,執行緒自動在棧記憶體中釋放了。但是當所有的執行執行緒都結束了,那麼進程就結束了。

1.5.3    獲取執行緒名稱

開啟的執行緒都會有自己的獨立運行棧記憶體,那麼這些運行的執行緒的名字是什麼呢?該如何獲取呢?既然是執行緒的名字,按照面向對象的特點,是哪個對象的屬性和誰的功能,那麼我們就去找那個對象就可以了。查閱Thread類的API文檔發現有個方法是獲取當前正在運行的執行緒對象。還有個方法是獲取當前執行緒對象的名稱。既然找到了,我們就可以試試。

  •   Thread.currentThread()獲取當前執行緒對象
  •   Thread.currentThread().getName();獲取當前執行緒對象的名稱

class MyThread extends Thread {  //繼承Thread      MyThread(String name){          super(name);      }      //複寫其中的run方法      public void run(){          for (int i=1;i<=20 ;i++ ){              System.out.println(Thread.currentThread().getName()+",i="+i);          }      }  }  class ThreadDemo {      public static void main(String[] args)     {          //創建兩個執行緒任務          MyThread d = new MyThread();          MyThread d2 = new MyThread();          d.run();//沒有開啟新執行緒, 在主執行緒調用run方法          d2.start();//開啟一個新執行緒,新執行緒調用run方法      }  }

View Code

1.6     創建執行緒方式—實現Runnable介面

創建執行緒的另一種方法是聲明實現 Runnable 介面的類。該類然後實現 run 方法。然後創建Runnable的子類對象,傳入到某個執行緒的構造方法中,開啟執行緒。

為何要實現Runnable介面,Runable是啥玩意呢?繼續API搜索。

查看Runnable介面說明文檔:Runnable介面用來指定每個執行緒要執行的任務。包含了一個 run 的無參數抽象方法,需要由介面實現類重寫該方法。

創建執行緒的步驟。

  1、定義類實現Runnable介面。

  2、覆蓋介面中的run方法。。

  3、創建Thread類的對象

  4、將Runnable介面的子類對象作為參數傳遞給Thread類的構造函數。

  5、調用Thread類的start方法開啟執行緒。

  •  程式碼演示:
public class Demo02 {      public static void main(String[] args) {          //創建執行緒執行目標類對象          Runnable runn = new MyRunnable();          //將Runnable介面的子類對象作為參數傳遞給Thread類的構造函數          Thread thread = new Thread(runn);          Thread thread2 = new Thread(runn);          //開啟執行緒          thread.start();          thread2.start();          for (int i = 0; i < 10; i++) {              System.out.println("main執行緒:正在執行!"+i);          }      }  }

  •   自定義執行緒執行任務類
public class MyRunnable implements Runnable{        //定義執行緒要執行的run方法邏輯      @Override      public void run() {            for (int i = 0; i < 10; i++) {              System.out.println("我的執行緒:正在執行!"+i);          }      }  }

1.6.1    實現Runnable的原理

為什麼需要定一個類去實現Runnable介面呢?繼承Thread類和實現Runnable介面有啥區別呢?

實現Runnable介面,避免了繼承Thread類的單繼承局限性。覆蓋Runnable介面中的run方法,將執行緒任務程式碼定義到run方法中。

創建Thread類的對象,只有創建Thread類的對象才可以創建執行緒。執行緒任務已被封裝到Runnable介面的run方法中,而這個run方法所屬於Runnable介面的子類對象,所以將這個子類對象作為參數傳遞給Thread的構造函數,這樣,執行緒對象創建時就可以明確要運行的執行緒的任務。

1.6.2    實現Runnable的好處

第二種方式實現Runnable介面避免了單繼承的局限性,所以較為常用。實現Runnable介面的方式,更加的符合面向對象,執行緒分為兩部分,一部分執行緒對象,一部分執行緒任務。繼承Thread類,執行緒對象和執行緒任務耦合在一起。一旦創建Thread類的子類對象,既是執行緒對象,有又有執行緒任務。實現runnable介面,將執行緒任務單獨分離出來封裝成對象,類型就是Runnable介面類型。Runnable介面對執行緒對象和執行緒任務進行解耦。

1.7     執行緒的匿名內部類使用

使用執行緒的內匿名內部類方式,可以方便的實現每個執行緒執行不同的執行緒任務操作。

  •  方式1:創建執行緒對象時,直接重寫Thread類中的run方法
new Thread() {              public void run() {                  for (int x = 0; x < 40; x++) {                      System.out.println(Thread.currentThread().getName()                              + "...X...." + x);                  }              }          }.start();

  •  方式2:使用匿名內部類的方式實現Runnable介面,重新Runnable介面中的run方法
Runnable r = new Runnable() {              public void run() {                  for (int x = 0; x < 40; x++) {                      System.out.println(Thread.currentThread().getName()                              + "...Y...." + x);                  }              }          };          new Thread(r).start();

2  執行緒池

2.1     執行緒池概念

執行緒池,其實就是一個容納多個執行緒的容器,其中的執行緒可以反覆使用,省去了頻繁創建執行緒對象的操作,無需反覆創建執行緒而消耗過多資源。

我們詳細的解釋一下為什麼要使用執行緒池?

在java中,如果每個請求到達就創建一個新執行緒,開銷是相當大的。在實際使用中,創建和銷毀執行緒花費的時間和消耗的系統資源都相當大,甚至可能要比在處理實際的用戶請求的時間和資源要多的多。除了創建和銷毀執行緒的開銷之外,活動的執行緒也需要消耗系統資源。如果在一個jvm里創建太多的執行緒,可能會使系統由於過度消耗記憶體或“切換過度”而導致系統資源不足。為了防止資源不足,需要採取一些辦法來限制任何給定時刻處理的請求數目,儘可能減少創建和銷毀執行緒的次數,特別是一些資源耗費比較大的執行緒的創建和銷毀,盡量利用已有對象來進行服務。

執行緒池主要用來解決執行緒生命周期開銷問題和資源不足問題。通過對多個任務重複使用執行緒,執行緒創建的開銷就被分攤到了多個任務上了,而且由於在請求到達時執行緒已經存在,所以消除了執行緒創建所帶來的延遲。這樣,就可以立即為請求服務,使用應用程式響應更快。另外,通過適當的調整執行緒中的執行緒數目可以防止出現資源不足的情況。

2.2     使用執行緒池方式–Runnable介面

通常,執行緒池都是通過執行緒池工廠創建,再調用執行緒池中的方法獲取執行緒,再通過執行緒去執行任務方法。                                                                                                                               

  •   Executors:執行緒池創建工廠類

  public static ExecutorService newFixedThreadPool(int nThreads):返回執行緒池對象

  •   ExecutorService:執行緒池類

  Future<?> submit(Runnable task):獲取執行緒池中的某一個執行緒對象,並執行

  •   Future介面:用來記錄執行緒任務執行完畢後產生的結果。執行緒池創建與使用
  •   使用執行緒池中執行緒對象的步驟:

  創建執行緒池對象

  創建Runnable介面子類對象

  提交Runnable介面子類對象

  關閉執行緒池

程式碼:

public class ThreadPoolDemo {      public static void main(String[] args) {          //創建執行緒池對象          ExecutorService service = Executors.newFixedThreadPool(2);//包含2個執行緒對象          //創建Runnable實例對象          MyRunnable r = new MyRunnable();            //自己創建執行緒對象的方式          //Thread t = new Thread(r);          //t.start(); ---> 調用MyRunnable中的run()            //從執行緒池中獲取執行緒對象,然後調用MyRunnable中的run()          service.submit(r);          //再獲取個執行緒對象,調用MyRunnable中的run()          service.submit(r);          service.submit(r);  //注意:submit方法調用結束後,程式並不終止,是因為執行緒池控制了執行緒的關閉。將使用完的執行緒又歸還到了執行緒池中    //關閉執行緒池          //service.shutdown();      }  }

  •   Runnable介面實現類
public class MyRunnable implements Runnable {      @Override      public void run() {          System.out.println("我要一個教練");            try {              Thread.sleep(2000);          } catch (InterruptedException e) {              e.printStackTrace();          }          System.out.println("教練來了: " +Thread.currentThread().getName());          System.out.println("教我游泳,交完後,教練回到了游泳池");      }  }

2.3     使用執行緒池方式—Callable介面

  •   Callable介面:與Runnable介面功能相似,用來指定執行緒的任務。其中的call()方法,用來返回執行緒任務執行完畢後的結果,call方法可拋出異常。
  •   ExecutorService:執行緒池類

  <T> Future<T> submit(Callable<T> task):獲取執行緒池中的某一個執行緒對象,並執行執行緒中的call()方法

  •   Future介面:用來記錄執行緒任務執行完畢後產生的結果。執行緒池創建與使用
  •   使用執行緒池中執行緒對象的步驟:

  創建執行緒池對象

  創建Callable介面子類對象

  提交Callable介面子類對象

  關閉執行緒池

程式碼:

public class ThreadPoolDemo {      public static void main(String[] args) {          //創建執行緒池對象          ExecutorService service = Executors.newFixedThreadPool(2);//包含2個執行緒對象          //創建Callable對象          MyCallable c = new MyCallable();            //從執行緒池中獲取執行緒對象,然後調用MyRunnable中的run()          service.submit(c);            //再獲取個教練          service.submit(c);          service.submit(c);  //注意:submit方法調用結束後,程式並不終止,是因為執行緒池控制了執行緒的關閉。將使用完的執行緒又歸還到了執行緒池中    //關閉執行緒池          //service.shutdown();      }  }

  •  Callable介面實現類,call方法可拋出異常、返回執行緒任務執行完畢後的結果
public class MyCallable implements Callable {      @Override      public Object call() throws Exception {          System.out.println("我要一個教練:call");          Thread.sleep(2000);          System.out.println("教練來了: " +Thread.currentThread().getName());          System.out.println("教我游泳,交完後,教練回到了游泳池");          return null;      }  }

2.4     執行緒池練習:返回兩個數相加的結果

要求:通過執行緒池中的執行緒對象,使用Callable介面完成兩個數求和操作

  •   Future介面:用來記錄執行緒任務執行完畢後產生的結果。執行緒池創建與使用

  V get() 獲取Future對象中封裝的數據結果

程式碼:

public class ThreadPoolDemo {      public static void main(String[] args) throws InterruptedException, ExecutionException {          //創建執行緒池對象          ExecutorService threadPool = Executors.newFixedThreadPool(2);            //創建一個Callable介面子類對象          //MyCallable c = new MyCallable();          MyCallable c = new MyCallable(100, 200);          MyCallable c2 = new MyCallable(10, 20);            //獲取執行緒池中的執行緒,調用Callable介面子類對象中的call()方法, 完成求和操作          //<Integer> Future<Integer> submit(Callable<Integer> task)          // Future 結果對象          Future<Integer> result = threadPool.submit(c);          //此 Future 的 get 方法所返回的結果類型          Integer sum = result.get();          System.out.println("sum=" + sum);            //再演示          result = threadPool.submit(c2);          sum = result.get();          System.out.println("sum=" + sum);          //關閉執行緒池(可以不關閉)        }  }

  •   Callable介面實現類
public class MyCallable implements Callable<Integer> {      //成員變數      int x = 5;      int y = 3;      //構造方法      public MyCallable(){      }      public MyCallable(int x, int y){          this.x = x;          this.y = y;      }        @Override      public Integer call() throws Exception {          return x+y;      }  }