1. 程式人生 > 其它 >多執行緒篇

多執行緒篇

1、簡介

1.1、程序

程序是一個具有一定獨立功能的程式在一個數據集上的一次動態執行的過程,是作業系統進行資源分配和排程的一個獨立單位,是應用程式執行的載體。

1.2、執行緒

程式執行中一個單一的順序控制流程,是程式執行流的最小單元,是處理器排程和分派的基本單位。一個程序可以有一個或多個執行緒,各個執行緒之間共享程式的記憶體空間(也就是所在程序的記憶體空間)。

1.3、程序與執行緒的區別

  1. 執行緒是程式執行的最小單位,而程序是作業系統分配資源的最小單位。

  2. 一個程序由一個或多個執行緒組成,執行緒是一個程序中程式碼的不同執行路線。

  3. 程序之間相互獨立,但同一程序下的各個執行緒之間共享程式的記憶體空間(包括程式碼段,資料集,堆等)及一些程序級的資源(如開啟檔案和訊號等),某程序內的執行緒在其他程序不可見。

  4. 排程和切換:執行緒上下文切換比程序上下文切換要快得多。

2、執行緒的三種建立方式

  • 繼承Thread類
  • 實現Runnable介面
  • 實現Callable介面

補充:Runnable與Callable的相同與不同:

  • 相同:都是介面,都可以編寫多執行緒程式,都採用Thread.start()啟動執行緒。
  • 不同:Runnable沒有返回值,Callable可以返回執行結果。Callable的call()方法允許丟擲異常,Runnable的run方法不能丟擲異常。Callable的返回結果需要呼叫FutureTask.get()得到,此方法會阻塞主執行緒,不呼叫則不會阻塞。

3、併發與並行

3.1、併發

把任務在不同的時間點交給處理器進行處理。在同一時間點,多個任務並不會同時執行。

多個執行緒操作同一個資源下,執行緒不安全,導致資料混亂的問題。

3.2、並行

把每一個任務分配給每一個處理器獨立完成。在同一時間點,任務一定是同時執行的。

4、執行緒的五個狀態

  1. 建立執行緒:構建出一個執行緒例項。
  2. 就緒:呼叫start()方法。
  3. 執行:獲得CPU資源,開始執行。
  4. 阻塞:因為某種原因放棄CPU使用權。
    • 等待阻塞:wait()方法,進入等待佇列,返回就緒狀態。
    • 同步阻塞:物件的同步鎖被別的執行緒佔用,放入鎖池(lock pool)。
    • 其他阻塞:sleep()或者join()方法,只有當sleep()方法超時或者join()方法等待執行緒終止或超時,執行緒重新轉入可執行狀態。
  5. 死亡:執行緒正常執行完畢,或者因異常退出了run()方法,死亡的執行緒不可再執行。

5、執行緒的方法

方法 說明
setPriority(int newPriority) 更該執行緒的優先順序
static void sleep(long millis) 在指定的毫秒數內讓當前正在執行的執行緒休眠
void join() 等待該執行緒終止
static void yield() 暫停當前正在執行的執行緒物件,並執行其他執行緒
void interrupt() 中斷執行緒,別用這種方式
boolean isAlive() 測試執行緒是否處於活動狀態

5.1、停止執行緒

不推薦JDK的方法,推薦讓執行緒自己停下來。

使用一個標誌位進行終止變數。

如下:

// 1.執行緒中定義執行緒體使用的標識
private boolean flag = true;

@Override
public void run() {
    // 2.執行緒體使用該標識
	while (flag) {
        // 執行內容程式碼...
    }
}

// 3.對外提供方法改變標識
public void stop() {
    this.flag = false;
}

5.2、執行緒休眠

  • sleep(時間)指定當前執行緒阻塞的毫秒數。
  • sleep存在異常InterruptedException。
  • sleep時間達到後執行緒進入就緒狀態。
  • sleep可以模擬網路延時,倒計時等。
  • 每一個物件都有一個鎖,sleep不會釋放鎖。

5.3、執行緒禮讓

  • 讓當前正在執行的執行緒暫停,但不阻塞。
  • 將執行緒從執行狀態轉未就緒狀態。
  • 讓CPU重新排程,但是禮讓不一定成功,取決於CPU排程。

5.4、執行緒合併

  • 等當前執行緒執行完成後,再執行其他執行緒,其他執行緒阻塞。
  • 可以想象成插隊。

6、觀測執行緒狀態

呼叫getState()方法可以檢視當前執行緒的當前狀態。

7、執行緒的優先順序

  • java提供了一個執行緒排程器來監控程式中啟動後進入就緒狀態的所有執行緒,執行緒排程器按照優先順序決定應該排程哪個執行緒來執行。
  • 執行緒的優先順序用數字表示,取值範圍1~10。
  • 使用以下方式更改或者獲取優先順序:
    • getPriority()
    • setPriority(int priority)
  • 優先順序不代表級別越高的就一定比級別低的先執行,只是被CPU排程到的概率被提高了。
  • 優先順序建議設定在start()方法之前。

8、守護執行緒

  • 執行緒分為使用者執行緒與守護執行緒。
  • 虛擬機器必須確保使用者執行緒執行完畢。
  • 虛擬機器不用等待守護執行緒執行完畢,如後臺記錄操作日誌、監控記憶體、垃圾回收等待...
  • 設定方式為setDaemon(boolean on),預設false表示是使用者執行緒,true為守護執行緒。

9、執行緒同步

為了解決在多執行緒中出現的併發問題。

執行緒同步就是一種等待機制。多個需要同時訪問此物件的執行緒進入這個物件的等待池形成佇列,讓前面的執行緒使用完畢後,下一個執行緒再使用。

9.1、synchronized

9.1.1、修飾物件的幾種方式

  1. 修飾一個程式碼塊,被修飾的程式碼塊稱為同步語句塊,其作用的範圍是大括號{}括起來的程式碼,作用的物件是呼叫這個程式碼塊的物件;

    1. 修飾一個方法,被修飾的方法稱為同步方法,其作用的範圍是整個方法,作用的物件是呼叫這個方法的物件;
    2. 修改一個靜態的方法,其作用的範圍是整個靜態方法,作用的物件是這個類的所有物件;
    3. 修改一個類,其作用的範圍是synchronized後面括號括起來的部分,作用的物件是這個類的所有物件。

9.1.2、同步監視器

  • 同步塊:

    synchronized(Obj) {
    	// 此處省略...
    }
    
  • Obj被稱之為同步監視器。

    • Obj可以是任何物件,但是推薦使用共享資源作為同步監視器。
    • 同步方法中無需指定同步監視器,因為同步方法的同步監視器就是呼叫該同步方法的物件。
  • 同步監視器的執行過程

    1. 第一個執行緒訪問,鎖定同步監視器,執行其中程式碼。
    2. 第二個執行緒訪問,發現同步監視器被鎖定,無法執行。
    3. 第一個執行緒執行完畢,解鎖同步監視器。
    4. 第二個執行緒訪問,發現同步監視器沒有鎖定,然後鎖定並訪問。

9.2、死鎖

多個執行緒持有對方的鎖,同時都需要對方釋放鎖從而導致僵持的就是死鎖。

9.2.1、產生死鎖的必要條件

  • 互斥條件:一個資源僅為一程序所佔用。

  • 請求和保持條件:一個程序請求資源而阻塞時,對已獲得的資源保持不放。

  • 不剝奪條件:程序已獲得的資源在未使用完之前,不能強勢剝奪。

  • 迴圈等待條件:在發生死鎖時,必然存在一個迴圈鏈。

9.2.2、預防死鎖

避免或者破壞產生死鎖的四個必要條件中的一個或幾個來預防死鎖的產生。

9.2.3、檢測死鎖

使用jps命令。

  1. jps -l檢視JAVA程序號。
  2. jstack pid檢視程序的堆疊情況。

9.3、Lock

java.util.concurrent.locks.Lock介面是控制多個執行緒對共享資源進行訪問的工具。ReentrantLock類(可重入鎖)擁有與synchronized相同的執行緒同步機制。

9.3.1、ReentrantLock

可重入鎖。無參構造方法是預設(false)的非公平鎖,傳boolean值true給構造方法則是公平鎖。

可重入就是說某個執行緒已經獲得某個鎖,可以再次獲取這個鎖而不會出現死鎖。

注意:

  • ReentrantLock 和 synchronized 不一樣,需要手動釋放鎖,所以使用 ReentrantLock的時候一定要手動釋放鎖,並且加鎖次數和釋放次數要一樣。

9.4、synchronized與Lock的區別

  • synchronized是Java語言的關鍵字,Lock是JUC包下的一個介面類。
  • Lock是顯示鎖(手動開啟和關閉鎖);synchronized是隱式鎖,出了作用域自動釋放。
  • Lock鎖只有程式碼塊鎖,synchronized有程式碼塊鎖和方發鎖。
  • synchronized無法判斷是否獲取鎖的狀態,Lock可以通過tryLock()方法判斷是否獲取到鎖。
  • 使用Lock鎖,JVM將花費較少的時間來排程執行緒,效能更好。

10、執行緒協作(通訊)

方法名 作用
wait() 表示執行緒一直等待,直到其他執行緒通知。與sleep不同,他會釋放鎖。
wait(long timeout) 指定等待的毫秒數。
notify() 喚醒一個處於等待狀態的執行緒。
notifyAll() 喚醒同一個物件上所有呼叫wait()方法的執行緒,優先級別高的執行緒優先排程。

注意:以上均是Object類的方法,都只能在同步方法或者同步程式碼塊中使用,否則會丟擲異常IIIegalMonitorStateException。

10.1、生產者--消費者例子

10.1.1、管程法

  • 生產者:負責生產資料。
  • 消費者:負責處理資料。
  • 緩衝區:消費者不能直接使用生產者的資料,在生產者與消費者之間有一個數據的緩衝區。

程式碼例項:

package com.thread.gaoji;

//測試: 生產者消費者模型-->利用緩衝區解決:管程法

//生產者 , 消費者 , 產品 , 緩衝區
public class TestPC {

    public static void main(String[] args) {
        SynContainer container = new SynContainer();

        new Productor(container).start();
        new Consumer(container).start();
    }
}

//生產者
class Productor extends Thread {
    SynContainer container;

    public Productor(SynContainer container) {
        this.container = container;
    }

    //生產
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            container.push(new Chicken(i));
            System.out.println("生產了" + i + "只雞");
        }
    }
}

//消費者
class Consumer extends Thread {
    SynContainer container;

    public Consumer(SynContainer container) {
        this.container = container;
    }

    //消費
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("消費了-->" + container.pop().id + "只雞");
        }
    }
}

//產品
class Chicken {
    int id;//編號

    public Chicken(int id) {
        this.id = id;
    }
}

//緩衝區
class SynContainer {

    //需要一個容器大小
    Chicken[] chickens = new Chicken[10];

    //容器計數器
    int count = 0;

    //生產者放入產品
    public synchronized void push(Chicken chicken) {
        //如果容器滿了,就需要等待消費者消費
        if (count == chickens.length) {
            //生產者等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //如果沒有滿,我們需要丟入產品
        chickens[count] = chicken;
        count++;

        //可以通知消費者消費了.
        this.notifyAll();
    }

    //消費者消費產品
    public synchronized Chicken pop() {
        //判斷能否消費
        if (count == 0) {
            //消費者等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        //如果可以消費
        count--;
        Chicken chicken = chickens[count];

        //吃完了,通知生產者生產
        this.notifyAll();
        return chicken;
    }
}

10.1.2、訊號燈法

  • 生產者:負責生產資料。
  • 消費者:負責處理資料。
  • 標誌位:通過標誌位boolean值來決定生產者和消費者執行緒的執行。通過這樣一個判斷方式,只要來判斷什麼時候讓他等待,什麼時候將他喚醒就ok。

程式碼例項:

package com.thread.gaoji;

//測試生產者消費者問題2:訊號燈法,通過標誌位解決

public class TestPC2 {
    public static void main(String[] args) {
        TV tv = new TV();
        new Player(tv).start();
        new Watcher(tv).start();
    }
}

//生產者-->演員
class Player extends Thread {
    TV tv;

    public Player(TV tv) {
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            if (i % 2 == 0) {
                this.tv.play("快樂大本營播放中");
            } else {
                this.tv.play("抖音:記錄美好生活");
            }
        }
    }
}

//消費者-->觀眾
class Watcher extends Thread {
    TV tv;

    public Watcher(TV tv) {
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            tv.watch();
        }
    }
}

//產品-->節目
class TV {
    //演員表演,觀眾等待 T
    //觀眾觀看,演員等待 F
    String voice; // 表演的節目
    boolean flag = true;


    //表演
    public synchronized void play(String voice) {

        if (!flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("演員表演了:" + voice);
        //通知觀眾觀看
        this.notifyAll();
        this.voice = voice;
        this.flag = !this.flag;
    }

    //觀看
    public synchronized void watch() {
        if (flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("觀看了:" + voice);
        //通知演員表演
        this.notifyAll();
        this.flag = !this.flag;
    }
}

本文來自部落格園,作者:是老胡啊,轉載請註明原文連結:https://www.cnblogs.com/jetty-9527/p/15906437.html