多線程編程基礎
今日學習目標
- 理解進程與線程的區別及多線程的應用場景
- 掌握Java中創建線程的兩種核心方式
- 學會線程的生命週期管理與常用控制方法
- 能夠識別並解決簡單的線程安全問題
核心知識點講解
1. 進程與線程的基本概念
當你啓動一個Java程序時,操作系統會創建一個進程,它擁有獨立的內存空間和系統資源。而線程則是進程內部的執行單元,一個進程可以包含多個線程,它們共享進程的內存空間但擁有各自的執行棧。
舉個生活例子:進程就像一家餐廳,擁有獨立的廚房和座位;線程則是餐廳裏的服務員,多個服務員可以同時為顧客服務,但共享餐廳的資源。
為什麼需要多線程?在單核CPU時代,多線程通過時間片切換實現"偽並行";而現在的多核CPU已經能真正並行執行多個線程,這讓文件下載器能同時下載多個文件、遊戲能同時處理畫面渲染和用户輸入成為可能。
2. 創建線程的兩種方式
方式一:繼承Thread類
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "執行: " + i);
try {
Thread.sleep(500); // 線程休眠500毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
MyThread thread1 = new MyThread();
MyThread thread2 = new MyThread();
thread1.setName("線程A");
thread2.setName("線程B");
thread1.start(); // 啓動線程,會自動調用run()方法
thread2.start();
}
}
方式二:實現Runnable接口
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "執行: " + i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable();
Thread thread1 = new Thread(runnable, "線程C");
Thread thread2 = new Thread(runnable, "線程D");
thread1.start();
thread2.start();
}
}
注意:調用start()方法才是真正啓動線程,直接調用run()方法只是普通的方法調用,不會創建新線程。
3. 線程的生命週期
線程從創建到銷燬會經歷以下狀態:
- 新建狀態(New):線程對象創建但未調用start()
- 就緒狀態(Runnable):調用start()後等待CPU調度
- 運行狀態(Running):獲得CPU時間片正在執行
- 阻塞狀態(Blocked):因等待資源或sleep等暫時停止執行
- 死亡狀態(Terminated):run()方法執行完畢或異常終止
4. 線程安全與synchronized關鍵字
當多個線程同時操作共享資源時,可能出現線程安全問題。例如兩個線程同時對同一變量進行自增操作:
public class ThreadSafetyDemo implements Runnable {
private int count = 0;
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
count++; // 非線程安全的操作
}
}
public static void main(String[] args) throws InterruptedException {
ThreadSafetyDemo demo = new ThreadSafetyDemo();
Thread t1 = new Thread(demo);
Thread t2 = new Thread(demo);
t1.start();
t2.start();
t1.join(); // 等待t1執行完畢
t2.join(); // 等待t2執行完畢
System.out.println("最終計數: " + demo.count); // 預期2000,實際可能小於2000
}
}
解決方法:使用synchronized關鍵字修飾共享資源的操作方法或代碼塊:
// 方法一:修飾方法
public synchronized void increment() {
count++;
}
// 方法二:修飾代碼塊
public void increment() {
synchronized (this) {
count++;
}
}
實例代碼分析
多線程售票系統模擬
下面是一個模擬火車站售票系統的多線程程序,包含4個售票窗口同時售票:
public class TicketSeller implements Runnable {
private int tickets = 100; // 總票數
private Object lock = new Object(); // 鎖對象
@Override
public void run() {
while (true) {
synchronized (lock) { // 同步代碼塊,保證線程安全
if (tickets <= 0) break;
// 模擬售票過程
System.out.println(Thread.currentThread().getName() +
"售出第" + tickets + "張票");
tickets--;
try {
Thread.sleep(100); // 模擬售票需要時間
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println(Thread.currentThread().getName() + "售票結束");
}
public static void main(String[] args) {
TicketSeller seller = new TicketSeller();
// 創建4個線程模擬4個售票窗口
new Thread(seller, "窗口1").start();
new Thread(seller, "窗口2").start();
new Thread(seller, "窗口3").start();
new Thread(seller, "窗口4").start();
}
}
代碼解析:
- 使用Runnable接口實現多線程,便於多個線程共享售票數據
- 創建Object對象作為鎖,確保同一時間只有一個線程執行售票操作
- 使用synchronized同步代碼塊保護共享資源tickets的訪問
- 通過Thread.sleep(100)模擬真實售票場景中的延遲
運行程序,你會看到4個窗口交替售票,不會出現重複售票或超售現象,這就是線程同步的作用。
課後練習
基礎練習
- 編寫程序,創建兩個線程,一個線程打印1-10的偶數,另一個線程打印1-10的奇數,要求兩個線程交替執行。
- 修改上述售票系統,添加餘票查詢功能,實現一個線程負責售票,另一個線程每3秒查詢一次剩餘票數。
進階挑戰
- 實現一個簡單的生產者-消費者模型:創建一個生產者線程不斷生成數據放入緩衝區,同時創建多個消費者線程從緩衝區取出數據處理,要求使用wait()和notify()方法實現線程間通信。
思考題
- 為什麼説實現Runnable接口比繼承Thread類更好?
- synchronized和volatile關鍵字有什麼區別?
- 什麼情況下會導致死鎖?如何避免?
學習總結
今天我們學習了Java多線程編程的基礎知識,包括線程的創建方式、生命週期管理和線程安全問題。多線程是Java編程中的重要概念,也是面試高頻考點。掌握多線程編程能讓你編寫出更高效的程序,充分利用現代計算機的多核處理能力。
明天我們將深入學習線程池、Callable與Future等高級多線程特性,以及Java併發包中的工具類。記得完成今天的練習,帶着問題來學習新知識!