1. 程式人生 > >Java多執行緒01(Thread類、執行緒建立、執行緒池)

Java多執行緒01(Thread類、執行緒建立、執行緒池)

Java多執行緒(Thread類、執行緒建立、執行緒池)

第一章 多執行緒

1.1 多執行緒介紹

1.1.1 基本概念

  • 程序:程序指正在執行的程式。確切的來說,當一個程式進入記憶體執行,即變成一個程序,程序是處於執行過程中的程式,並且具有一定獨立功能。
  • 執行緒:執行緒是程序中的一個執行單元,負責當前程序中程式的執行,一個程序中至少有一個執行緒。一個程序中是可以有多個執行緒的,這個應用程式也可以稱之為多執行緒程式。
  • 簡而言之:一個程式執行後至少有一個程序,一個程序中可以包含多個執行緒

1.1.2 單執行緒程式

- 從入口main到結束,一條路走到底
- 好處:沒有安全隱患
- 缺點:效率低
- 解決缺點:讓方法一同執行起來,為程式開啟多個執行的路,每個執行的路,成為執行緒。
- 示例程式碼:
    ```
    public static void main(String[] args){
        add();
        remove();
        get();
        System.out.println(222);
    }

    public static void add(){
        // 一萬次迴圈
    }
    public static void remove(){
    }
    public static void get(){
    }
    ```

1.1.3 深入理解多執行緒

  • CPU中央處理器
    • Inter AMD
    • Inter Core i7 6680M
    • 四核心,八執行緒
    • 執行多執行緒時,每個功能都可以單獨執行,開啟功能,對CPU開啟新的執行路徑。
    • 執行緒深入理解:每個功能對於CPU的獨立執行路徑,就是執行緒。
  • 網上下載一個軟體:
    • 下載的流程:
    • 單執行緒下載:一次讀取n個位元組
      • 瀏覽器下載:(IE)寫一個,讀一個,寫一個,讀一個
    • 多執行緒下載:一次讀取m*m個位元組,多(m)個執行緒讀取同一個檔案
      • 迅雷:開啟一個執行緒,讀取其中的一部分,再開啟一個執行緒再讀取一部分,...

1.2 程式執行原理

  • 分時排程:
    • 所有執行緒輪流使用 CPU 的使用權,平均分配每個執行緒佔用 CPU 的時間。
  • 搶佔式排程:
    • 優先讓優先順序高的執行緒使用 CPU,如果執行緒的優先順序相同,那麼會隨機選擇一個(執行緒隨機性),Java使用的為搶佔式排程。

搶佔式排程詳解

大部分作業系統都支援多程序併發執行,現在的作業系統幾乎都支援同時執行多個程式。比如:現在我們上課一邊使用編輯器,一邊使用錄屏軟體,同時還開著畫圖板, dos視窗等軟體。此時,這些程式是在同時執行,”感覺這些軟體好像在同一時刻執行著“。
實際上,CPU(中央處理器)使用搶佔式排程模式在多個執行緒間進行著高速的切換。對於CPU的一個核而言,某個時刻,只能執行一個執行緒,而 CPU的在多個執行緒間切換速度相對我們的感覺要快,看上去就是在同一時刻執行。
其實,多執行緒程式並不能提高程式的執行速度,但能夠提高程式執行效率,讓CPU的使用率更高。

1.3 主執行緒

  • 程式碼示例:
public class Demo {
    public static void main(String[] args) {
        fun();
        System.out.println(Math.abs(-9));
    }
    public static void fun() {
        for(int i=0;i<10000; i++) {
            System.out.println(i);
        }
    }
}
  • 分析:
    • 程式:從上到下的執行過程
    • 在dos視窗中:java Demo
    • 啟動JVM,執行Demo.main
    • JVM 執行方法main,作業系統開啟執行緒
    • 對於CPU有了一個執行的路徑,執行方法main路徑,有個名字“main”
    • 主執行緒:
  • 思考:
    • 能否實現一個主執行緒負責執行其中一個迴圈,再由另一個執行緒負責其他程式碼的執行,最終實現多部分程式碼同時執行的效果?
    • 能夠實現同時執行,通過Java中的多執行緒技術來解決該問題。

1.4 Thread類

  • 通過API中搜索,查到Thread類。通過閱讀Thread類中的描述。Thread是程式中的執行執行緒。Java 虛擬機器允許應用程式併發地執行多個執行執行緒。
java.lang
Class Thread
java.lang.Object 
    java.lang.Thread 

All Implemented Interfaces: 
Runnable 
Direct Known Subclasses: 
ForkJoinWorkerThread 

    執行緒是程式中執行的執行緒。 Java虛擬機器允許應用程式同時執行多個執行執行緒。
    每個執行緒都有優先權。 具有較高優先順序的執行緒優先於具有較低優先順序的執行緒執行。 每個執行緒可能也可能不會被標記為守護程序。
    當在某個執行緒中執行的程式碼建立一個新的Thread物件時,新執行緒的優先順序最初設定為等於建立執行緒的優先順序,並且當且僅當建立執行緒是守護程序時才是守護程序執行緒。

- 常用構造方法
Thread() 
    Allocates a new Thread object. 
Thread(Runnable target) 
    Allocates a new Thread object. 
Thread(Runnable target, String name) 
    Allocates a new Thread object. 
Thread(String name) 
    Allocates a new Thread object. 

- 常用方法
void run() 
    If this thread was constructed using a separate Runnable run object, then that Runnable object's run method is called; otherwise, this method does nothing and returns. 
static void sleep(long millis) 
    Causes the currently executing thread to sleep (temporarily cease execution) for the specified number of milliseconds, subject to the precision and accuracy of system timers and schedulers. 
void start() 
    Causes this thread to begin execution; the Java Virtual Machine calls the run method of this thread. 
  • 建立新執行執行緒有兩種方法:
    • 一種方法是將類宣告為 Thread 的子類。該子類應重寫 Thread 類的 run 方法。建立物件,開啟執行緒。run方法相當於其他執行緒的main方法。

      class PrimeThread extends Thread {
          long minPrime;
          PrimeThread(long minPrime) {
              this.minPrime = minPrime;
          }
      
          public void run() {
              // compute primes larger than minPrime
              . . .
          }
      }
    • 另一種方法是宣告一個實現 Runnable 介面的類。該類然後實現 run 方法。然後建立Runnable的子類物件,傳入到某個執行緒的構造方法中,開啟執行緒。

      class PrimeRun implements Runnable {
          long minPrime;
          PrimeRun(long minPrime) {
              this.minPrime = minPrime;
          }
      
          public void run() {
              // compute primes larger than minPrime
               . . .
          }
      }
  • 兩種方法的區別;

1.5 建立新執行執行緒方法一:繼承Thread類

  • 建立執行緒的步驟:
    1. 定義一個類繼承Thread。
    2. 重寫run方法。
    3. 建立子類物件,就是建立執行緒物件。
    4. 呼叫start方法,開啟執行緒並讓執行緒執行,同時還會告訴jvm去呼叫run方法。
public class SubThread extends Thread {
    @Override
    public void run() {
        for(int i=0; i<50; i++) {
            System.out.println("run方法中的變數:"+i);
        }       
    }
}

public class ThreadDemo {   
    public static void main(String[] args) {   // 1、JVM從入口main開始執行主執行緒,從CPU開啟第一條執行緒路徑,執行main()方法。
        SubThread subThread = new SubThread();    // 2、建立執行緒物件,相當於開啟了一個新的執行緒,從CPU開啟第二條執行緒路徑,執行run()方法。
        subThread.start();   // 3、執行start方法時,呼叫run()方法,與main()方法同時要呼叫CPU,兩個執行路徑都會被CPU執行,CPU自己選擇的權利,出現執行結果,隨機性結果。
        for(int i=0; i<50; i++) {
            System.out.println("main方法中的變數:"+i);
        }
    }
}

1.5.1 繼承Thread類原理

  • 思考1:執行緒物件呼叫 run方法和呼叫start方法區別?
    • 執行緒物件呼叫run方法不開啟執行緒。僅是物件呼叫方法。執行緒物件呼叫start開啟執行緒,並讓jvm呼叫run方法在開啟的執行緒中執行。
  • 思考2:我們為什麼要繼承Thread類,並呼叫其的start方法才能開啟執行緒呢?
    • 繼承Thread類:因為Thread類用來描述執行緒,具備執行緒應該有功能。
  • 思考3:那為什麼不直接建立Thread類的物件呢?如下程式碼:
    Thread t1 = new Thread();t1.start();
    • 這樣做沒有錯,但是該start呼叫的是Thread類中的run方法,而這個run方法沒有做什麼事情,更重要的是這個run方法中並沒有定義我們需要讓執行緒執行的程式碼。
  • 思考4:建立執行緒的目的是什麼?
    • 是為了建立程式單獨的執行路徑,讓多部分程式碼實現同時執行。也就是說執行緒建立並執行需要給定執行緒要執行的任務。
    • 對於之前所講的主執行緒,它的任務定義在main函式中。自定義執行緒需要執行的任務都定義在run方法中。
  • 思考5:為什麼要重寫run方法
    • Thread類run方法中的任務並不是我們所需要的,只有重寫這個run方法。既然Thread類已經定義了執行緒任務的編寫位置(run方法),那麼只要在編寫位置(run方法)中定義任務程式碼即可。所以進行了重寫run方法動作。

1.5.2 執行緒執行時的記憶體情況

  • 單執行緒:所有執行的方法都在一個棧中,後呼叫的方法先執行,執行結束後彈出;
  • 多執行緒:每呼叫一個執行緒,就分配一個棧區,例如:main和run方法分別屬於不同的棧區,CPU執行時,隨機選擇執行一個棧區中的方法。

1.5.3 獲取執行緒名字Thread類方法getName()

  • Java API 中

    String getName()  // 返回執行緒的名字 
    static Thread currentThread()  // 獲取當前執行緒
  • 示例:
public class NameThread extends Thread {
    @Override
    public void run() {
        System.out.println(super.getName());  // 輸出本執行緒的名字
    }
}

/*
 * 每個執行緒都有自己的名字
 * 執行方法main執行緒的名字是"main"
 * 其他執行緒也有名字,預設為"Thread-0","Thread-1", ...
 * 獲得主執行緒的名字的方法:JVM開啟主執行緒,執行方法main,主執行緒也是執行緒,是執行緒必然是Thread類的物件,Thread類中的靜態方法:
 *  static Thread currentThread() 返回正在執行的執行緒物件
 *  該物件呼叫getName方法,獲取執行緒名字
 */
public class ThreadDemo2 {
    public static void main(String[] args) {
        NameThread nameThread = new NameThread();
        nameThread.start();
        
        // 獲取主執行緒的執行緒名
        System.out.println(Thread.currentThread().getName());
    }
}

1.5.3 設定執行緒名字Thread類方法

  • Java API 中

    Thread(String name)  // 分配一個新的名字為name的物件
    void setName(String name) // 改變執行緒的名字為name
  • 方法一:

// 在主執行緒中
NameThread nameThread = new NameThread();
nameThread.setName("Thread執行緒名字");
  • 方法二:
// 在建立的執行緒類中的run方法中,呼叫Thread父類建構函式
super("Thread執行緒名字");

1.5.4 Thread類方法sleep()

  • 位置:可以寫在main方法中,也可以寫在Thread類中。
  • 程式碼:
public class SleepThread extends Thread{    
    @Override
    public void run() {
        for(int i=0; i<5; i++) {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(i);
        }
    }
}

public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        SleepThread sleepThread = new SleepThread();
        sleepThread.start();
        Thread.sleep(2000);
    }
}

1.6 建立新執行執行緒方法二:實現Runnable介面

建立執行緒的另一種方法是宣告實現 Runnable 介面的類。該類然後實現 run 方法。然後建立Runnable的子類物件,傳入到某個執行緒的構造方法中,開啟執行緒。
為何要實現Runnable介面,Runable是啥玩意呢?繼續API搜尋。
檢視Runnable介面說明文件:Runnable介面用來指定每個執行緒要執行的任務。包含了一個 run 的無引數抽象方法,需要由介面實現類重寫該方法。

  • Java API 中
// Runnable介面中只有一個run方法
// 方法摘要
void run() // 當使用實現介面Runnable的物件來建立執行緒時,啟動該執行緒會導致在該單獨執行的執行緒中呼叫該物件的run方法。

// 建立執行緒使用Thread類中的構造方法:
Thread(Runnable target)  // 分配一個執行緒物件,引數為Runnable介面實現類的物件
  • 建立執行緒的步驟。
    1. 定義類實現Runnable介面。
    2. 覆蓋介面中的run方法。。
    3. 建立Thread類的物件
    4. 將Runnable介面的子類物件作為引數傳遞給Thread類的建構函式。
    5. 呼叫Thread類的start方法開啟執行緒。
  • 示例:
public class SubRunnable implements Runnable{
    @Override
    public void run() {
        for(int i=0; i<50; i++) {
            System.out.println("run..."+i);
        }       
    }
}

/**
 * 實現介面方式的執行緒
 * 建立Thread類物件,構造方法中,傳遞Runnable介面實現類物件
 * 呼叫Thread類的方法start
 */
public class RunnableDemo {
    public static void main(String[] args) {
        SubRunnable subRunnable = new SubRunnable();
        Thread t = new Thread(subRunnable);
        t.start();  
        for(int i=0; i<50; i++) {
            System.out.println("main..."+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方法
    • 方式2:使用匿名內部類的方式實現Runnable介面,重新Runnable介面中的run方法

1.8 執行緒狀態(6種)

  • 執行緒狀態。 執行緒可以處於以下狀態之一:
    • new:尚未啟動的執行緒處於此狀態。
    • runnable:在Java虛擬機器中執行的執行緒處於此狀態。
    • blocked:被阻塞等待監視器鎖定的執行緒處於此狀態。
    • wait:無限期等待另一個執行緒執行特定操作的執行緒處於此狀態。
    • timed_waiting:正在等待另一個執行緒執行最多指定等待時間的操作的執行緒處於此狀態。
    • terminated:已退出的執行緒處於此狀態。
  • 執行緒在給定時間點只能處於一種狀態。 這些狀態是虛擬機器狀態,不反映任何作業系統執行緒狀態。

執行緒狀態圖:

第二章 執行緒池

2.1 執行緒池概念

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

執行緒池示意圖:

2.1.2 為什麼要使用執行緒池?

- 在java中,如果每個請求到達就建立一個新執行緒,開銷是相當大的。在實際使用中,建立和銷燬執行緒花費的時間和消耗的系統資源都相當大,甚至可能要比在處理實際的使用者請求的時間和資源要多的多。除了建立和銷燬執行緒的開銷之外,活動的執行緒也需要消耗系統資源。如果在一個JVM裡建立太多的執行緒,可能會使系統由於過度消耗記憶體或“切換過度”而導致系統資源不足。為了防止資源不足,需要採取一些辦法來限制任何給定時刻處理的請求數目,儘可能減少建立和銷燬執行緒的次數,特別是一些資源耗費比較大的執行緒的建立和銷燬,儘量利用已有物件來進行服務。
- 執行緒池主要用來解決執行緒生命週期開銷問題和資源不足問題。通過對多個任務重複使用執行緒,執行緒建立的開銷就被分攤到了多個任務上了,而且由於在請求到達時執行緒已經存在,所以消除了執行緒建立所帶來的延遲。這樣,就可以立即為請求服務,使用應用程式響應更快。另外,通過適當的調整執行緒中的執行緒數目可以防止出現資源不足的情況。

2.1.3 執行緒池原理

  • 自己建立執行緒池
ArrayList<Thread> threads = new ArrayList<Thread>();
threads.add(new Thread());
threads.add(new Thread());
threads.add(new Thread());
threads.add(new Thread());
threads.add(new Thread());
threads.add(new Thread());
threads.add(new Thread());
threads.add(new Thread());
threads.add(new Thread());
threads.add(new Thread());

// 程式一開始的時候,建立多個執行緒物件,儲存到集合中,需要執行緒,從集合中獲取執行緒出來
Thread t = threads.remove(0);
// 使用執行緒
t.start();
// 執行緒用完,回到容器中繼續等待使用
threads.add(t);
  • 從JDK5開始,內建執行緒池技術,不需要自己建立,直接使用即可。

2.2 使用執行緒池

2.2.1 使用執行緒池方式--Runnable介面

  • 通常,執行緒池都是通過執行緒池工廠建立,再呼叫執行緒池中的方法獲取執行緒,再通過執行緒去執行任務方法。
     - Executors:執行緒池建立工廠類
     - public static ExecutorService newFixedThreadPool(int nThreads):返回執行緒池物件
     - ExecutorService:執行緒池類
     - Future<?> submit(Runnable task):獲取執行緒池中的某一個執行緒物件,並執行
     - Future介面:用來記錄執行緒任務執行完畢後產生的結果。執行緒池建立與使用

  • 使用執行緒池中執行緒物件的步驟:
     - 建立執行緒池物件
     - 建立Runnable介面子類物件
     - 提交Runnable介面子類物件
     - 關閉執行緒池

  • 示例:

public class ThreadPoolRunnable implements Runnable{
    @Override
    public void run() {     
        System.out.println(new Thread().getName());
    }
}


import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/*
 * JDK1.5之後的新特性,實現執行緒池程式
 *  1、使用工廠類,Executors中的靜態方法建立執行緒物件,指定執行緒個數
 *  2、static ExecutorsService newFixedThreadPool(int 執行緒個數)   返回執行緒池物件
 *  3、返回的是 ExecutorsService介面的實現類(執行緒池物件)
 *  4、介面實現類物件,呼叫方法submit(Runnable r) 提交執行緒,執行任務
 */
public class ThreadPoolDemo {
    public static void main(String[] args) {
        // 呼叫工廠類的靜態方法,建立執行緒池物件
        // Executors.newFixedThreadPool(2);返回執行緒池物件,是介面的實現類物件
        ExecutorService es = Executors.newFixedThreadPool(2);
        // 呼叫介面實現類物件es的方法submit,提交一個執行緒任務
        es.submit(new ThreadPoolRunnable());
        es.submit(new ThreadPoolRunnable());
        es.submit(new ThreadPoolRunnable());
    }
}
  • Runnable介面的缺陷
    • 執行緒執行完沒有結果
    • 不能丟擲異常

2.2.2 使用執行緒池方式—Callable

  • Callable介面與Runnable介面功能相似,用來指定執行緒的任務。其中的call()方法,用來返回執行緒任務執行完畢後的結果,call方法可丟擲異常。
    • ExecutorService:執行緒池類
    • <T> Future<T> submit(Callable<T> task):獲取執行緒池中的某一個執行緒物件,並執行執行緒中的call()方法
    • Future介面:用來記錄執行緒任務執行完畢後產生的結果。執行緒池建立與使用
  • 使用執行緒池中執行緒物件的步驟:
     - 建立執行緒池物件
     - 建立Callable介面子類物件
     - 提交Callable介面子類物件
     - 關閉執行緒池

  • 示例:

import java.util.concurrent.Callable;

public class ThreadPoolCallable implements Callable<String>{
    @Override
    public String call() throws Exception {
        return "abc";
    }
}

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class ThreadPoolDemo {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ExecutorService es = Executors.newFixedThreadPool(2);
        // 提交任務的方法,返回Future介面的實現類
        Future<String> f = es.submit(new ThreadPoolCallable());
        String s = f.get();
        System.out.println(s);
    }
}

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

import java.util.concurrent.Callable;
/*
 * 多執行緒的非同步計算
 * 
 */
public class GetSumCallable implements Callable<Integer>{
    private int a;
    public GetSumCallable(int a) {
        this.a = a;
    }
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for(int i=0; i<=a; i++) {
            sum += i;
        }
        return sum;
    }   
}

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class ThreadPoolDemo {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ExecutorService es = Executors.newFixedThreadPool(2);
        Future<Integer> f1 = es.submit(new GetSumCallable(100));  // 加法
        Future<Integer> f2 = es.submit(new GetSumCallable(200));  // 減法
        System.out.println("1+2+...+100 = "+f1.get());
        System.out.println("1+2+...+200 = "+f2.get());
        es.shutdown();
    }
}

第三章 總結

建立執行緒的方式

  • 方式1,繼承Thread執行緒類
    • 步驟
      • 自定義類繼承Thread類
      • 在自定義類中重寫Thread類的run方法
      • 建立自定義類物件(執行緒物件)
      • 呼叫start方法,啟動執行緒,通過JVM,呼叫執行緒中的run方法
  • 方式2,實現Runnable介面
    • 步驟
      • 建立執行緒任務類 實現Runnable介面
      • 線上程任務類中 重寫介面中的run方法
      • 建立執行緒任務類物件
      • 建立執行緒物件,把執行緒任務類物件作為Thread類構造方法的引數使用
      • 呼叫start方法,啟動執行緒,通過JVM,呼叫執行緒任務類中的run方法
  • 方式3,實現Callable介面
    • 步驟
      • 工廠類Executors靜態方法newFixedThreadPool方法,建立執行緒物件
      • 執行緒池物件ExecutorService介面實現類,呼叫方法submit提交執行緒任務:submit(Callable c)