多線程編程基礎

今日學習目標

  1. 理解進程與線程的區別及多線程的應用場景
  2. 掌握Java中創建線程的兩種核心方式
  3. 學會線程的生命週期管理與常用控制方法
  4. 能夠識別並解決簡單的線程安全問題

核心知識點講解

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();
    }
}

代碼解析

  1. 使用Runnable接口實現多線程,便於多個線程共享售票數據
  2. 創建Object對象作為鎖,確保同一時間只有一個線程執行售票操作
  3. 使用synchronized同步代碼塊保護共享資源tickets的訪問
  4. 通過Thread.sleep(100)模擬真實售票場景中的延遲

運行程序,你會看到4個窗口交替售票,不會出現重複售票或超售現象,這就是線程同步的作用。

課後練習

基礎練習

  1. 編寫程序,創建兩個線程,一個線程打印1-10的偶數,另一個線程打印1-10的奇數,要求兩個線程交替執行。
  2. 修改上述售票系統,添加餘票查詢功能,實現一個線程負責售票,另一個線程每3秒查詢一次剩餘票數。

進階挑戰

  1. 實現一個簡單的生產者-消費者模型:創建一個生產者線程不斷生成數據放入緩衝區,同時創建多個消費者線程從緩衝區取出數據處理,要求使用wait()和notify()方法實現線程間通信。

思考題

  • 為什麼説實現Runnable接口比繼承Thread類更好?
  • synchronized和volatile關鍵字有什麼區別?
  • 什麼情況下會導致死鎖?如何避免?

學習總結

今天我們學習了Java多線程編程的基礎知識,包括線程的創建方式、生命週期管理和線程安全問題。多線程是Java編程中的重要概念,也是面試高頻考點。掌握多線程編程能讓你編寫出更高效的程序,充分利用現代計算機的多核處理能力。

明天我們將深入學習線程池、Callable與Future等高級多線程特性,以及Java併發包中的工具類。記得完成今天的練習,帶着問題來學習新知識!