java基礎(26):Thread、線程創建、線程池

来源:https://www.cnblogs.com/liuhui0308/archive/2019/10/11/11657077.html
-Advertisement-
Play Games

1. 多線程 1.1 多線程介紹 學習多線程之前,我們先要瞭解幾個關於多線程有關的概念。 進程:進程指正在運行的程式。確切的來說,當一個程式進入記憶體運行,即變成一個進程,進程是處於運行過程中的程式,並且具有一定獨立功能。 線程:線程是進程中的一個執行單元,負責當前進程中程式的執行,一個進程中至少有一 ...


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!");
    }
}

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

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

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

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

1.4 Thread類

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

 

 

構造方法

 

 

常用方法

 

 

 

繼續閱讀,發現創建新執行線程有兩種方法。

一種方法是將類聲明為 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方法
    }
}

通過結果觀察,原來主線程的名稱:main;自定義的線程:Thread-0,線程多個時,數字順延。如Thread-1......

進行多線程編程時,不要忘記了Java程式運行是從主線程開始,main方法就是主線程的線程執行內容。

1.6 創建線程方式—實現Runnable介面

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

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

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

 

 

介面中的方法

 

 

Thread類構造方法

 

 

 

創建線程的步驟。

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;
    }
}

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 在開始本篇文章之前,我想你對SpringCloud和SpringBoot的基本使用已經比較熟悉了,如果不熟悉的話可以參考我之前寫過的文章 本篇文章的源碼基於SpringBoot2.0,SpringCloud的Finchley.RELEASE 註解 我們知道,在使用Eureka作為註冊中心的時候,我們 ...
  • 位運算在redis中非常的方便使用,並且理由利用這個可以實現很多特殊的功能。這也迫使我去研究更多的redis提供的函數,只有研究的多,思路才能夠更加開放。今天我就對strings下麵的幾個函數進行了測試,也收穫頗豐。 使用setBit和bitCount可以實現用戶活躍天數的統計,大體的思路如下:我們 ...
  • #include<stdio.h>int main(){ double i; double bonus1,bonus2,bonus4,bonus6,bonus10,bonus; printf("你的利潤是:\n"); scanf("%lf",&i); bonus1=100000*0.1; bonus ...
  • Thymeleaf中有許多內置對象,可以在模板中實現各種功能。 下麵有幾個基本對象。 Web對象常用有:request、session、servletContext。 Thymeleaf提供了幾個內置變數param、session、application,分別可以訪問請求參數、session屬... ...
  • 手工操作 —— 穿孔卡片 1946年第一臺電腦誕生--20世紀50年代中期,電腦工作還在採用手工操作方式。此時還沒有操作系統的概念。 程式員將對應於程式和數據的已穿孔的紙帶(或卡片)裝入輸入機,然後啟動輸入機把程式和數據輸入電腦記憶體,接著通過控制台開關啟動程式針對數據運行;計算完畢,印表機輸出 ...
  • 一、基本介紹 logging 模塊是python自帶的一個包,因此在使用的時候,不必安裝,只需要import即可。 logging有 5 個不同層次的日誌級別,可以將給定的 logger 配置為這些級別: DEBUG:詳細信息,用於診斷問題。Value=10。 INFO:確認代碼運行正常。Value ...
  • 1. 多線程 如果有多個線程在同時運行,而這些線程可能會同時運行這段代碼。程式每次運行結果和單線程運行的結果是一樣的,而且其他的變數的值也和預期的是一樣的,就是線程安全的。 我們通過一個案例,演示線程的安全問題: 電影院要賣票,我們模擬電影院的賣票過程。假設要播放的電影是 “功夫熊貓3”,本次電影的 ...
  • 線程 什麼是線程 官方定義: 線程(thread)是操作系統能夠進行運算調度的最小單位。它被包含在進程之中,是進程中的實際運作單位。一條線程指的是進程中一個單一順序的控制流,一個進程中可以併發多個線程,每條線程並行執行不同的任務。 說人話: 假如 進程 是保潔公司, 線程 就是公司的員工。當公司接到 ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...