Java基礎-多線程篇

1. 多線程基礎

想要設計一個程序,邊打遊戲邊聽歌,怎麼設計?java

要解決上述問題,得使用多進程或者多線程來解決.編程

1.1 併發和並行

並行和併發

  • 併發:指兩個或多個事件在同一個時間段內發生 (交替執行)。
    • 在操做系統中,安裝了多個程序,併發指的是在一段時間內宏觀上有多個程序同時運行,這在單 CPU 系統中,每 一時刻只能有一道程序執行,即微觀上這些程序是分時的交替運行,只不過是給人的感受是同時運行,那是由於分時交替運行的時間是很是短的。
  • 並行:指兩個或多個事件在同一時刻發生(同時發生)。
    • 而在多個 CPU 系統中,則這些能夠併發執行的程序即可以分配到多個處理器上(CPU),實現多任務並行執行,即利用每一個處理器來處理一個能夠併發執行的程序,這樣多個程序即可以同時執行。目前電腦市場上說的多核 CPU,即是多核處理器,核 越多,並行處理的程序越多,能大大的提升電腦運行的效率。

注意:單核處理器的計算機確定是不能並行的處理多個任務的,只能是多個任務在單個CPU上併發運行。同安全

理,線程也是同樣的,從宏觀角度上理解線程是並行運行的,可是從微觀角度上分析倒是串行運行的,即一個服務器

線程一個線程的去運行,當系統只有一個CPU時,線程會以某種順序執行多個線程,咱們把這種狀況稱之爲多線程

線程調度。併發

1.2 線程與進程

進程:是指一個內存中運行的應用程序,每一個進程都有一個獨立的內存空間,一個應用程序能夠同時運行多ide

個進程;進程也是程序的一次執行過程,是系統運行程序的基本單位;系統運行一個程序便是一個進程從創函數

建、運行到消亡的過程。
進程工具

線程:線程是進程中的一個執行單元,負責當前進程中程序的執行,一個進程中至少有一個線程。一個進程學習

中是能夠有多個線程的,這個應用程序也能夠稱之爲多線程程序。

線程

簡而言之:一個程序運行後至少有一個進程,一個進程中能夠包含多個線程

1.3 線程調度

  • 分時調度

    全部線程輪流使用 CPU 的使用權,平均分配每一個線程佔用 CPU 的時間。

  • 搶佔式調度

    • 優先讓優先級高的線程使用 CPU,若是線程的優先級相同,那麼會隨機選擇一個(線程隨機性),Java使用的爲搶佔式調度。
      優先級

    • 多線程能夠提升cpu的利用率

      大部分操做系統都支持多進程併發運行,如今的操做系統幾乎都支持同時運行多個程序。在同時運行的程序,」感受這些軟件好像在同一時刻運行着「。

      實際上,CPU(中央處理器)使用搶佔式調度模式在多個線程間進行着高速的切換。對於CPU的一個核而言,某個時刻,只能執行一個線程,而 CPU的在多個線程間切換速度相對咱們的感受要快,看上去就是在同一時刻運行。 其實,多線程程序並不能提升程序的運行速度,但可以提升程序運行效率,讓CPU的使用率更高。

2. Java中使用線程

2.1 建立線程-方式1

​ Java使用 java.lang.Thread 類表明線程,全部的線程對象都必須是Thread類或其子類的實例。每一個線程的做用是 完成必定的任務,實際上就是執行一段程序流即一段順序執行的代碼。Java使用線程執行體來表明這段程序流。 Java中經過繼承Thread類來建立並啓動多線程的步驟以下:

  • 步驟:

    1. 定義Thread類的子類,並重寫該類的run()方法,該run()方法的方法體就表明了線程須要完成的任務,所以把 run()方法稱爲線程執行體。
    2. 建立Thread子類的實例,即建立了線程對象 。
    3. 調用線程對象的start()方法來啓動該線程 。
  • 代碼:

    /*測試類中的代碼*/
    public class DemoThread {
      public static void main(String[] args) {
        MyThread mt = new MyThread("線程1");
        mt.start(); // 啓動線程1的任務
        MyThread mt2 = new MyThread("線程2");
        mt2.start(); // 啓動線程1的任務
    
      }
    }
    /*定義的線程類代碼*/
    public class MyThread extends Thread {
      public MyThread(String name) {
        super(name);
      }
    
      @Override
      public void run() {
        for (int i = 0; i < 20; i++) {
          System.out.println(getName() + "線程執行" + i);
        }
      }
    }

2.2 多線程原理

​ 多個線程之間的程序不會影響彼此(好比一個線程崩潰了並不會影響另外一個線程)。

在Java中,main方法是程序執行的入口,也是Java程序的主線程。當在程序中開闢新的線程時,執行過程是這樣的。

  • 執行過程

    • 首先main方法做爲主程序先壓棧執行。
    • 在主程序的執行過程當中,若建立了新的線程,則內存中會另開闢一個新的棧來執行新的線程。
    • 每個新的線程都會有一個新的棧來存放新的線程任務。
    • 棧與棧之間的任務不會互相影響。
    • CPU會隨機切換執行不一樣棧中的任務。
  • 圖解執行過程:(上述代碼爲例)
    1573523815071

2.3 Thread類經常使用方法

  • 構造方法

    • public Thread() :分配一個新的線程對象。
    • public Thread(String name) :分配一個指定名字的新的線程對象。
    • public Thread(Runnable target) :分配一個帶有指定目標新的線程對象。
    • public Thread(Runnable target,String name) :分配一個帶有指定目標新的線程對象並指定名字。
  • 經常使用方法

    • public String getName() :獲取當前線程名稱。
    • public void start() :致使此線程開始執行; Java虛擬機調用此線程的run方法。
    • public void run() :此線程要執行的任務在此處定義代碼。
    • public static void sleep(long millis) :使當前正在執行的線程以指定的毫秒數暫停(暫時中止執行)。
    • public static Thread currentThread() :返回對當前正在執行的線程對象的引用。
  • 代碼:

    //【代碼測試類】
    public class Main01 {
      public static void main(String[] args) {
        MyThread mt = new MyThread("線程1");
        mt.start();
        // 打印線程名稱
        System.out.println(mt.getName());
        System.out.println("當前線程是" + Thread.currentThread().getName());
        // 每間隔一秒鐘打印一個數字
        for (int i = 0; i < 60; i++) {
          System.out.println(i);
          try {
            // sleep拋出了異常,須要處理異常
            Thread.sleep(1000);
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
        }
      }
    }
    //【MyThread類】
    public class MyThread extends Thread{
      public  MyThread(){
        super();
      }
      // 構造函數中調用父類構造函數傳入線程名稱
      public MyThread(String name) {
        super(name);
      }
      @Override
      public void run() {
        // 打印線程名稱
        System.out.println(this.getName());
        System.out.println("當前線程是" + Thread.currentThread().getName());
      }
    }

2.4 建立線程-方式2

​ 翻閱API後得知建立線程的方式總共有兩種,一種是繼承Thread類方式,一種是實現Runnable接口方式 。

  • Runnable使用步驟

    1. 定義Runnable接口的實現類,並重寫該接口的run()方法,該run()方法的方法體一樣是該線程的線程執行體。
    2. 建立Runnable實現類的實例,並以此實例做爲Thread的target來建立Thread對象,該Thread對象纔是真正的線程對象。
    3. 調用線程對象的start()方法來啓動線程。
  • 代碼

    // 測試類
    public class Main01 {
      public static void main(String[] args) {
        // 建立Runnable對象
        RunnableImpl ra = new RunnableImpl();
        // 建立線程對象並傳入Runnable對象
        Thread th = new Thread(ra);
        // 啓動並執行線程任務
        th.start();
      }
    }
    // 【Runnable實現類】
    public class RunnableImpl implements Runnable {
      @Override
      public void run() {
        System.out.println("線程任務1");
      }
    }
  • 總結

    • 經過實現Runnable接口,使得該類有了多線程類的特徵。run()方法是多線程程序的一個執行目標。全部的多線程代碼都在run方法裏面。Thread類實際上也是實現了Runnable接口的類。

    • 在啓動的多線程的時候,須要先經過Thread類的構造方法Thread(Runnable target) 構造出對象,而後調用Thread對象的start()方法來運行多線程代碼。

    • 實際上全部的多線程代碼都是經過運行Thread的start()方法來運行的。所以,無論是繼承Thread類仍是實現Runnable接口來實現多線程,最終仍是經過Thread的對象的API來控制線程的,熟悉Thread類的API是進行多線程編程的基礎。

    • Runnable對象僅僅做爲Thread對象的target,Runnable實現類裏包含的run()方法僅做爲線程執行體。

    而實際的線程對象依然是Thread實例,只是該Thread線程負責執行其target的run()方法。

2.5 Runnable和Thread的關係

​ 建立線程方式2好像比建立線程方式1操做要麻煩一些,爲什麼要畫蛇添足呢?

​ 由於若是一個類繼承Thread,則不適合資源共享。可是若是實現了Runable接口的話,則很容易的實現資源共享。

  • 總結:實現Runnable接口比繼承Thread類所具備的優點:

    • 適合多個相同的程序代碼的線程去共享同一個資源。
    • 能夠避免java中的單繼承的侷限性。
    • 增長程序的健壯性,實現解耦操做,代碼能夠被多個線程共享,代碼和線程獨立。
    • 線程池只能放入實現Runable或Callable類線程,不能直接放入繼承Thread的類。
  • 擴展:

    在java中,每次程序運行至少啓動2個線程。一個是main線程,一個是垃圾收集線程。由於每當使用

    java命令執行一個類的時候,實際上都會啓動一個JVM,每個JVM其實在就是在操做系統中啓動了一個進程。

2.6 匿名內部類實現線程的建立

使用線程的內匿名內部類方式,能夠方便的實現每一個線程執行不一樣的線程任務操做。

簡而言之,使用匿名內部類能夠簡化代碼。

  • 格式new 類名/接口名(){ //重寫父類/接口中的方法 }

  • 代碼

    // 匿名內部類建立線程方式1
        new Thread(){
          @Override
          public void run() {
            System.out.println(Thread.currentThread().getName());
          }
        }.start();
        // 匿名內部類建立線程方式2
        new Thread(new Runnable() {
          @Override
          public void run() {
            System.out.println(Thread.currentThread().getName());
          }
        }).start();

3. 線程安全問題

3.1 線程安全概述

​ 多個線程執行同一個任務並操做同一個數據時,就會形成數據的安全問題。咱們經過如下案例來看線程安全問題。

  • 案例需求:

    ​ 電影院要賣票,咱們模擬電影院的賣票過程。假設要播放的電影是 「皮卡丘大戰葫蘆娃」,本次電影的座位共100個 (本場電影只能賣100張票)。

    ​ 咱們來模擬電影院的售票窗口,實現多個窗口同時賣 「葫蘆娃大戰奧特曼」這場電影票(多個窗口一塊兒賣這100張票) 須要窗口,採用線程對象來模擬;須要票,Runnable接口子類來模擬 。

  • 案例代碼實現:

    //【操做票的任務代碼類】
    public class RunnableImpl implements Runnable {
      // 線程任務要操做的數據
      private int ticket = 100;
      @Override
      public void run() {
        while (true){
            if(ticket>0){
                System.out.println(Thread.currentThread().getName() + "正在賣第" + ticket+"張票");
                  ticket--;
            }
        }
      }
    }
    //【測試類】
    public class Main01 {
      public static void main(String[] args) {
        // 建立線程任務
        RunnableImpl ra = new RunnableImpl();
        // 建立第一個線程執行線程任務
        new Thread(ra).start();
        // 建立第二線程執行線程任務
        new Thread(ra).start();
        // 建立第三個線程執行線程任務
        new Thread(ra).start();
    
      }
    }
  • 執行結果及問題
    執行結果及問題

  • 緣由

    • 搶奪cpu執行權和線程執行時間是不肯定的,好比線程0搶到了cpu執行權並執行到了打印代碼處,此時cpu又被線程1搶奪,其餘線程處於等待線程1頁執行到了打印代碼處,沒等ticket--,兩個線程都打印了售票信息。
  • 總結:這種問題,幾個窗口(線程)票數不一樣步了,這種問題稱爲線程不安全。

    線程安全問題都是由全局變量及靜態變量引發的。若每一個線程中對全局變量、靜態變量只有讀操做,而無寫 操做,通常來講,這個全局變量是線程安全的;如有多個線程同時執行寫操做,通常都須要考慮線程同步, 不然的話就可能影響線程安全。

3.2 線程安全解決方案

​ 上述咱們知道,線程安全問題是由於線程在操做數據時不一樣步形成的,因此只要可以實現操做數據同步,就能夠解決線程安全問題。

​ 同步指的就是,當一個線程執行指定同步的代碼任務時,其餘線程必須等該線程操做完畢後再執行。

​ 根據案例描述:窗口1線程進入操做的時候,窗口2和窗口3線程只能在外等着,窗口1操做結束,窗口1和窗口2和窗口3纔有機會進入代碼 去執行。也就是說在某個線程修改共享資源的時候,其餘線程不能修改該資源,等待修改完畢同步以後,才能去搶奪CPU 資源,完成對應的操做,保證了數據的同步性,解決了線程不安全的現象。

​ 爲了保證每一個線程都能正常執行原子操做,Java引入了線程同步機制(synchronize)。

​ 那麼怎麼去使用呢?有三種方式完成同步操做:

  1. 同步代碼塊
  2. 同步方法
  3. 同步鎖

3.3 同步代碼塊

  • 同步代碼塊:synchronized關鍵字能夠用於方法中的某個區塊中,表示只對這個區塊的資源實行互斥訪問。

  • 格式:synchronized(同步鎖){ 須要同步操做的代碼 }

    • 同步鎖:對象的同步鎖只是一個概念,能夠想象爲在對象上標記了一個鎖。
    • 鎖對象能夠是任意類型。
    • 多個線程對象 要使用同一把鎖。
    • 注意:在任什麼時候候,最多容許一個線程擁有同步鎖,誰拿到鎖就進入代碼塊,其餘的線程只能在外等着 。
  • 代碼:

    //【測試類】
    public class Main01 {
      public static void main(String[] args) {
        // 建立線程任務
        RunnableImpl ra = new RunnableImpl();
        // 建立第一個線程執行線程任務
        new Thread(ra).start();
        // 建立第二線程執行線程任務
        new Thread(ra).start();
        // 建立第三個線程執行線程任務
        new Thread(ra).start();
    
      }
    }
    //【線程任務類】
    public class RunnableImpl implements Runnable {
      // 線程任務要操做的數據
      private int ticket = 100;
      // 定義線程鎖對象(任意對象)
      Object obj = new Object();
      @Override
      public void run() {
        while (true){
          synchronized (obj){
            if(ticket>0){
              try {
                Thread.sleep(10);
                System.out.println(Thread.currentThread().getName()+"在售賣第" + ticket + "張票");
                ticket--;
              } catch (InterruptedException e) {
                e.printStackTrace();
              }
    
            }
          }
    
        }
      }
    }

3.4 同步方法

  • 同步方法:使用synchronized修飾的方法,就叫作同步方法,保證A線程執行該方法的時候,其餘線程只能在方法外等着

  • 格式:public synchronized void method(){ // 可能會產生線程安全問題的代碼 }

    • 同步鎖是誰?
      • 對於非static方法,同步鎖就是this
      • 對於static方法,咱們使用當前方法所在類的字節碼對象(類名.class)。
  • 代碼:

    //【測試類】
    public class Main01 {
      public static void main(String[] args) {
        // 建立線程任務
        RunnableImpl ra = new RunnableImpl();
        // 建立第一個線程執行線程任務
        new Thread(ra).start();
        // 建立第二線程執行線程任務
        new Thread(ra).start();
        // 建立第三個線程執行線程任務
        new Thread(ra).start();
      }
    }
    //【線程任務類】
    public class RunnableImpl implements Runnable {
      // 線程任務要操做的數據
      private int ticket = 100;
      @Override
      public void run() {
        while (true) {
          func();
        }
    
      }
      public synchronized void func() {
        if (ticket > 0) {
          try {
            Thread.sleep(10);
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
          System.out.println(Thread.currentThread().getName() + "在售賣第" + ticket + "張票");
          ticket--;
        }
      }
    }

3.5 同步鎖

  • Lock:java.util.concurrent.locks.Lock 機制提供了比synchronized代碼塊和synchronized方法更普遍的鎖定操做, 同步代碼塊/同步方法具備的功能Lock都有,除此以外更強大,更體現面向對象。

  • 方法:Lock鎖也稱同步鎖,加鎖與釋放鎖方法化了

    • public void lock() :加同步鎖。
    • public void unlock() :釋放同步鎖。
  • 代碼:

    //【測試類】
    public class Main01 {
      public static void main(String[] args) {
        // 建立線程任務
        RunnableImpl ra = new RunnableImpl();
        // 建立第一個線程執行線程任務
        new Thread(ra).start();
        // 建立第二線程執行線程任務
        new Thread(ra).start();
        // 建立第三個線程執行線程任務
        new Thread(ra).start();
      }
    }
    //【線程任務類】
    public class RunnableImpl implements Runnable {
      // 線程任務要操做的數據
      private int ticket = 100;
      // 建立鎖對象
      Lock lock = new ReentrantLock();
      @Override
      public void run() {
        while (true) {
          // 開啓同步鎖
          lock.lock();
          if (ticket > 0) {
            try {
              Thread.sleep(10);
              System.out.println(Thread.currentThread().getName() + "在售賣第" + ticket + "張票");
              ticket--;
            } catch (InterruptedException e) {
              e.printStackTrace();
            }finally {
              // 釋放同步鎖
              lock.unlock();
            }
    
          }
        }
    
      }
    }

4. 線程狀態

線程狀態

4.1 線程狀態介紹

​ 當線程被建立並啓動之後,它既不是一啓動就進入了執行狀態,也不是一直處於執行狀態。在線程的生命週期中, 有幾種狀態呢?在API中 java.lang.Thread.State 這個枚舉中給出了六種線程狀態:

線程狀態

​ 咱們不須要去研究這幾種狀態的實現原理,咱們只需知道在作線程操做中存在這樣的狀態。那咱們怎麼去理解這幾 個狀態呢,新建與被終止仍是很容易理解的,咱們就研究一下線程從Runnable(可運行)狀態與非運行狀態之間 的轉換問題。

4.2 TimedWaiting計時等待

​ Timed Waiting在API中的描述爲:一個正在限時等待另外一個線程執行一個(喚醒)動做的線程處於這一狀態。

​ 單獨 的去理解這句話,真是玄之又玄,其實咱們在以前的操做中已經接觸過這個狀態了,在哪裏呢? 在咱們寫賣票的案例中,爲了減小線程執行太快,現象不明顯等問題,咱們在run方法中添加了sleep語句,這樣就 強制當前正在執行的線程休眠(暫停執行),以「減慢線程」。

​ 其實當咱們調用了sleep方法以後,當前執行的線程就進入到「休眠狀態」,其實就是所謂的Timed Waiting(計時等 待),那麼咱們經過一個案例加深對該狀態的一個理解。

  • 案例:實現一個計數器,計數到100,在每一個數字之間暫停1秒,每隔10個數字輸出一個字符串 。

  • 代碼:

    public class MyThread extends Thread {
        public void run() {
            for (int i = 0; i < 100; i++) {
                if ((i) % 10 == 0) {
                    System.out.println("‐‐‐‐‐‐‐" + i);
                }
                System.out.print(i);
                try {
                    Thread.sleep(1000);
                    System.out.print(" 線程睡眠1秒!\n");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        public static void main(String[] args) {
            new MyThread().start();
        }
    }
  • 總結:經過案例能夠發現,sleep方法的使用仍是很簡單的。咱們須要記住下面幾點:

    1. 進入 TIMED_WAITING 狀態的一種常見情形是調用的 sleep 方法,單獨的線程也能夠調用,不必定非要有協 做關係。
    2. 爲了讓其餘線程有機會執行,能夠將Thread.sleep()的調用放線程run()以內。這樣才能保證該線程執行過程 中會睡眠 。
    3. sleep與鎖無關,線程睡眠到期自動甦醒,並返回到Runnable(可運行)狀態 。
  • 注意:sleep()中指定的時間是線程不會運行的最短期。所以,sleep()方法不能保證該線程睡眠到期後就 開始馬上執行。

  • 圖解:
    執行流程

4.3 Blocked鎖阻塞

​ Blocked狀態在API中的介紹爲:一個正在阻塞等待一個監視器鎖(鎖對象)的線程處於這一狀態 。

​ 咱們已經學完同步機制,那麼這個狀態是很是好理解的了。好比,線程A與線程B代碼中使用同一鎖,若是線程A獲 取到鎖,線程A進入到Runnable狀態,那麼線程B就進入到Blocked鎖阻塞狀態。

​ 這是由Runnable狀態進入Blocked狀態。除此Waiting以及Time Waiting狀態也會在某種狀況下進入阻塞狀態。

執行流程

4.4 Waiting 無限等待

​ Wating狀態在API中介紹爲:一個正在無限期等待另外一個線程執行一個特別的(喚醒)動做的線程處於這一狀態。

​ 咱們經過一段代碼來 學習一下:

  • 代碼

    // 鎖對象
      public static Object obj = new Object();
    
      public static void main(String[] args) {
        // 消費者線程
        new Thread(new Runnable() {
          @Override
          public void run() {
            while (true) {
              synchronized (obj) {
                System.out.println(Thread.currentThread().getName() + "-顧客1:老闆包子好了嗎?");
                try {
                  // 等待
                  obj.wait();
                } catch (InterruptedException e) {
                  e.printStackTrace();
                }
                // 喚醒以後要執行的代碼
                System.out.println(Thread.currentThread().getName() + "顧客1:能夠吃包子了。");
                System.out.println("--------------------------------------");
              }
            }
          }
        }).start();
        // 生產者線程
        new Thread(new Runnable() {
          @Override
          public void run() {
            while (true) {
              try {
                Thread.sleep(3000);
              } catch (InterruptedException e) {
                e.printStackTrace();
              }
              synchronized (obj) {
                System.out.println("等待3秒後...");
                System.out.println(Thread.currentThread().getName() + "老闆說:包子好了!");
                // 喚醒
                obj.notify();
              }
            }
          }
        }).start();
      }
  • 分析:

    ​ 經過上述案例咱們會發現,一個調用了某個對象的 Object.wait 方法的線程會等待另外一個線程調用此對象的 Object.notify()方法 或 Object.notifyAll()方法 。

    ​ 其實waiting狀態並非一個線程的操做,它體現的是多個線程間的通訊,能夠理解爲多個線程之間的協做關係, 多個線程會爭取鎖,同時相互之間又存在協做關係。就比如在公司裏你和你的同事們,大家可能存在晉升時的競 爭,但更多時候大家更可能是一塊兒合做以完成某些任務。

    ​ 當多個線程協做時,好比A,B線程,若是A線程在Runnable(可運行)狀態中調用了wait()方法那麼A線程就進入 了Waiting(無限等待)狀態,同時失去了同步鎖。假如這個時候B線程獲取到了同步鎖,在運行狀態中調用了 notify()方法,那麼就會將無限等待的A線程喚醒。注意是喚醒,若是獲取到鎖對象,那麼A線程喚醒後就進入 Runnable(可運行)狀態;若是沒有獲取鎖對象,那麼就進入到Blocked(鎖阻塞狀態)。

  • 圖解:
    執行流程

5. 等待喚醒機制

5.1 線程間的通訊

  • 概念:多個線程在處理同一個資源,可是處理的動做(線程的任務)卻不相同。

    ​ 好比:線程A用來生成包子的,線程B用來吃包子的,包子能夠理解爲同一資源,線程A與線程B處理的動做,一個是生產,一個是消費,那麼線程A與線程B

    線程通訊

  • 爲何要處理線程間的通訊

    ​ 多個線程併發執行時, 在默認狀況下CPU是隨機切換線程的,當咱們須要多個線程來共同完成一件任務,而且咱們 但願他們有規律的執行, 那麼多線程之間須要一些協調通訊,以此來幫咱們達到多線程共同操做一份數據。

  • 如何保證線程間通訊有效利用資源

    ​ 多個線程在處理同一個資源,而且任務不一樣時,須要線程通訊來幫助解決線程之間對同一個變量的使用或操做。 就是多個線程在操做同一份數據時, 避免對同一共享變量的爭奪。也就是咱們須要經過必定的手段使各個線程能有效的利用資源。而這種手段即—— 等待喚醒機制。

5.2 什麼是等待喚醒機制

​ 這是多個線程間的一種協做機制。談到線程咱們常常想到的是線程間的競爭(race),好比去爭奪鎖,但這並非故事的所有,線程間也會有協做機制。就比如在公司裏你和你的同事們,大家可能存在在晉升時的競爭,但更多時候大家更可能是一塊兒合做以完成某些任務。

​ 就是在一個線程進行了規定操做後,就進入等待狀態(wait()), 等待其餘線程執行完他們的指定代碼事後 再將其喚醒(notify());在有多個線程進行等待時, 若是須要,可使用 notifyAll()來喚醒全部的等待線程。

wait/notify 就是線程間的一種協做機制。

5.3 等待喚醒中的方法

等待喚醒機制就是用於解決線程間通訊的問題的,使用到的3個方法的含義以下:

  1. wait

    ​ 線程再也不活動,再也不參與調度,進入 wait set 中,所以不會浪費 CPU 資源,也不會去競爭鎖了,這時的線程狀態便是 WAITING。它還要等着別的線程執行一個特別的動做,也便是「通知(notify)」在這個對象上等待的線程從wait set 中釋放出來,從新進入到調度隊列(ready queue)中。

  2. notify

    則選取所通知對象的 wait set 中的一個線程釋放;例如,餐館有空位置後,等候就餐最久的顧客最早

    入座。

  3. notifyAll

    則釋放所通知對象的 wait set 上的所有線程。

  • 注意事項

    1. 注意1:

      ​ 哪怕只通知了一個等待的線程,被通知線程也不能當即恢復執行,由於它當初中斷的地方是在同步塊內,而此刻它已經不持有鎖,因此她須要再次嘗試去獲取鎖(極可能面臨其它線程的競爭),成功後才能在當初調用 wait 方法以後的地方恢復執行。

      總結以下:

      若是能獲取鎖,線程就從 WAITING 狀態變成 RUNNABLE 狀態;

      不然,從 wait set 出來,又進入 entry set,線程就從 WAITING 狀態又變成 BLOCKED 狀態

    2. 注意2:

      1. wait方法與notify方法必需要由同一個鎖對象調用。由於:對應的鎖對象能夠經過notify喚醒使用同一個鎖對象調用的wait方法後的線程。

      2. wait方法與notify方法是屬於Object類的方法的。由於:鎖對象能夠是任意對象,而任意對象的所屬類都是繼承了Object類的。

      3. wait方法與notify方法必需要在同步代碼塊或者是同步函數中使用。由於:必需要經過鎖對象調用這2個方 法。

5.4 代碼演示

等待喚醒機制其實就是經典的「生產者與消費者」的問題。

就拿生產包子消費包子來講等待喚醒機制如何有效利用資源。

包子鋪線程生產包子,吃貨線程消費包子。當包子沒有時(包子狀態爲false),吃貨線程等待,包子鋪線程生產包子(即包子狀態爲true),並通知吃貨線程(解除吃貨的等待狀態),由於已經有包子了,那麼包子鋪線程進入等待狀態。接下來,吃貨線程可否進一步執行則取決於鎖的獲取狀況。若是吃貨獲取到鎖,那麼就執行吃包子動做,包子吃完(包子狀態爲false),並通知包子鋪線程(解除包子鋪的等待狀態),吃貨線程進入等待。包子鋪線程可否進一步執行則取決於鎖的獲取狀況。

  • 包子類代碼

    public class BaoZi {
       // 包子皮
       String pi;
       // 包子餡
       String xian;
       // 包子的狀態 true有包子,false沒有包子,默認是false
       boolean flag = false;
    }
  • 生產者代碼

    public class BaoZiPu extends Thread {
      // 包子
      BaoZi bz;
      public BaoZiPu(BaoZi bz){
        this.bz = bz;
      }
      // 線程任務
      @Override
      public void run() {
        int i = 0; // 用來交替生產不一樣的包子
        while (true){
          synchronized (bz){
            // 判斷是否有包子
            if(bz.flag){
              // 有包子,則進入wait狀態
              try {
                bz.wait();
              } catch (InterruptedException e) {
                e.printStackTrace();
              }
            }
            // 沒有有包子,則等待3秒鐘
            try {
              Thread.sleep(3000);
            } catch (InterruptedException e) {
              e.printStackTrace();
            }
            // 生產包子
            if(i%2==0){
              bz.pi="蔬菜麪皮";
              bz.xian="三鮮";
            }else if(i%2==1) {
              bz.pi = "米麪皮";
              bz.xian = "牛肉";
            }
            bz.flag = true;
            i++;
            System.out.println("包子鋪線程" + Thread.currentThread().getName() + ":生產好了" + bz.pi+bz.xian + "包子" );
            // 喚醒相同鎖的其餘線程
            bz.notify();
          }
        }
      }
    }
  • 消費者代碼

    public class ChiHuo extends  Thread{
      // 包子
      BaoZi bz;
      public ChiHuo(BaoZi bz) {
        this.bz = bz;
      }
      // 線程任務
      @Override
      public void run() {
        while (true){
          synchronized (bz){
            if(!bz.flag){
              // 沒有包子,則等待
              try {
                bz.wait();
              } catch (InterruptedException e) {
                e.printStackTrace();
              }
    
            }
            // 有包子,則吃包子
            System.out.println("吃貨線程" + Thread.currentThread().getName() + ":正在吃" + bz.pi + bz.xian + "包子");
            try {
              Thread.sleep(3000);
            } catch (InterruptedException e) {
              e.printStackTrace();
            }
            // 三秒鐘後,吃完包子,並喚醒其餘同步鎖線程
            System.out.println("吃貨線程" + Thread.currentThread().getName() +"吃完了包子");
            bz.flag = false; // 沒有包子了
            System.out.println("-----------------------------------------------------");
            bz.notify();
          }
        }
      }
    }
  • 測試代碼

    public class Test {
      public static void main(String[] args) {
        // 建立包子對象
        BaoZi bz = new BaoZi();
        // 建立生產者
        new BaoZiPu(bz).start();
        // 建立消費者
        new ChiHuo(bz).start();
      }
    }

6. 線程池

6.1 線程池思想介紹

​ 咱們使用線程的時候就去建立一個線程,這樣實現起來很是簡便,可是就會有一個問題:

​ 若是併發的線程數量不少,而且每一個線程都是執行一個時間很短的任務就結束了,這樣頻繁建立線程就會大大下降系統的效率,由於頻繁建立線程和銷燬線程須要時間

​ 那麼有沒有一種辦法使得線程能夠複用,就是執行完一個任務,並不被銷燬,而是能夠繼續執行其餘的任務?

​ 在Java中能夠經過線程池來達到這樣的效果。

6.2 什麼是線程池

​ 其實就是一個容納多個線程的容器,其中的線程能夠反覆使用,省去了頻繁建立線程對象的操做,無需反覆建立線程而消耗過多資源。

線程池

  • 合理使用線程池的好處:
    1. 下降資源消耗。減小了建立和銷燬線程的次數,每一個工做線程均可以被重複利用,可執行多個任務。
    2. 提升響應速度。當任務到達時,任務能夠不須要的等到線程建立就能當即執行。
    3. 提升線程的可管理性。能夠根據系統的承受能力,調整線程池中工做線線程的數目,防止由於消耗過多的內存,而把服務器累趴下(每一個線程須要大約1MB內存,線程開的越多,消耗的內存也就越大,最後死機)。

6.3 使用線程

​ Java裏面線程池的頂級接口是 java.util.concurrent.Executor ,可是嚴格意義上講 Executor 並非一個線程 池,而只是一個執行線程的工具。真正的線程池接口是 java.util.concurrent.ExecutorService

​ 要配置一個線程池是比較複雜的,尤爲是對於線程池的原理不是很清楚的狀況下,頗有可能配置的線程池不是較優的,所以在 java.util.concurrent.Executors 線程工廠類裏面提供了一些靜態工廠,生成一些經常使用的線程池。官方建議使用Executors工程類來建立線程池對象。

  • Executors類中有個建立線程池的方法以下

    • public static ExecutorService newFixedThreadPool(int nThreads) :返回線程池對象。(建立的是有界線程池,也就是池中的線程個數能夠指定最大數量)
  • 獲取到了一個線程池ExecutorService 對象,那麼怎麼使用呢,在這裏定義了一個使用線程池對象的方法以下:

    • public Future<?> submit(Runnable task) :獲取線程池中的某一個線程對象,並執行
    • Future接口:用來記錄線程任務執行完畢後產生的結果。線程池建立與使用。
  • 使用線程池中線程對象的步驟:

    1. 建立線程池對象。
    2. 建立Runnable接口子類對象。(task)
    3. 提交Runnable接口子類對象。(take task)
    4. 關閉線程池(通常不作)。
  • 代碼

    public class Test {
      public static void main(String[] args) {
        // 線程任務對象1
        Runnable task = new Runnable(){
          @Override
          public void run() {
            System.out.println(Thread.currentThread().getName() + ":執行了任務1");
          }
        };
        // 線程任務對象2
        Runnable task2 = new Runnable(){
          @Override
          public void run() {
            System.out.println(Thread.currentThread().getName() + ":執行了任務2");
          }
        };
        // 建立線程池
        ExecutorService pool =  Executors.newFixedThreadPool(2);
        // 提交任務
        pool.submit(task);
        pool.submit(task2);
        pool.submit(task);
        pool.submit(task2);
        pool.submit(task);
        pool.submit(task2);
        // 關閉線程池,不建議關閉
        // pool.shutdown();
      }
    }
相關文章
相關標籤/搜索