JavaSE_多執行緒入門 執行緒安全 死鎖 狀態 通訊 執行緒池

Epicenter發表於2022-05-30

1 多執行緒入門

1.1 多執行緒相關的概念

  • 併發與並行
    • 並行:在同一時刻,有多個任務在多個CPU上同時執行。
    • 併發:在同一時刻,有多個任務在單個CPU上交替執行。
  • 程式與執行緒
    • 程式:就是作業系統中正在執行的一個應用程式。
    • 執行緒:就是應用程式中做的事情。比如:360軟體中的防毒,掃描木馬,清理垃圾。
  • 多執行緒的概念
    • 是指從軟體或者硬體上實現多個執行緒併發執行的技術。
      具有多執行緒能力的計算機因有硬體支援而能夠在同一時間執行多個執行緒,提升效能。
    • 好處 : 提高任務的執行效能

1.2 多執行緒的建立方式

多執行緒的實現方式主要有三種
1.2.1 繼承Thread類
  • 基本步驟

      1. 建立一個類繼承Thread類。
      1. 在類中重寫run方法(執行緒執行的任務放在這裡)
      1. 建立執行緒物件,呼叫執行緒的start方法開啟執行緒。
  • 優點 :

    • 實現起來比較簡單,可以直接使用Thread類中的功能
  • 缺點 :

    • 擴充性較差,只能單繼承Thread類,任務執行完畢沒有返回值,出現異常只能捕獲
1.2.2 實現Runnable介面
  • 基本步驟

      1. 自定義類 實現Runnable介面
      1. 重寫 run()方法,在 run() 方法中定義執行緒執行的任務
      1. 建立任務類物件
      1. 建立執行緒物件(Thread類物件),把任務類物件作為引數傳遞給執行緒類物件
      1. 呼叫 start() 方法,開啟了一條執行緒
  • 優點 :

    • 程式碼實現比較簡單,擴充性較強,還能繼承其他類
  • 缺點 :

    • 不能直接使用Thread類功能,出現異常只能捕獲
1.2.3 實現Callable介面
  • 基本步驟

      1. 自定義類 實現Callable介面
      1. 重寫call()方法,在call()方法中定義執行緒執行的任務
      1. 由於Thread構造中接收不了Callable型別物件,因此需要一箇中間橋樑物件 FutureTask,在它的構造中傳入Callable介面的實現類物件,FutureTask的物件是Runable的子類,可以作為Thread構造的引數,這樣讓Callable實現類物件能夠關聯執行緒類物件
      1. 建立執行緒物件(Thread類物件),構造中傳入FurureTask物件
      1. 呼叫 start() 方法,開啟了一條執行緒
  • 注意事項

    • Callable執行緒執行完畢會有一個返回值,獲取的方式是通過FutureTask物件呼叫get()方法得到,但需注意成get()方法在拿到返回值前會形成阻塞
  • 優點 :

    • 擴充性較強,還能繼承其他類,任務執行完畢有返回值,出現異常可以捕獲也可以丟擲,相對靈活
  • 缺點 :

    • 不能直接使用Thread類功能,實現起來較為複雜

2 執行緒安全

2.1 執行緒安全產生的原因

  • 多個執行緒在對共享資料進行讀改寫的時候,可能導致的資料錯亂就是執行緒的安全問題了

2.2 執行緒安全問題解決方式

執行緒安全問題解決方式主要有三種
2.2.1 同步程式碼塊
同步程式碼塊 : 鎖住多條語句操作共享資料,可以使用同步程式碼塊實現

第一部分 : 格式
           synchronized(任意物件) {
               多條語句操作共享資料的程式碼
           }

第二部分 : 注意
           1 預設情況鎖是開啟的,只要有一個執行緒進去執行程式碼了,鎖就會關閉
           2 當執行緒執行完出來了,鎖才會自動開啟

第三部分 : 同步的好處和弊端
            好處 : 解決了多執行緒的資料安全問題
            弊端 : 當執行緒很多時,因為每個執行緒都會去判斷同步上的鎖,這是很耗費資源的,無形中會降低程式的執行效率 
2.2.2 同步方法
同步方法:就是把synchronized關鍵字加到方法上

格式:修飾符 synchronized 返回值型別 方法名(方法引數) {    }

同步程式碼塊和同步方法的區別:
    1 同步程式碼塊可以鎖住指定程式碼,同步方法是鎖住方法中所有程式碼
    2 同步程式碼塊可以指定鎖物件,同步方法不能指定鎖物件

注意 : 同步方法時不能指定鎖物件的 , 但是有預設存在的鎖物件的。
    1 對於非static方法,同步鎖就是this。
    2 對於static方法,我們使用當前方法所在類的位元組碼物件(類名.class)。   Class型別的物件
2.2.3 Lock鎖
雖然我們可以理解同步程式碼塊和同步方法的鎖物件問題,但是我們並沒有直接看到在哪裡加上了鎖,在哪裡釋放了鎖,
為了更清晰的表達如何加鎖和釋放鎖,JDK5以後提供了一個新的鎖物件Lock

Lock中提供了獲得鎖和釋放鎖的方法
    void lock():獲得鎖
    void unlock():釋放鎖

Lock是介面不能直接例項化,這裡採用它的實現類ReentrantLock來例項化
    ReentrantLock的構造方法
    ReentrantLock():建立一個ReentrantLock的例項

注意:多個執行緒使用相同的Lock鎖物件,需要多執行緒運算元據的程式碼放在lock()和unLock()方法之間。一定要確保unlock最後能夠呼叫

3 執行緒死鎖

3.1 概述 :

  • 死鎖是一種少見的,而且難於除錯的錯誤,在兩個執行緒對兩個同步鎖物件具有迴圈依賴時,就會大概率的出現死鎖。我們要避免死鎖的產生。否則一旦死鎖,除了重啟沒有其他辦法的

3.2 產生條件 :

  • 多個執行緒
  • 存在鎖物件的迴圈依賴

3.3 程式碼實踐

public class DeadLockDemo {
    public static void main(String[] args) {
        String 筷子A = "筷子A";
        String 筷子B = "筷子B";

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    synchronized (筷子A) {
                        System.out.println("小白拿到了筷子A ,等待筷子B....");
                        synchronized (筷子B) {
                            System.out.println("小白拿到了筷子A和筷子B , 開吃!!!!!");
                        }
                    }
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "小白").start();


        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    synchronized (筷子B) {
                        System.out.println("小黑拿到了筷子B ,等待筷子A....");
                        synchronized (筷子A) {
                            System.out.println("小黑拿到了筷子B和筷子A , 開吃!!!!!");
                        }
                    }
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "小黑").start();
    }
}

4 執行緒的狀態

在 java.lang.Thread.State 這個列舉中給出了六種執行緒狀態
  1. 新建狀態(NEW) 建立執行緒狀態
  2. 就緒狀態(RUNNABLE)start方法
  3. 阻塞狀態(BLOCKED)無法獲得鎖物件
  4. 等待狀態(WAITING) wait方法
  5. 計時等待(TIMED_WAITING)sleep方法
  6. 結束狀態(TERMINATED)全部程式碼執行完畢

5 執行緒通訊

  • 執行緒間的通訊技術就是通過等待和喚醒機制,來實現多個執行緒協同操作完成某一項任務,例如經典的生產者和消費者案例。等待喚醒機制其實就是讓執行緒進入等待狀態或者讓執行緒從等待狀態中喚醒,需要用到兩種方法,如下:

  • 等待方法 :

    • void wait() 讓執行緒進入無限等待。
    • void wait(long timeout) 讓執行緒進入計時等待
    • 以上兩個方法呼叫會導致當前執行緒釋放掉鎖資源。
  • 喚醒方法 :

    • void notify() 喚醒在此物件監視器(鎖物件)上等待的單個執行緒。
    • void notifyAll() 喚醒在此物件監視器上等待的所有執行緒。
    • 以上兩個方法呼叫不會導致當前執行緒釋放掉鎖資源
  • 注意

    • 等待和喚醒的方法,都要使用鎖物件呼叫(需要在同步程式碼塊中呼叫)
    • 等待和喚醒方法應該使用相同的鎖物件呼叫
生產者和消費者案例
package com.itcast.waitnotify_demo2;

import sun.security.krb5.internal.crypto.Des;

/*
    生產者步驟:
        1,判斷桌子上是否有漢堡包
            如果有就等待,如果沒有才生產。
        2,把漢堡包放在桌子上。
        3,叫醒等待的消費者開吃
 */
public class Cooker implements Runnable {
    @Override
    public void run() {
        while (true) {
            synchronized (Desk.lock) {
                if (Desk.count == 0) {
                    break;
                } else {
                    if (Desk.flag) {
                        // 桌子上有食物
                        try {
                            Desk.lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    } else {
                        // 桌子上沒有食物
                        System.out.println("廚師生產了一個漢堡包...");
                        Desk.flag = true;
                        Desk.lock.notify();
                    }
                }
            }
        }
    }
}

package com.itcast.waitnotify_demo2;

import sun.security.krb5.internal.crypto.Des;

/*
    消費者步驟:
        1,判斷桌子上是否有漢堡包。
        2,如果沒有就等待。
        3,如果有就開吃
        4,吃完之後,桌子上的漢堡包就沒有了
            叫醒等待的生產者繼續生產
            漢堡包的總數量減一
 */
public class Foodie implements Runnable {
    @Override
    public void run() {
        while (true) {
            synchronized (Desk.lock) {
                if (Desk.count == 0) {
                    break;
                } else {
                    if (Desk.flag) {
                        // 桌子上有食物
                        System.out.println("吃貨吃了一個漢堡包...");
                        Desk.count--; // 漢堡包的數量減少一個
                        Desk.flag = false;// 桌子上的食物被吃掉 , 值為false
                        Desk.lock.notify();
                    } else {
                        // 桌子上沒有食物
                        try {
                            Desk.lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }
}

package com.itcast.waitnotify_demo2;

public class Test {
    public static void main(String[] args) {
        new Thread(new Foodie()).start();
        new Thread(new Cooker()).start();
    }
}

6 執行緒池

6.1 執行緒使用存在的問題

  • 如果併發的執行緒數量很多,並且每個執行緒都是執行一個時間很短的任務就結束了,這樣頻繁建立執行緒就會大大降低系統的效率,因為頻繁建立執行緒和銷燬執行緒需要時間。
    如果大量執行緒在執行,會涉及到執行緒間上下文的切換,會極大的消耗CPU運算資源

6.2 執行緒池的介紹

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

6.3 執行緒池使用的大致流程

  • 建立執行緒池指定執行緒開啟的數量
  • 提交任務給執行緒池,執行緒池中的執行緒就會獲取任務,進行處理任務。
  • 執行緒處理完任務,不會銷燬,而是返回到執行緒池中,等待下一個任務執行。
  • 如果執行緒池中的所有執行緒都被佔用,提交的任務,只能等待執行緒池中的執行緒處理完當前任

6.4 執行緒池的好處

  • 降低資源消耗。減少了建立和銷燬執行緒的次數,每個工作執行緒都可以被重複利用,可執行多個任務。
  • 提高響應速度。當任務到達時,任務可以不需要等待執行緒建立 , 就能立即執行。
  • 提高執行緒的可管理性。可以根據系統的承受能力,調整執行緒池中工作線執行緒的數目,防止因為消耗過多的記憶體 (每個執行緒需要大約1MB記憶體,執行緒開的越多,消耗的記憶體也就越大,最後當機)。

6.5 Java提供好的執行緒池

  • java.util.concurrent.ExecutorService 是執行緒池介面型別。使用時我們不需自己實現,JDK已經幫我們實現好了
  • 獲取執行緒池我們使用工具類java.util.concurrent.Executors的靜態方
    • public static ExecutorService newFixedThreadPool (int num) : 指定執行緒池最大執行緒池數量獲取執行緒池
  • 執行緒池ExecutorService的相關方法
    • Future submit(Callable task)
    • Future<?> submit(Runnable task)
  • 關閉執行緒池方法(一般不使用關閉方法,除非後期不用或者很長時間都不用,就可以關閉)
    • void shutdown() 啟動一次順序關閉,執行以前提交的任務,但不接受新任務

6.6 執行緒池處理Runnable任務

package com.itcast.threadpool_demo;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/*
    1 需求 :
        使用執行緒池模擬游泳教練教學生游泳。
        游泳館(執行緒池)內有3名教練(執行緒)
        游泳館招收了5名學員學習游泳(任務)。

    2 實現步驟:
        建立執行緒池指定3個執行緒
        定義學員類實現Runnable,
        建立學員物件給執行緒池
 */
public class Test1 {
    public static void main(String[] args) {
        // 建立指定執行緒的執行緒池
        ExecutorService threadPool = Executors.newFixedThreadPool(3);

        // 提交任務
        threadPool.submit(new Student("小花"));
        threadPool.submit(new Student("小紅"));
        threadPool.submit(new Student("小明"));
        threadPool.submit(new Student("小亮"));
        threadPool.submit(new Student("小白"));

        threadPool.shutdown();// 關閉執行緒池
    }
}

class Student implements Runnable {
    private String name;

    public Student(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        String coach = Thread.currentThread().getName();
        System.out.println(coach + "正在教" + name + "游泳...");

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(coach + "教" + name + "游泳完畢.");
    }
}

6.7 執行緒池處理Callable任務

package com.itcast.threadpool_demo;

import java.util.concurrent.*;

/*
    需求: Callable任務處理使用步驟
        1 建立執行緒池
        2 定義Callable任務
        3 建立Callable任務,提交任務給執行緒池
        4 獲取執行結果

    <T> Future<T> submit(Callable<T> task) : 提交Callable任務方法
    返回值型別Future的作用就是為了獲取任務執行的結果。
    Future是一個介面,裡面存在一個get方法用來獲取值

    練一練:使用執行緒池計算 從0~n的和,並將結果返回
 */
public class Test2 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 建立指定執行緒數量的執行緒池
        ExecutorService threadPool = Executors.newFixedThreadPool(10);

        Future<Integer> future = threadPool.submit(new CalculateTask(100));
        Integer sum = future.get();
        System.out.println(sum);
    }
}

// 使用執行緒池計算 從0~n的和,並將結果返回
class CalculateTask implements Callable<Integer> {
    private int num;

    public CalculateTask(int num) {
        this.num = num;
    }

    @Override
    public Integer call() throws Exception {
        int sum = 0;// 求和變數
        for (int i = 0; i <= num; i++) {
            sum += i;
        }
        return sum;
    }
}

相關文章