多線程與高併發(一)多線程入門

1、基礎概念

多線程的學習從一些概念開始,進程和線程,併發與並行,同步與異步,高併發。編程

1.1 進程與線程

幾乎全部的操做系統都支持同時運行期多個任務,全部運行中的任務一般就是一個進程,進程是處於運行過程當中的程序,進程是操做系統進行資源分配和調度的一個獨立單位。多線程

進程有三個以下特徵:架構

  • 獨立性:進程是系統中獨立存在的實體,它能夠擁有本身獨立的資源,每個進程都擁有本身私有的地址空間。在沒有通過進程自己容許的狀況下,一個用戶進程不能夠直接訪問其餘進程的地址空間。併發

  • 動態性:進程與程序的區別在於,程序只是一個靜態的指令集合,而進程是一個正在系統中活動的指令集合。在進程中加入了時間的概念,進程具備本身的生命週期和各類不一樣的狀態,這些概念在程序中部是不具有的。異步

  • 併發性:多個進程能夠在單個處理器上併發執行,多個進程之間不會互相影響。分佈式

線程是進程的組成部分,一個進程能夠擁有多個線程,而線程必須有一個父進程,線程能夠有本身的堆棧、本身的程序計數器和本身的局部變量,但不擁有系統資源。好比使用QQ時,咱們能夠同事傳文件,發送圖片,聊天,這就是多個線程在進行。ide

線程能夠完成必定的任務,線程可以獨立運行的,它不知道有其餘線程的存在,線程的執行是搶佔式的,當前線程隨時可能被掛起。高併發

總之:一個程序運行後至少有一個進程,一個進程裏能夠有多個線程,但至少要有一個線程。學習

1.2 併發和並行

併發和並行是比較容易混淆的概念,他們都表示兩個或者多個任務一塊兒執行,但併發側重多個任務交替執行,同一時刻只能有一條指令執行,但多個進程指令被快速輪換執行,使得在宏觀上具備多個進程同時執行的效果。而並行確實真正的同時執行,有多條指令在多個處理器上同時執行,並行的前提條件就是多核CPU。this

1.3 同步和異步

同步和異步一般用來形容一次方法調用。同步方法調用一旦開始,調用者必須等到方法調用返回後,才能繼續後續的行爲。異步方法調用更像一個消息傳遞,一旦開始,方法調用就會當即返回,調用者能夠繼續後續的操做。

1.4 高併發

高併發通常是指在短期內遇到大量操做請求,很是具備表明性的場景是秒殺活動與搶票,高併發是互聯網分佈式系統架構設計中必須考慮的因素之一,高併發相關經常使用的一些指標有響應時間(Response Time),吞吐量(Throughput),每秒查詢率QPS(Query Per Second),併發用戶數等。

多線程在這裏只是在同/異步角度上解決高併發問題的其中的一個方法手段,是在同一時刻利用計算機閒置資源的一種方式

1.5 多線程的好處

線程在程序中是獨立的、併發的執行流,擁有獨立的內存單元,多個線程共享父進程裏的所有資源,線程共享的環境有進程的代碼段,進程的公有數據等,利用這些共享數據,線程很容易實現相互之間的通訊,能夠提升程序的運行效率。

多線程的好處主要有:

  • 進程之間不能共享內存,但線程之間共享內存很是容易。

  • 系統建立進程時須要給進程從新分配系統資源,但建立線程代價小得多,因此使用多線程實現多任務併發比多進程效率高

  • Java語言內置了多線程功能支持。

2、使用多線程

上面講了多線程的一些概念,都有些抽象,下面將學習如何使用多線程,建立多線程的方式有三種。

2.1 繼承Thread類建立

繼承Thread建立並啓動多線程有三個步驟:

  1. 定義類並繼承Thread,重寫run()方法,run()方法中爲須要多線程執行的任務。

  2. 建立該類的實例,即建立了線程對象。

  3. 調用實例的start()方法啓動線程。

public class FirstThread extends Thread {

    private int i=0;
    public void run() {
        for (; i < 100; i++) {
            //獲取當前線程名稱
            System.out.println(this.getName() + " " + i);
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            //Thread的靜態方法currentThread,獲取當前線程
            System.out.println(Thread.currentThread().getName());
            if (i == 20) {
                //建立線程並啓動
                new FirstThread().start();
                new FirstThread().start();
            }

        }
    }
}

運行結果能夠看到兩個線程的i並非連續的,說明他們並不共享數據。

2.2 實現Runnable接口

實現Runnable接口建立並啓動多線程也有如下步驟:

  1. 定義類並繼承Runnable接口,重寫run()方法,run()方法中爲須要多線程執行的任務。

  2. 建立該類的實例,並以此實例做爲target爲參數來建立Thread對象,這個Thread對象纔是真正的多線程對象。

public class SecondThread implements Runnable {
    private int i = 0;
    
    @Override
    public void run() {
        for (; i < 100; i++) {
            //此時想要獲取到多線程對象,只能使用Thread.currentThread()方法
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            //Thread的靜態方法currentThread,獲取當前線程
            System.out.println(Thread.currentThread().getName());
            if (i == 20) {
                //建立線程並啓動
                SecondThread secondThread=new SecondThread();
                new Thread(secondThread,"線程一").start();
                new Thread(secondThread,"線程二").start();
            }

        }
    }
}

2.3 使用Callable和Future

Callable是Runnable的增長版,主要是接口中的call()方法能夠有返回值,而且能夠申明拋出異常,使用Callable建立的步驟以下:

  1. 定義類並繼承Callable接口,重寫call()方法,run()方法中爲須要多線程執行的任務。

  2. 建立類實例,使用FutureTask來包裝對象實例,

  3. 使用FutureTask對象做爲Thread的target來建立多線程,並啓動線程。

  4. 調用FutureTask對象的get()方法來獲取子線程結束後的返回值。

public class ThirdThread {

    public static void main(String[] args) {
        //使用lambda表達式
        FutureTask<Integer> task = new FutureTask<>((Callable<Integer>) () -> {
            int i = 0;
            for (; i < 100; i++) {
                System.out.println(Thread.currentThread().getName() + "的循環變量i的值:" + i);
            }
            return i;
        });
        for (int i = 0; i < 100; i++) {
            //Thread的靜態方法currentThread,獲取當前線程
            System.out.println(Thread.currentThread().getName());
            if (i == 20) {
                //建立線程並啓動
                new Thread(task, "有返回值的線程").start();
            }
        }
        try {
            System.out.println("線程的返回值:" + task.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

這裏使用了lambda表達式,不使用表達式的方式也很簡單,能夠去源碼中查看。Callable與Runnable方式基本相同,只不過增長了返回值且可容許聲明拋出異常。

使用三種方式均可以建立線程,且方式也相對簡單,大致分爲實現接口和實現Thread類兩種,這兩種都各有優缺點。

繼承接口實現:

  • 優勢:除了繼承接口以外,還能夠繼承其餘類。這種方式多個線程共享一個target對象,能夠處理用於共同資源的狀況。
  • 缺點:編程稍微複雜一些,而且沒有直接獲取當前線程對象的方式,必須使用Thread.currentThread()方式。

基礎Thread類:

  • 優勢:編程簡單

  • 缺點:不能繼承其餘類

3、多線程的生命週期

線程狀態是線程中很是重要的一個概念,然而我看過不少資料,線程的狀態理解有不少種方式,不少人將其分爲五個基本狀態:新建、就緒、運行、阻塞、死亡,但在狀態枚舉中並非這五個狀態,我不知道是什麼緣由(有大神能夠解答更好),只能按照枚舉中的狀態根據本身的理解。

  1. 初始(NEW):新建立了一個線程對象,但尚未調用start()方法,並且就算調用了改方法也不表明狀態當即改變。

  2. 運行(RUNNABLE):在運行的狀態確定就處於RUNNABLE狀態。

  3. 阻塞(BLOCKED):表示線程阻塞,或者說線程已經被掛起了。

  4. 等待(WAITING):進入該狀態的線程須要等待其餘線程作出一些特定動做(通知或中斷)。

  5. 超時等待(TIMED_WAITING):該狀態不一樣於WAITING,它能夠在指定的時間後自行返回。

  6. 終止(TERMINATED):表示該線程已經執行完畢。

狀態流程圖以下:

理解:初始狀態很好理解,這個時候其實還不能被稱爲一個線程,由於他還沒被啓動,當調用start()方法後,線程正式啓動,可是也不表明當即就改變了狀態。

運行狀態中其實包含兩種狀態,運行中(RUNING)就緒(READY)

就緒狀態表示你有資格運行,只要CPU還未調度到你,就處於就緒狀態,有幾個狀態會是線程狀態編程就緒狀態

  • 調用線程的start()方法。

  • 當前線程sleep()方法結束,其餘線程join()結束,等待用戶輸入完畢,某個線程拿到對象鎖。

  • 當前線程時間片用完了,調用當前線程的yield()方法。

  • 鎖池裏的線程拿到對象鎖後。

運行中(RUNING)狀態比較好理解,線程調度程序選擇了當前線程做。

阻塞狀態是線程阻塞在進入synchronized關鍵字修飾的方法或代碼塊(獲取鎖)時的狀態。

等待狀態是指線程沒有被CPU分配執行時間,須要等待,這種等待是須要被顯示的喚醒,不然會無限等待下去。

超時等待狀態是這如今沒有被CPU分配執行時間,須要等待,不過這種等待不須要被顯示的喚醒,會設置必定的時間後zi懂喚醒。

死亡狀態也很好理解,說明線程方法被執行完成,或者出錯了,線程一旦進入這個狀態就表明完全的結束

相關文章
相關標籤/搜索