學習目標
- 理解線程與多線程的基本概念
- 掌握為什麼要使用多線程編程的主要原因
- 學習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();
小結
在這一章中,我們學習了以下核心內容:
- 線程概念: 瞭解了什麼是線程,以及線程作為程序執行的最小單位的概念。
- 多線程優勢: 掌握了為什麼要使用多線程編程,包括提高CPU利用率、改善程序響應性、更好的資源利用以及簡化複雜問題處理。
- 線程創建方式: 學習了Java中創建線程的兩種基本方式:繼承Thread類和實現Runnable接口,以及它們各自的優缺點。
- 簡化線程創建: 瞭解瞭如何使用Java 8 Lambda表達式簡化Runnable的實現。
- 實戰應用: 通過一個文件下載模擬器的實例,綜合應用了所學的多線程知識。
通過本章的學習,你已經具備了創建和啓動Java線程的基本能力。在後續章節中,我們將深入探討線程的生命週期、線程同步和安全等更高級的多線程編程主題。
記住一點:多線程編程是Java開發中的重要技能,但也是比較複雜的主題。掌握好基礎概念和實踐案例,是走向高級多線程編程的關鍵第一步。
在下一章,我們將詳細介紹線程的生命週期和狀態轉換,幫助你更深入理解線程的工作機制。
本章節源代碼地址為 https://github.com/qianmoQ/tutorial/tree/main/java-multithreading-tutorial/src/main/java/org/devlive/tutorial/multithreading/chapter01