1. 程式人生 > >Java多線程01(Thread類、線程創建、線程池)

Java多線程01(Thread類、線程創建、線程池)

自己的 資源共享 nat pub start 守護 軟件 是我 運行速度

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)

Java多線程01(Thread類、線程創建、線程池)