Stories

Detail Return Return

輕鬆掌握Java多線程 - 第一章:多線程入門 - Stories Detail

學習目標

  • 理解線程與多線程的基本概念
  • 掌握為什麼要使用多線程編程的主要原因
  • 學習Java中實現多線程的兩種基本方式
  • 創建並運行你的第一個多線程程序

1. 什麼是線程與多線程

1.1 線程的概念

線程是操作系統能夠進行運算調度的最小單位,也是程序執行流的最小單位。簡單來説,線程就是一個單獨的執行路徑,它可以獨立執行特定的代碼片段。

📌 提示: 可以把線程比作是一條流水線上的工人,每個工人負責完成自己的工作。多個線程就像多個工人同時工作,提高了效率。

在Java中,當我們運行一個Java程序時,JVM會創建一個主線程來執行main()方法。這個主線程就是程序默認的執行路徑。

package org.devlive.tutorial.multithreading.chapter01;

/**
 * 演示主線程的基本概念
 */
public class MainThreadDemo
{
    public static void main(String[] args)
    {
        // 獲取當前線程(主線程)
        Thread mainThread = Thread.currentThread();

        // 打印主線程信息
        System.out.println("當前執行的線程名稱:" + mainThread.getName());
        System.out.println("線程ID:" + mainThread.getId());
        System.out.println("線程優先級:" + mainThread.getPriority());
        System.out.println("線程是否為守護線程:" + mainThread.isDaemon());
        System.out.println("線程狀態:" + mainThread.getState());
    }
}

運行上面的代碼,你會看到類似這樣的輸出:

當前執行的線程名稱:main
線程ID:1
線程優先級:5
線程是否為守護線程:false
線程狀態:RUNNABLE

1.2 多線程的概念

多線程是指在一個程序中同時運行多個線程,每個線程可以執行不同的任務,且線程之間可以併發執行。在傳統的單線程程序中,任務是按順序一個接一個地執行的,而在多線程程序中,多個任務可以看起來像是同時執行的。

📌 提示: 在單核CPU上,多線程通過時間片輪轉實現"偽並行";在多核CPU上,多線程可以實現真正的並行執行。

2. 為什麼需要多線程編程

在實際開發中,多線程編程有很多優勢:

2.1 提高CPU利用率

現代計算機通常有多個CPU核心,單線程程序只能使用一個核心,而多線程程序可以充分利用多核心資源,提高CPU的利用率。

2.2 提高程序響應性

在GUI應用程序中,如果所有操作都在一個線程中進行,那麼當執行耗時操作時,整個界面會卡住無法響應用户操作。通過將耗時操作放在單獨的線程中執行,可以保持界面的響應性。

2.3 更好的資源利用

當一個線程因為I/O操作(如讀寫文件、網絡通信)而阻塞時,CPU可以切換到其他線程繼續執行,提高整體的資源利用效率。

2.4 簡化複雜問題的處理

有些問題天然適合使用多線程處理,比如服務器同時處理多個客户端請求,或者並行處理大量數據。

下面我們來看一個簡單例子,直觀感受單線程和多線程的區別:

package org.devlive.tutorial.multithreading.chapter01;

/**
 * 單線程與多線程計算對比
 */
public class MultiThreadAdvantageDemo
{
    // 執行大量計算的方法
    private static void doHeavyCalculation(String threadName)
    {
        System.out.println(threadName + " 開始計算...");
        long sum = 0;
        for (long i = 0; i < 3_000_000_000L; i++) {
            sum += i;
        }
        System.out.println(threadName + " 計算完成,結果:" + sum);
    }

    public static void main(String[] args)
    {
        long startTime = System.currentTimeMillis();

        // 單線程執行兩次計算
        System.out.println("===== 單線程執行 =====");
        doHeavyCalculation("計算任務1");
        doHeavyCalculation("計算任務2");

        long endTime = System.currentTimeMillis();
        System.out.println("單線程執行總耗時:" + (endTime - startTime) + "ms");

        // 多線程執行兩次計算
        System.out.println("\n===== 多線程執行 =====");
        startTime = System.currentTimeMillis();

        // 創建兩個線程分別執行計算任務
        Thread thread1 = new Thread(() -> doHeavyCalculation("線程1"));
        Thread thread2 = new Thread(() -> doHeavyCalculation("線程2"));

        // 啓動線程
        thread1.start();
        thread2.start();

        // 等待兩個線程執行完成
        try {
            thread1.join();
            thread2.join();
        }
        catch (InterruptedException e) {
            e.printStackTrace();
        }

        endTime = System.currentTimeMillis();
        System.out.println("多線程執行總耗時:" + (endTime - startTime) + "ms");
    }
}

在多核CPU的電腦上運行這段代碼,你會發現多線程執行的總時間明顯少於單線程執行的總時間,這就是多線程並行計算的優勢。

3. Java中實現多線程的兩種基本方式

Java提供了兩種基本的方式來創建線程:繼承Thread類和實現Runnable接口。

3.1 繼承Thread類

通過繼承Thread類並重寫其run()方法來創建一個新的線程類:

package org.devlive.tutorial.multithreading.chapter01;

/**
 * 通過繼承Thread類實現多線程
 */
public class ThreadExtendsDemo
{
    // 自定義線程類,繼承Thread類
    static class MyThread
            extends Thread
    {
        private final String message;

        public MyThread(String message)
        {
            this.message = message;
        }

        // 重寫run方法,定義線程執行的任務
        @Override
        public void run()
        {
            // 打印信息,顯示當前線程名稱
            for (int i = 0; i < 5; i++) {
                System.out.println(getName() + " 執行: " + message + " - 第" + (i + 1) + "次");

                try {
                    // 線程休眠一段隨機時間,模擬任務執行
                    Thread.sleep((long) (Math.random() * 1000));
                }
                catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(getName() + " 執行完畢!");
        }
    }

    public static void main(String[] args)
    {
        System.out.println("程序開始執行...");

        // 創建兩個線程對象
        MyThread thread1 = new MyThread("你好,世界");
        MyThread thread2 = new MyThread("Hello, World");

        // 設置線程名稱
        thread1.setName("線程1");
        thread2.setName("線程2");

        // 啓動線程
        thread1.start(); // 注意:不要直接調用run()方法
        thread2.start();

        // 主線程繼續執行
        for (int i = 0; i < 3; i++) {
            System.out.println("主線程執行 - 第" + (i + 1) + "次");
            try {
                Thread.sleep(500);
            }
            catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        System.out.println("主線程執行完畢,但程序不會立即結束,因為還有其他線程在運行");
    }
}
⚠️ 要: 啓動線程必須調用start()方法,而不是直接調用run()方法。調用start()方法會創建一個新線程並使這個線程開始執行run()方法;而直接調用run()方法只會在當前線程中執行該方法,不會創建新線程。

3.2 實現Runnable接口

通過實現Runnable接口並實現其run()方法來創建一個任務,然後將該任務傳遞給Thread對象:

package org.devlive.tutorial.multithreading.chapter01;

/**
 * 通過實現Runnable接口實現多線程
 */
public class RunnableImplDemo
{
    // 自定義任務類,實現Runnable接口
    static class MyRunnable
            implements Runnable
    {
        private final String message;

        public MyRunnable(String message)
        {
            this.message = message;
        }

        // 實現run方法,定義任務執行的內容
        @Override
        public void run()
        {
            // 獲取當前執行的線程
            Thread currentThread = Thread.currentThread();

            // 打印信息,顯示當前線程名稱
            for (int i = 0; i < 5; i++) {
                System.out.println(currentThread.getName() + " 執行: " + message + " - 第" + (i + 1) + "次");

                try {
                    // 線程休眠一段隨機時間,模擬任務執行
                    Thread.sleep((long) (Math.random() * 1000));
                }
                catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(currentThread.getName() + " 執行完畢!");
        }
    }

    public static void main(String[] args)
    {
        System.out.println("程序開始執行...");

        // 創建兩個Runnable對象
        Runnable task1 = new MyRunnable("你好,世界");
        Runnable task2 = new MyRunnable("Hello, World");

        // 創建線程對象,並傳入Runnable任務
        Thread thread1 = new Thread(task1, "線程1");
        Thread thread2 = new Thread(task2, "線程2");

        // 啓動線程
        thread1.start();
        thread2.start();

        // 主線程繼續執行
        for (int i = 0; i < 3; i++) {
            System.out.println("主線程執行 - 第" + (i + 1) + "次");
            try {
                Thread.sleep(500);
            }
            catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        System.out.println("主線程執行完畢,但程序不會立即結束,因為還有其他線程在運行");
    }
}

3.3 兩種方式的比較

特點 繼承Thread類 實現Runnable接口
代碼結構 需要繼承Thread類,Java不支持多繼承,限制了類的擴展性 只需實現Runnable接口,可以繼承其他類,更加靈活
資源共享 每個線程都是獨立的對象,不方便在多個線程間共享數據 可以多個線程使用同一個Runnable對象,便於共享數據
耦合性 任務和線程高度耦合 任務和線程分離,解耦合
適用場景 簡單的獨立線程任務 需要共享數據或複用任務的場景
📌 提示: 在實際開發中,通常推薦使用實現Runnable接口的方式,因為它更加靈活,也符合設計原則中的"組合優於繼承"原則。

3.4 使用Java 8 Lambda表達式簡化Runnable實現

從Java 8開始,我們可以使用Lambda表達式大大簡化Runnable的實現:

package org.devlive.tutorial.multithreading.chapter01;

/**
 * 使用Lambda表達式簡化多線程創建
 */
public class LambdaThreadDemo
{
    public static void main(String[] args)
    {
        System.out.println("程序開始執行...");

        // 使用Lambda表達式創建Runnable實例
        Runnable task1 = () -> {
            Thread currentThread = Thread.currentThread();
            for (int i = 0; i < 5; i++) {
                System.out.println(currentThread.getName() + " 執行: 你好,世界 - 第" + (i + 1) + "次");
                try {
                    Thread.sleep((long) (Math.random() * 1000));
                }
                catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(currentThread.getName() + " 執行完畢!");
        };

        // 再簡化一點,直接在創建線程時使用Lambda表達式
        Thread thread1 = new Thread(task1, "線程1");
        Thread thread2 = new Thread(() -> {
            Thread currentThread = Thread.currentThread();
            for (int i = 0; i < 5; i++) {
                System.out.println(currentThread.getName() + " 執行: Hello, World - 第" + (i + 1) + "次");
                try {
                    Thread.sleep((long) (Math.random() * 1000));
                }
                catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(currentThread.getName() + " 執行完畢!");
        }, "線程2");

        // 啓動線程
        thread1.start();
        thread2.start();

        // 主線程繼續執行
        for (int i = 0; i < 3; i++) {
            System.out.println("主線程執行 - 第" + (i + 1) + "次");
            try {
                Thread.sleep(500);
            }
            catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        System.out.println("主線程執行完畢,但程序不會立即結束,因為還有其他線程在運行");
    }
}

Lambda表達式使代碼更加簡潔,特別適合簡單的Runnable實現。

4. 實戰案例:創建並啓動你的第一個線程

現在,讓我們通過一個實戰案例來綜合運用所學知識。我們將創建一個模擬文件下載的程序,使用多線程同時下載多個文件。

package org.devlive.tutorial.multithreading.chapter01;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;

/**
 * 多線程文件下載模擬器
 */
public class FileDownloaderDemo
{
    // 文件下載器,實現Runnable接口
    static class FileDownloader
            implements Runnable
    {
        private final String fileName;
        private final int fileSize; // 模擬文件大小,單位MB

        public FileDownloader(String fileName, int fileSize)
        {
            this.fileName = fileName;
            this.fileSize = fileSize;
        }

        @Override
        public void run()
        {
            System.out.println(getCurrentTime() + " - 開始下載文件:" + fileName + "(" + fileSize + "MB)");

            // 模擬下載過程
            try {
                for (int i = 1; i <= 10; i++) {
                    // 計算當前下載進度
                    int progress = i * 10;
                    int downloadedSize = fileSize * progress / 100;

                    // 休眠一段時間,模擬下載耗時
                    TimeUnit.MILLISECONDS.sleep(fileSize * 50);

                    // 打印下載進度
                    System.out.println(getCurrentTime() + " - " + Thread.currentThread().getName()
                            + " 下載 " + fileName + " 進度: " + progress + "% ("
                            + downloadedSize + "MB/" + fileSize + "MB)");
                }

                System.out.println(getCurrentTime() + " - " + Thread.currentThread().getName()
                        + " 下載完成:" + fileName);
            }
            catch (InterruptedException e) {
                System.out.println(getCurrentTime() + " - " + Thread.currentThread().getName()
                        + " 下載中斷:" + fileName);
                Thread.currentThread().interrupt(); // 重設中斷狀態
            }
        }

        // 獲取當前時間的格式化字符串
        private String getCurrentTime()
        {
            SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSS");
            return sdf.format(new Date());
        }
    }

    public static void main(String[] args)
    {
        System.out.println("=== 文件下載模擬器 ===");

        // 創建多個下載任務
        FileDownloader task1 = new FileDownloader("電影.mp4", 200);
        FileDownloader task2 = new FileDownloader("音樂.mp3", 50);
        FileDownloader task3 = new FileDownloader("文檔.pdf", 10);

        // 創建線程執行下載任務
        Thread thread1 = new Thread(task1, "下載線程-1");
        Thread thread2 = new Thread(task2, "下載線程-2");
        Thread thread3 = new Thread(task3, "下載線程-3");

        // 啓動線程,開始下載
        thread1.start();
        thread2.start();
        thread3.start();

        // 主線程監控下載進度
        try {
            // 等待所有下載線程完成
            thread1.join();
            thread2.join();
            thread3.join();

            System.out.println("\n所有文件下載完成!");
        }
        catch (InterruptedException e) {
            System.out.println("主線程被中斷");
        }
    }
}

在這個實例中,我們模擬了三個不同大小文件的並行下載過程。通過使用多線程,這三個文件可以同時下載,而不需要等待一個文件下載完成後再開始下載下一個文件。join()方法使主線程等待所有下載線程完成後才結束程序。

常見問題與解決方案

問題1:Thread.sleep()方法拋出InterruptedException

問題描述: 為什麼使用Thread.sleep()方法必須捕獲InterruptedException異常?

解決方案: sleep()方法可能會被其他線程中斷,此時會拋出InterruptedException。正確的處理方式是捕獲異常並重設中斷狀態:

try {
    Thread.sleep(1000);
} catch (InterruptedException e) {
    // 記錄日誌或者執行必要的清理工作
    Thread.currentThread().interrupt(); // 重設中斷狀態
}

問題2:直接調用run()方法而不是start()方法

問題描述: 為什麼直接調用run()方法不會創建新線程?

解決方案: 直接調用run()方法只是在當前線程中執行該方法,不會啓動新線程。必須調用start()方法才能創建新線程並執行run()方法。

問題3:多線程執行順序不確定

問題描述: 如何確保多個線程按特定順序執行?

解決方案: 可以使用join()方法讓一個線程等待另一個線程完成:

Thread thread1 = new Thread(() -> {
    // 線程1的任務
});

Thread thread2 = new Thread(() -> {
    try {
        thread1.join(); // 等待thread1完成
        // 線程2的任務
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
});

thread1.start();
thread2.start();

小結

在這一章中,我們學習了以下核心內容:

  1. 線程概念: 瞭解了什麼是線程,以及線程作為程序執行的最小單位的概念。
  2. 多線程優勢: 掌握了為什麼要使用多線程編程,包括提高CPU利用率、改善程序響應性、更好的資源利用以及簡化複雜問題處理。
  3. 線程創建方式: 學習了Java中創建線程的兩種基本方式:繼承Thread類和實現Runnable接口,以及它們各自的優缺點。
  4. 簡化線程創建: 瞭解瞭如何使用Java 8 Lambda表達式簡化Runnable的實現。
  5. 實戰應用: 通過一個文件下載模擬器的實例,綜合應用了所學的多線程知識。

通過本章的學習,你已經具備了創建和啓動Java線程的基本能力。在後續章節中,我們將深入探討線程的生命週期、線程同步和安全等更高級的多線程編程主題。

記住一點:多線程編程是Java開發中的重要技能,但也是比較複雜的主題。掌握好基礎概念和實踐案例,是走向高級多線程編程的關鍵第一步。

在下一章,我們將詳細介紹線程的生命週期和狀態轉換,幫助你更深入理解線程的工作機制。

本章節源代碼地址為 https://github.com/qianmoQ/tutorial/tree/main/java-multithreading-tutorial/src/main/java/org/devlive/tutorial/multithreading/chapter01

Add a new Comments

Some HTML is okay.