執行緒的定義與Java操作例子


Posted by altheachu on 2025-07-19

本文將比較Program、Process與Thread的定義,說明基本概念,再以Java的不同方式展示多執行緒的特點。

兩岸名詞比較

關於執行緒,網路上有很多學習資源是用大陸慣用語撰寫的,所以畫張對照表當作筆記。遇到中文描述「並行」,需仔細根據上下文判斷是指Concorrent還是Parallel。這篇文章將使用台灣地區的中文名稱撰寫

英文 台灣用語 大陸用語
Process 程序 進程
Thread 執行緒 線程
Concorrent 並行 並發
Parallel 平行 並行

Program、Process 與 Thread

  • Program 程式:一整組程式代碼,因為尚未被執行,所以保留在次級儲存裝置。
  • Process 程序:已在記憶體中執行的程式,也是OS分配資源的最小單位,它的獨立ID就是在cmd裡要手動關閉程式的pid。不同程序不會共享資源,所以不需要對資源管理做特殊處理。
  • Thread 執行緒:一個程序中至少含有一個執行緒,是OS能進行運算排程的最小單位。不同執行緒會共享資源,如果多個執行緒同時存取資源,可能會發生同步(synchronized)問題,例如:A執行緒更新的值被B執行緒覆蓋掉。

Program vs Process vs Thread

所以多程序(multiprocessing)的優點是讓電腦在同一單位時間內可以完成多個不同的程序;多執行緒(multithreading)的優點則是讓同一個程序可以在較短的時間內完成。

多執行緒的簡易範例

假設我們現在想要製作10萬個執行緒,每個執行緒都在初始值為0的計數器上 + 1,最後將計數器的最終值打印出來,可以寫出以下這段程式:

public class ThreadTest {

    private static int counter = 0;

    public static void main(String[] args) throws InterruptedException {

        Thread[] threads = new Thread[100000];

        for(int i = 0; i < 100000; i++){
            threads[i] = new Thread(()->{
                counter++;
            });
            threads[i].start();
        }

        for(Thread thread:threads){
            thread.join();
        }
        System.out.println(counter);
    }
}

類別 Thread 可以幫我們產生一個執行緒,start 方法則表示執行這個執行緒預先寫入的業務邏輯;join 方法則是會封鎖主執行緒,讓Main方法的這個執行緒,在其他Thread物件執行完畢後,再繼續執行。

問題

理想上,執行打印出的結果是100000,但實際上卻並非如此,有時是99988、有時是99998,並不固定。這是因為 counter++ 實際上是 counter = counter + 1;,如果有2個執行緒同時執行就可能發生計算後的數值被覆蓋的情況:

Thread A Thread B
取得counter = 100 -
counter加1 取得counter = 100
counter被指回成101 counter加1
- counter被指回成101,覆蓋目前的counter

解決方法

ReentrantLock

這個類別產生的物件,可以鎖住其本身,只讓取得鎖的那個執行緒進入臨界區,也就是工程師要進行業務邏輯操作的部分。類別的建構子可以依參數決定實作公平鎖或非公平鎖。

    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

公平鎖依照執行緒的請求順序排序執行先後,適用於銀行櫃檯抽號機;非公平鎖則無限制,因此有可能發生插隊,但因為省去了排隊及排程成本,效能較高、吞吐量大(系統在某段時間內成功處理的請求數量),適用高併發任務。

由於只讓取得鎖的執行緒進入臨界區執行業務邏輯,所以可以避免兩個執行緒同時修改計數器。

public class ThreadTest {

    private static int counter = 0;
    // 非公平鎖
    private static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {

        Thread[] threads = new Thread[100000];

        for(int i = 0; i < 100000; i++){
            threads[i] = new Thread(()->{
                lock.lock();
                try {
                    //臨界區
                    counter++;
                } finally {
                    //釋放鎖
                    lock.unlock();
                }
            });
            threads[i].start();
        }

        for(Thread thread:threads){
            thread.join();
        }

        System.out.println(counter);
    }
}

synchronized

synchronized 字符是針對某個物件上鎖,寫成 synchronized (Object obj),讓依據這個物件執行的業務邏輯一次只能被一個執行緒操作。

public class ThreadTest {

    private static int counter = 0;
    private final static Object obj = new Object();

    public static void main(String[] args) throws InterruptedException {

        Thread[] threads = new Thread[100000];

        for(int i = 0; i < 100000; i++){
            threads[i] = new Thread(()->{
                synchronized (obj){
                    counter++;
                }
            });
            threads[i].start();
        }

        for(Thread thread:threads){
            thread.join();
        }

        System.out.println(counter);
}

但要特別小心 synchronized 包裹的物件類型,像是以下寫法就沒有上鎖的意義。

public class ThreadTest {

    private static Integer counter = 0;

    public static void main(String[] args) throws InterruptedException {

        Thread[] threads = new Thread[100000];

        for(int i = 0; i < 100000; i++){
            threads[i] = new Thread(()->{
                synchronized (counter){
                    counter++;
                }
            });
            threads[i].start();
        }

        for(Thread thread:threads){
            thread.join();
        }

        System.out.println(counter);
}

雖然Integer是物件類別,但是每次計算counter++,其實都會另建立一個不同記憶體位置的物件,所以上鎖毫無意義,計算結果仍然不會等於100000。

補充:AtomicInteger

如果是操作整數型別的邏輯,可以使用原子級別的AtomicInteger,效能更高,但這就不是鎖機制的實作了。

public class ThreadTest {

    static final AtomicInteger atomicCounter = new AtomicInteger();

    public static void main(String[] args) throws InterruptedException {

        Thread[] threads = new Thread[100000];

        for(int i = 0; i < 100000; i++){
            threads[i] = new Thread(()->{
                atomicCounter.incrementAndGet();
            });
            threads[i].start();
        }

        for(Thread thread:threads){
            thread.join();
        }

        System.out.println(atomicCounter);
}

參考資料

  1. 程序(進程)、執行緒(線程)、協程,傻傻分得清楚!
  2. Optimistic Locking vs Pessimistic Locking: Managing Concurrent Access

#執行緒 #線程







Related Posts

出發點

出發點

[ 筆記 ] React 03 - Functional Componet & Component 之間的溝通

[ 筆記 ] React 03 - Functional Componet & Component 之間的溝通

程式解題新手入門注意事項

程式解題新手入門注意事項


Comments