Java---多線程入門

来源:https://www.cnblogs.com/buzuweiqi/archive/2022/09/05/16641509.html
-Advertisement-
Play Games

前置知識 什麼是進程,什麼又是線程?咱不是講系統,簡單說下,知道個大概就好了。 進程:一個可執行文件執行的過程。 線程:操作系統能夠進行運算調度的最小單位。它被包含在進程之中,是進程中的實際運作單位。一條線程指的是進程中一個單一順序的控制流,一個進程中可以併發多個線程,每條線程並行執行不同的任務 什 ...


前置知識

什麼是進程,什麼又是線程?咱不是講系統,簡單說下,知道個大概就好了。

進程:一個可執行文件執行的過程。
線程:操作系統能夠進行運算調度的最小單位。它被包含在進程之中,是進程中的實際運作單位。一條線程指的是進程中一個單一順序的控制流,一個進程中可以併發多個線程,每條線程並行執行不同的任務

什麼是並行,什麼是併發?這個也簡單說下。

並行:cpu的兩個核心分別執行兩個線程。
併發:cpu的一個核心在兩個(或多個)線程上反覆橫跳執行。

線程的創建

繼承Thread

// 聲明
class T extends Thread {
    public void run() {
        // do something
    }
}
// 使用
new T().start();

實現Runnable

// 聲明
class T implements Runnable {
    public void run() {
        // do something
    }
}
// 使用
new Thread(new T()).start();

為什麼是start,而不是run,其實run只是個很普通的方法,我們來看看start的源碼。

public synchronized void start() {
    if (threadStatus != 0)
        throw new IllegalThreadStateException();
    group.add(this);
    boolean started = false;
    try {
        start0(); // 這個才是開啟線程
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
            /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
        }
    }
}
// start0的實現
private native void start0(); // 這是一個native方法,通常使用C/C++來實現

多線程機流程(從啟動到終止)

我們通過一個案例來說明。

順便說說sleep()

public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        new Thread(new T0(), "T0").start();
        int cnt = 0;
        while (cnt < 50) {
            cnt ++;
            System.out.println(Thread.currentThread().getName());
            Thread.sleep(200); // 讓當前線程停止200毫秒
        }
    }
}
class T0 implements Runnable {
    int cnt;
    @Override
    public void run() {
        while (cnt < 50) {
            cnt ++;
            System.out.println(Thread.currentThread().getName());
            try {
                Thread.sleep(200); // 讓當前線程停止200毫秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

執行流程:

  1. 啟動main方法(進程開啟)
  2. 啟動main線程
  3. 列印main/列印T0(根據線程調度執行)
  4. 其中一個線程結束
  5. 另一個線程結束
  6. 進程結束

線程常用方法

  • setName:設置線程名稱,不設置則使用預設線程名稱。

  • getName:獲取線程名稱。

  • start:開啟線程。實際開啟線程的方法為start0。

  • run:調用start後創建的新線程會調用run方法。單純調用run方法無法達到多線程的效果,run方法只是個普通的方法。

  • setPriority:更改線程優先順序。

  • getPriority:獲取線程優先順序。

    線程的優先順序:
    public static final int MIN_PRIORITY = 1;
    public static final int NORM_PRIORITY = 5;
    public static final int MAX_PRIORITY = 10;
    
  • sleep:讓線程休眠指定時間。

線程終止

雖然Thread中提供了一個stop方法用來停止線程,但目前已經被廢棄。那麼我們如何停止線程呢?我們來看看下麵這個例子。

public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        T0 t0 = new T0();
        new Thread(t0, "T0").start();
        int cnt = 0;
        while (cnt < 50) {
            cnt ++;
            System.out.println(Thread.currentThread().getName());
        }
        t0.flag = false; // 設置為false用以跳出T0線程的迴圈
        /*
        	在main線程執行了50次後退出T0線程,接著退出main線程
         */
    }
}
class T0 implements Runnable {
    boolean flag = true; // 定義一個標記,用來控制線程是否停止
    @Override
    public void run() {
        while (flag) {
            System.out.println(Thread.currentThread().getName());
        }
    }
}

通過定義一個標記flag讓線程退出。

線程中斷

Thread中有一個interrupt方法,這個方法不是說中斷線程的運行,而是中斷線程當前執行的操作。我們來看下樣例。

public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        T0 t0 = new T0();
        Thread thread0 = new Thread(t0, "T0");
        thread0.start();
        System.out.println("張三在划水。。。");
        Thread.sleep(2000);
        thread0.interrupt(); // 通過拋出一個InterruptedException異常來打斷當前操作(sleep)
    }
}
class T0 implements Runnable {
    boolean flag = true;
    @Override
    public void run() {
        System.out.println("李四在打盹。。。");
        while (flag) {
            try {
                Thread.sleep(20000); // 2秒後被interrupt中斷
            } catch (InterruptedException e) {
                flag = false;
                System.out.println("老闆來了,張三搖醒了李四。。。"); // 由於main線程調用了interrupt,實際過了2秒就輸出了
            }
        }
    }
}

輸出結果:

張三在划水。。。
李四在打盹。。。
老闆來了,張三搖醒了李四。。。

線程讓步

Thread類中提供了yield方法,用來禮讓cpu資源。禮讓了資源就會執行其他線程嗎?我們看看這個例子

public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        T0 t0 = new T0();
        Thread thread0 = new Thread(t0, "T0");
        thread0.start();
        int cnt = 0;
        while (cnt < 5) {
            cnt ++;
            System.out.println(Thread.currentThread().getName());
            Thread.yield(); // 放棄當前的cpu資源,讓cpu重新分配資源。
        }
    }
}
class T0 implements Runnable {
    int cnt;
    @Override
    public void run() {
        int cur = 0; // 連續吃的包子數
        while (cnt < 5) {
            cnt ++;
            System.out.println(Thread.currentThread().getName());
            Thread.yield(); // 放棄當前的cpu資源,讓cpu重新分配資源。
        }
    }
}

輸出結果:

main
T0
main
main
main
main
T0
T0
T0
T0

可以看到兩個線程互相禮讓,如果yield方法會強制執行其他線程的話,那線程應該會交替執行,而不是有連續執行同一個線程的情況。所以證明瞭yield並不是強制禮讓。

線程插隊

Thread類提供了join方法,可以指定一個線程優先執行。

public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        Thread thread0 = new Thread(new T0(), "T0");
        thread0.start();
        Thread thread1 = new Thread(new T0(), "T1");
        thread1.start();
        int cnt = 0;
        while (cnt < 2) {
            cnt ++;
            System.out.println(Thread.currentThread().getName());
            thread0.join(); // 讓線程thread0插隊,執行完thread0的所有任務後回到當前線程。
        }
    }
}
class T0 implements Runnable {
    int cnt;
    @Override
    public void run() {
        int cur = 0;
        while (cnt < 2) {
            cnt ++;
            System.out.println(Thread.currentThread().getName());
        }
    }
}

輸出結果:

main
T1
T1
T0
T0
main

總體上main線程確實被T0插隊了,但為啥T1在T0的前面被執行?因為當前是多核CPU的環境,其它的核心在執行剩下的線程,執行T1線程的核心比T0的快所以T1在T0之前被輸出。不止有併發,還有並行。在單純併發的條件下,就變成了T0的所有任務都執行完畢後,才會執行其他線程。當前的main與T0是併發的,與T1是並行。

守護線程

Thread類提供setDaemon方法,可以設置目標線程為當前線程的守護線程,當前線程終止時,目標(守護)線程也隨之終止。

public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        Thread thread0 = new Thread(new T0(), "T0");
        thread0.setDaemon(true);
        thread0.start();
        System.out.println("張三 --> 新一天的工作開始了");
        int time = 0;
        while (time < 8) { // 張三每天工作八個小時。。。
            time ++;
        }
        System.out.println("張三 --> 下班了,回家吃老婆做的飯咯");
        System.out.println("小紅 --> 我老公張三下班了,今天就到這了");
        System.out.println("小紅 --> 守護線程YYDS");
    }
}
class T0 implements Runnable {
    int cnt;
    @Override
    public void run() {
        System.out.println("小紅 --> 李四來我家甜蜜雙排王者榮耀");
        while (true);
    }
}

輸出結果:

張三 --> 新一天的工作開始了
小紅 --> 李四來我家甜蜜雙排王者榮耀
張三 --> 下班了,回家吃老婆做的飯咯
小紅 --> 我老公張三下班了,今天就到這了
小紅 --> 守護線程YYDS

當main線程終止時,T0線程也終止。

線程的狀態

5種狀態是OS的線程狀態。而6種則說的是JVM的線程狀態。以下是狀態圖。

線程狀態

OS的線程狀態為粗體
JVM的線程狀態為英文

線程同步機制

想看個經典問題。

多線程售票問題

我們來看看案例。

public class ThreadTest {
    public static void main(String[] args) {
        T0 t0 = new T0();
        Thread thread0 = new Thread(t0, "張三");
        Thread thread1 = new Thread(t0, "李四");
        thread0.start();
        thread1.start();
    }
}
class T0 implements Runnable {
    int ticket = 100;
    @Override
    public void run() {
        while (ticket > 0) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.printf("%s買了一張票,還剩%d張%n", Thread.currentThread().getName(), -- ticket);
        }
    }
}

輸出結果:

...
張三買了一張票,還剩3張
張三買了一張票,還剩2張
李四買了一張票,還剩1張
張三買了一張票,還剩0張
李四買了一張票,還剩0張

出現了重覆賣票,造成這種現象的原因是線程不安全。那如何讓線程安全呢。這就是接下來要介紹的互斥鎖。

互斥鎖

java提供了synchronized關鍵字用以開啟鎖。看看如何使用鎖解決上面的線程安全問題。

public class ThreadTest {
    public static void main(String[] args) {
        T0 t0 = new T0();
        Thread thread0 = new Thread(t0, "張三");
        Thread thread1 = new Thread(t0, "李四");
        thread0.start();
        thread1.start();
    }
}
class T0 implements Runnable {
    int ticket = 100;
    @Override
    public synchronized void run() { // 我們將鎖加在run方法上
        while (ticket > 0) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.printf("%s買了一張票,還剩%d張%n", Thread.currentThread().getName(), -- ticket);
        }
    }
}

輸出結果:

...
張三買了一張票,還剩4張
張三買了一張票,還剩3張
張三買了一張票,還剩2張
張三買了一張票,還剩1張
張三買了一張票,還剩0張

所有的輸出都線上程張三上,這顯然不是我們想要的。
首先,為什麼會有這種現象發生?其實,被synchronized修飾的方法或代碼塊會被上鎖,併發環境下先進入該方法或者代碼塊的線程將獲得鎖並執行這部分代碼,而其他線程則處於阻塞狀態直到獲得鎖的線程執行完被上鎖的所有代碼後,其他線程才有機會去爭奪鎖。

上述現象的原因是synchronized修飾了整個方法,所以當張三拿到鎖時會執行完所有的迴圈後釋放鎖,這時李四就什麼都輸出不了了,和單線程一樣,除了多了個一直阻塞的線程,性能低下。

那麼如何保證線程安全的前提下,保證併發的性能呢?第一次嘗試解決。

public class ThreadTest {
    public static void main(String[] args) {
        T0 t0 = new T0();
        Thread thread0 = new Thread(t0, "張三");
        Thread thread1 = new Thread(t0, "李四");
        thread0.start();
        thread1.start();
    }
}
class T0 implements Runnable {
    int ticket = 100;
    @Override
    public /*synchronized*/ void run() { // 我們將鎖加在run方法上
        while (ticket > 0) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized(this) { // 存在兩個線程執行時都滿足ticket>0,仍然有線程安全問題  
                System.out.printf("%s買了一張票,還剩%d張%n", Thread.currentThread().getName(), -- ticket);
            }
        }
    }
}

輸出結果:

...
李四買了一張票,還剩3張
李四買了一張票,還剩2張
張三買了一張票,還剩1張
張三買了一張票,還剩0張
李四買了一張票,還剩-1張

雖然兩個線程恢復了併發,但線程安全問題也隨之出現。

第二次嘗試解決。

public class ThreadTest {
    public static void main(String[] args) {
        T0 t0 = new T0();
        Thread thread0 = new Thread(t0, "張三");
        Thread thread1 = new Thread(t0, "李四");
        thread0.start();
        thread1.start();
    }
}
class T0 implements Runnable {
    int ticket = 100;
    @Override
    public /*synchronized*/ void run() { // 我們將鎖加在run方法上
        while (ticket > 0) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized(this) {
                if (ticket <= 0) return; // 在同步代碼塊中再次判斷以確保線程安全
                System.out.printf("%s買了一張票,還剩%d張%n", Thread.currentThread().getName(), -- ticket);
            }
        }
    }
}

輸出結果:

...
李四買了一張票,還剩4張
李四買了一張票,還剩3張
張三買了一張票,還剩2張
張三買了一張票,還剩1張
李四買了一張票,還剩0張

通過在同步代碼塊中再次判斷以達到線程安全。

死鎖

併發編程中不只有線程安全問題,還有死鎖問題。

public class ThreadTest {
    public static void main(String[] args) {
        String lock1 = "A";
        String lock2 = "B";
        String lock3 = "C";
        Thread t0 = new Thread(new T0(lock1, lock2));
        Thread t1 = new Thread(new T0(lock2, lock3));
        Thread t2 = new Thread(new T0(lock3, lock1));
        t0.start();
        t1.start();
        t2.start();
    }
}
class T0 implements Runnable {
    String lock1;
    String lock2;
    T1(String lock1, String lock2) {
        this.lock1 = lock1;
        this.lock2 = lock2;
    }
    @Override
    public void run() {
        synchronized(lock1) {
            System.out.println("獲取鎖: " + lock1);
            synchronized(lock2) {
                System.out.println("獲取鎖: " + lock2);
            }
            System.out.println("釋放鎖: " + lock2);
        }
        System.out.println("釋放鎖: " + lock1);
    }
}

輸出結果:

獲取鎖:B
獲取鎖:A
獲取鎖:C

可以看出三個線程分別持有一把鎖,相互鎖住不能釋放,形成死鎖。
為了不寫出死鎖的併發代碼,我們需要學習釋放鎖的時機。

釋放鎖

  • run執行完畢,釋放鎖。

  • wait執行,釋放鎖。

  • sleep執行,不會釋放鎖。

  • join執行,不會釋放鎖,而是掛起當前線程。

  • notify執行,不會釋放鎖。

    public class ThreadTest {
        public static void main(String[] args) {
            String lock = "A";
            Thread thread0 = new Thread(new T0(lock), "T0");
            Thread thread1 = new Thread(new T1(lock), "T1");
            thread0.start();
            thread1.start();
        }
    }
    class T0 implements Runnable {
        String lock;
        public T0(String lock) {
            this.lock = lock;
        }
        @Override
        public void run() {
            synchronized(lock) {
                System.out.println(Thread.currentThread().getName() + "獲取鎖");
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println(Thread.currentThread().getName() + "釋放鎖");
            }
        }
    }
    class T1 implements Runnable {
        String lock;
        public T1(String lock) {
            this.lock = lock;
        }
        @Override
        public void run() {
            try {
                Thread.sleep(5000);
                synchronized(lock) {
                    System.out.println(Thread.currentThread().getName() + "獲取鎖");
                    lock.notify();
                    Thread.sleep(5000);
                    System.out.println(Thread.currentThread().getName() + "釋放鎖");
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
    

    輸出結果:

    T0獲取鎖
    T1獲取鎖
    T1釋放鎖
    T0釋放鎖
    

​ 證明notify並不會釋放鎖,只是通知一個wait的線程:Waiting → Runnable(Ready),接著在調用notify的線程執行完畢後釋放鎖。

本文來自博客園,作者:buzuweiqi,轉載請註明原文鏈接:https://www.cnblogs.com/buzuweiqi/p/16641509.html


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

-Advertisement-
Play Games
更多相關文章
  • includes() 方法用來判斷一個數組是否包含一個指定的值,根據情況,如果包含則返回 true,否則返回 false。 indexOf() 方法可返回某個制定的字元串值在字元串中首次出現的位置 indexOf的一些缺點 語義化問題,其返回值需要和 -1 比較,第一次使用無法直觀理解。 內部使用嚴 ...
  • 同時使用過渡和動畫 點擊打開視頻講解更加詳細 Vue 為了知道過渡的完成,必須設置相應的事件監聽器。它可以是 transitionend 或 animationend,這取決於給元素應用的 CSS 規則。如果你使用其中任何一種,Vue 能自動識別類型並設置監聽。 但是,在一些場景中,你需要給同一個元 ...
  • 定義:適配器模式是將一個類的介面轉換成客戶希望的另一個介面,適配器模式使得原本由於介面不相容而不能一起工作的類可以一起工作,在軟體設計中我們需要將一些“現存的對象”放到新的環境中,而新環境要求的介面是現對象所不能滿足的,我們可以使用這種模式進行介面適配轉換,使得“老對象”符合新環境的要求。 使用場景 ...
  • 構建者是一種可以將複雜對象的構建和表示分離開來,從而使得一個構建過程可以生成多個不同的表示對象。建造者模式通過一步一步構建對象。 ...
  • 觀察者模式又叫發佈-訂閱(Publish-Subscribe)模式,是對象的行為模式,訂閱是表示這些觀察者對象需要向目標對象進行註冊,這樣目標對象才知道有哪些對象在觀察它。發佈指的是當目標對象的狀態改變時,它就向它所有的觀察者對象發佈狀態更改的消息,以讓這些觀察者對象知曉。定義對象間的一種一對多的依... ...
  • 工作中總是遇到數據存儲相關的 Bug 工單,新需求開發設計中也多多少少會有數據模型設計和存儲相關的問題。經過幾次存儲方案設計選型和討論後發現需要有更全面的思考框架。 日常開發中常用的存儲方案選型很多都是 “拿來主義” 的,憑藉著經驗、習慣選用,但對它們的細節特性或約束少有研究。 除了手邊會用的存儲方... ...
  • 坦克大戰【3】 筆記目錄:(https://www.cnblogs.com/wenjie2000/p/16378441.html) 坦克大戰0.6版 √增加功能 防止敵人坦克重疊運動 記錄玩家的成績(累積擊毀敵方坦克數),存檔退出【io流】 記錄當時的敵人坦克坐標與方向,存檔退出【io流】 玩游戲時 ...
  • 發現問題 前幾天在看別人的項目的時候,發現一個問題,簡單復現一下這個問題 // 註意這是一個Integer對象的數組哦 Integer[] arr = new Integer[]{9999,88,77}; List<Integer> list = Arrays.asList(arr); // 執行以 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...