Stories

Detail Return Return

深入淺出Java多線程(五):線程間通信 - Stories Detail

引言


大家好,我是你們的老夥計秀才!今天帶來的是[深入淺出Java多線程]系列的第五篇內容:線程間通信。大家覺得有用請點贊,喜歡請關注!秀才在此謝過大家了!!!

在現代編程實踐中,多線程技術是提高程序併發性能、優化系統資源利用率的關鍵手段。Java作為主流的多線程支持語言,不僅提供了豐富的API來創建和管理線程,更重要的是它內置了強大的線程間通信機制,使得多個線程能夠有效地協作並同步執行任務,從而確保數據的一致性和系統的穩定性。

在實際開發中,尤其是服務器端應用中,多線程並行處理可以極大地提升服務響應速度和吞吐量。然而,多線程環境中的共享資源訪問往往會帶來複雜性,比如競爭條件、死鎖等問題。為了解決這些問題,我們必須熟練掌握Java中用於控制線程同步與通信的各種方法和技術。

引言部分首先引入一個生活化比喻:想象一下,多線程就像是許多工人在同一工作台上協同作業,為了保證工作的有序進行和資源的安全使用,我們需要一種類似於“信號燈”或“調度員”的機制來協調這些工人之間的交互。在Java中,這種協調機制就體現在對象鎖(即互斥鎖)上,就如同只有一個工具箱可供同時操作一樣,一個對象鎖同一時間只能被一個線程持有。通過synchronized關鍵字對代碼塊或方法進行標註,我們能夠確保在任意時刻只有一個線程訪問特定的臨界區資源。

例如,考慮兩個學生線程A和B在抄寫同一份暑假作業答案的情景。為了防止他們因老師中途修改答案而造成兩人作業內容不一致的問題,我們可以通過給整個抄寫過程加上對象鎖,確保先讓老師完成修改再讓學生們開始抄寫,或者學生們抄完後再由老師去修改,這就體現了線程間的同步執行。

public class ObjectLock {
    private static final Object lock = new Object();

    static class StudentThread implements Runnable {
        @Override
        public void run() {
            synchronized (lock) {
                // 這裏模擬抄寫作業的過程
                for (int i = 0; i < 100; i++) {
                    System.out.println("Student is copying answer " + i);
                }
            }
        }
    }

    public static void main(String[] args) {
        Thread studentA = new Thread(new StudentThread());
        Thread teacher = new Thread(() -> {
            // 模擬老師修改答案前後的等待和通知邏輯
            synchronized (lock) {
                // 修改答案...
                lock.notifyAll();  // 告訴所有等待的學生現在可以繼續抄寫了
            }
        });

        studentA.start();
        // 假設老師需要修改答案
        try { Thread.sleep(1000); } catch (InterruptedException e) {}
        teacher.start();
    }
}

上述示例展示瞭如何利用對象鎖實現簡單的線程同步,確保了學生線程在老師修改答案後才開始抄寫。當然,更復雜的場景下,線程間通信還包括諸如等待/通知機制、管道流、join方法以及ThreadLocal等多樣化的技術手段。這些方法各有特色且應用場景各異,深入理解它們的工作原理並靈活運用,將有助於開發者構建高效、安全的多線程應用程序。後續章節我們將逐一探討這些機制,並通過實例代碼揭示其內在邏輯和應用場景。

鎖與同步


在Java多線程編程中,鎖和同步機制是確保多個線程正確訪問共享資源、避免併發問題的核心手段。首先,我們來深入理解這兩個概念。

概念解釋

鎖(Locking) 是基於對象的,每個Java對象都可以關聯一個內在的鎖,也被稱為“對象鎖”。當一個線程試圖訪問某個需要同步的代碼塊時,它必須先獲取到相關的對象鎖。如果該鎖已經被其他線程持有,那麼當前線程就必須等待,直到鎖被釋放。這種一對一的關係就如同婚姻中的排他性:一次只能有一個線程“結婚”(即持有鎖),而其他想要進入這段關係的線程則必須等到“離婚”(即釋放鎖)才能獲得機會。

同步(Synchronization) 則是為了保證線程間的執行順序和數據一致性。它通過synchronized關鍵字實現,使得同一時間只有一個線程可以執行特定的代碼塊或方法。同步確保了在臨界區內的操作不會被多個線程同時執行,從而有效防止了數據競爭和不一致的情況發生。比如,兩個學生線程A和B在抄寫同一份暑假作業答案時,同步機制會確保老師修改完答案後,所有學生都看到的是最新版本的答案,而不是舊版。

代碼示例

下面是一個使用對象鎖進行線程同步的簡單示例。在這個例子中,我們希望線程A完成其任務後再啓動線程B,以確保它們按序執行。

public class ObjectLockExample {
    private static final Object lock = new Object();

    static class ThreadA implements Runnable {
        @Override
        public void run() {
            synchronized (lock) {
                for (int i = 0; i < 100; i++) {
                    System.out.println("Thread A is working on task " + i);
                }
                // 線程A完成工作後,喚醒可能在等待的線程B
                lock.notify();
            }
        }
    }

    static class ThreadB implements Runnable {
        @Override
        public void run() {
            synchronized (lock) {
                try {
                    // 線程B先等待線程A完成工作
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                for (int i = 0; i < 100; i++) {
                    System.out.println("Thread B is now working on task " + i);
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread threadA = new Thread(new ThreadA());
        threadA.start();

        // 主線程等待片刻確保線程A已經獲得鎖並開始執行
        Thread.sleep(10);

        Thread threadB = new Thread(new ThreadB());
        threadB.start();
    }
}

鎖機制解析

在上述代碼中,synchronized關鍵字修飾的代碼塊表示對對象鎖的加鎖和解鎖過程。線程A首先獲得鎖並執行循環打印,執行完成後調用notify()通知等待的線程。線程B在運行時,同樣嘗試獲取相同的鎖,但由於線程A尚未釋放,因此線程B將被阻塞在synchronized代碼塊外,直至線程A調用notify()並退出同步塊,釋放鎖。此時,線程B得以獲取鎖,並從等待狀態轉為就緒狀態繼續執行。

總結起來,鎖與同步機制在Java多線程環境中起到至關重要的作用,它們約束了不同線程對共享資源的訪問秩序,確保了線程間的數據一致性以及程序的正確性。通過對鎖的合理運用,開發者可以有效地避免競態條件和死鎖等併發問題的發生。

等待/通知機制


基本原理

在Java多線程編程中,基於對象的等待/通知機制是一種高級同步手段,它允許一個或多個線程在特定條件滿足前進入等待狀態,而在其他線程完成某個操作後通過發送通知喚醒這些等待中的線程。這一機制主要依賴於java.lang.Object類提供的wait()notify()notifyAll()方法實現。

  • wait(): 當前線程調用該方法時,會釋放當前持有的對象鎖,並進入無限期等待狀態,直到被其他線程調用同一個對象的notify()notifyAll()方法喚醒。
  • notify(): 隨機喚醒一個正在等待該對象監視器(即鎖)的線程。
  • notifyAll(): 喚醒所有正在等待該對象監視器的線程。

使用等待/通知機制時,必須確保在synchronized修飾的方法或代碼塊內調用這些方法,因為只有持有對象鎖的線程才能執行它們。此外,調用wait()方法後,線程需要重新獲得鎖才能繼續執行。

實例演示

以下是一個使用等待/通知機制控制線程交替打印數字的例子:

public class WaitAndNotifyExample {
    private static final Object lock = new Object();

    static class ThreadA implements Runnable {
        @Override
        public void run() {
            synchronized (lock) {
                for (int i = 0; i < 5; i++) {
                    System.out.println("ThreadA: " + i);
                    lock.notify(); // 喚醒可能等待的線程B
                    try {
                        if (i != 4) { // 不是最後一個數則進入等待
                            lock.wait(); // 線程A等待被喚醒
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                lock.notify(); // 最後一次通知,以防萬一還有等待的線程
            }
        }
    }

    static class ThreadB implements Runnable {
        @Override
        public void run() {
            synchronized (lock) {
                for (int i = 0; i < 5; i++) {
                    try {
                        lock.wait(); // 線程B先等待,讓線程A開始
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("ThreadB: " + i);
                    lock.notify(); // 喚醒線程A進行下一輪輸出
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread threadA = new Thread(new ThreadA());
        threadA.start();
        Thread.sleep(100); // 給線程A一些時間初始化
        Thread threadB = new Thread(new ThreadB());
        threadB.start();
    }
}

運行上述代碼,將會看到線程A和線程B交替打印從0到4的整數序列。在這個例子中,線程A首先獲取鎖並打印第一個數字,然後調用notify()喚醒線程B;線程B在獲得鎖後立即調用wait()讓自己進入等待狀態,此時線程A再次獲取鎖並打印下一個數字,循環此過程直至完成五次打印。整個過程中,兩個線程通過共享的對象鎖與等待/通知機制實現了精確的協作和通信。

管道通信


定義與應用

在Java多線程編程中,管道(Pipes)是一種特殊的通信機制,它允許線程之間通過內存流進行數據傳輸。JDK提供的java.io.PipedWriterjava.io.PipedReader用於字符流之間的通信,而java.io.PipedOutputStreamjava.io.PipedInputStream則是基於字節流的通信工具。管道通信模型類似於現實生活中的水管,一個線程作為生產者將信息寫入管道的一端,另一個線程作為消費者從管道的另一端讀取這些信息。

管道通信特別適用於需要在線程間高效傳遞數據的場景,例如,一個線程負責生成數據並將其發送到另一個線程進一步處理或展示。這種機制尤其適用於避免使用共享變量帶來的同步問題,以及簡化線程間的協調工作。

代碼實踐

以下是一個利用Java管道進行線程間通信的實例代碼:

public class PipeExample {
    static class ReaderThread implements Runnable {
        private PipedReader reader;

        public ReaderThread(PipedReader reader) {
            this.reader = reader;
        }

        @Override
        public void run() {
            System.out.println("Reader thread is ready to read");
            try {
                int receive;
                while ((receive = reader.read()) != -1) {
                    System.out.print((char) receive);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    static class WriterThread implements Runnable {
        private PipedWriter writer;

        public WriterThread(PipedWriter writer) {
            this.writer = writer;
        }

        @Override
        public void run() {
            System.out.println("Writer thread is ready to write");
            try {
                writer.write("Hello, World from the pipe!");
                writer.flush(); // 確保數據被完全寫入管道
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    writer.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) throws IOException {
        PipedWriter writer = new PipedWriter();
        PipedReader reader = new PipedReader();

        // 注意:必須先連接管道兩端,否則會拋出異常
        reader.connect(writer);

        Thread readerThread = new Thread(new ReaderThread(reader));
        Thread writerThread = new Thread(new WriterThread(writer));

        readerThread.start();
        writerThread.start();

        // 等待兩個線程執行完畢
        readerThread.join();
        writerThread.join();
    }
}

運行上述代碼,輸出結果將是“Hello, World from the pipe!”。在這個示例中,我們創建了一個字符管道,並啓動了兩個線程,一個負責向管道中寫入字符串,另一個則負責從管道中讀取並打印出來。由於管道通信是單向的,因此確保了數據只能按照指定方向流動,從而實現線程間的有序通信。

其他通信方式


join方法

join() 方法是Java中 Thread 類的一個關鍵實例方法,用於同步線程執行。當一個線程調用另一個線程的 join() 方法時,當前線程將進入等待狀態,直到被調用 join() 的線程完成其任務並結束。這在需要確保主線程等待子線程執行完畢後再繼續執行的情況下尤為有用。

例如,假設主線程創建了一個耗時計算的任務交給子線程執行,並且主線程希望在子線程完成計算後獲取結果:

public class JoinExample {
    static class LongRunningTask implements Runnable {
        @Override
        public void run() {
            try {
                System.out.println("我是子線程,開始執行耗時計算...");
                Thread.sleep(2000); // 模擬耗時操作
                int result = performComputation(); // 執行計算
                System.out.println("我是子線程,計算完成,結果為: " + result);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        private int performComputation() {
            return 42; // 示例計算結果
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread longRunning = new Thread(new LongRunningTask());
        longRunning.start();

        // 主線程等待子線程完成
        longRunning.join();

        // 子線程結束後,主線程可以安全地訪問子線程的結果(此處假設已通過共享變量或其他機制傳遞)
        System.out.println("主線程:子線程已完成,我可以繼續執行後續操作了");
    }
}

sleep方法

sleep()Thread 類提供的一個靜態方法,它使當前線程暫停指定的時間量。與 wait() 方法不同的是,sleep() 不會釋放任何鎖資源,即線程在睡眠期間依然持有其已經獲得的鎖。此外,sleep() 方法不會拋出 InterruptedException 異常,除非在調用 sleep() 的過程中,該線程被中斷。

示例代碼:

public class SleepExample {
    public static void main(String[] args) throws InterruptedException {
        Thread threadA = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("Thread A is running: " + i);
                try {
                    Thread.sleep(1000); // 線程A每運行一次循環就休眠1秒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        threadA.start();
    }
}

ThreadLocal類

ThreadLocal 類提供了一種特殊的線程綁定存儲機制,每個線程都有自己的獨立副本。這意味着,即使多個線程同時引用同一個 ThreadLocal 實例,它們各自存取和修改的值也互不影響。

以下是一個使用 ThreadLocal 的簡單示例,展示如何在一個多線程環境下為每個線程維護獨立的上下文信息:

public class ThreadLocalDemo {
    public static class WorkerThread extends Thread {
        private final ThreadLocal<String> context;

        public WorkerThread(ThreadLocal<String> context, String name) {
            this.context = context;
            setName(name);
        }

        @Override
        public void run() {
            context.set(Thread.currentThread().getName() + ": Initial Value");

            // 假設進行了一些處理
            String newValue = "Processed by " + getName();
            context.set(newValue);

            System.out.println(getName() + " has its own value: " + context.get());
        }
    }

    public static void main(String[] args) {
        ThreadLocal<String> context = new ThreadLocal<>();
        WorkerThread worker1 = new WorkerThread(context, "Thread-1");
        WorkerThread worker2 = new WorkerThread(context, "Thread-2");

        worker1.start();
        worker2.start();
    }
}

在這個例子中,WorkerThread 繼承自 Thread 並使用 ThreadLocal 來保存線程特定的上下文信息。即使兩個線程都使用了相同的 ThreadLocal 實例,它們各自的 context 變量仍然保持隔離,每個線程都可以安全地讀取和更新自己的私有數據。

信號量機制


volatile關鍵字

在Java多線程編程中,volatile關鍵字用於確保變量的可見性和有序性。聲明為volatile的變量會保證當一個線程修改了該變量值時,其他所有線程都能立即看到這個修改的結果。例如,在下面的示例中,我們用volatile關鍵字實現了一個簡單的“信號量”模型來控制線程A和線程B交替打印數字:

public class SignalExample {
    private static volatile int signal = 0;

    static class ThreadA implements Runnable {
        @Override
        public void run() {
            while (signal < 5) {
                if (signal % 2 == 0) {
                    System.out.println("Thread A: " + signal);
                    synchronized (SignalExample.class) {
                        signal++;
                    }
                }
            }
        }
    }

    static class ThreadB implements Runnable {
        @Override
        public void run() {
            while (signal < 5) {
                if (signal % 2 == 1) {
                    System.out.println("Thread B: " + signal);
                    synchronized (SignalExample.class) {
                        signal = signal + 1;
                    }
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new Thread(new ThreadA()).start();
        Thread.sleep(100); // 確保線程A有機會先執行
        new Thread(new ThreadB()).start();
    }
}

儘管此處volatile關鍵字確保了對signal變量修改的可見性,但由於signal++不是原子操作,因此仍需要使用synchronized同步塊以確保更新操作的原子性。

Semaphore類

JDK提供的Semaphore類是一個更完整的信號量實現,它可以用來控制同時訪問特定資源的線程數量,從而有效解決線程間的併發控制問題。以下是一個使用Semaphore模擬停車場車位管理的例子:

import java.util.concurrent.Semaphore;

public class SemaphoreDemo {
    private final Semaphore parkingSpaces = new Semaphore(3); // 假設有3個停車位

    static class Car implements Runnable {
        private final Semaphore semaphore;

        public Car(Semaphore semaphore) {
            this.semaphore = semaphore;
        }

        @Override
        public void run() {
            try {
                semaphore.acquire(); // 請求獲取一個車位
                System.out.println(Thread.currentThread().getName() + "已停車");
                Thread.sleep(1000); // 模擬停車時間
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                semaphore.release(); // 釋放車位
                System.out.println(Thread.currentThread().getName() + "已離開");
            }
        }
    }

    public static void main(String[] args) {
        SemaphoreDemo demo = new SemaphoreDemo();

        for (int i = 0; i < 6; i++) { // 創建6輛車
            Thread car = new Thread(new Car(demo.parkingSpaces), "Car-" + (i + 1));
            car.start();
        }
    }
}

在這個例子中,Semaphore對象parkingSpaces初始化為3,表示有3個可用停車位。每輛汽車(線程)嘗試獲取一個車位前都要調用acquire()方法,成功獲取後才能進行停車動作;完成停車後通過調用release()方法釋放車位,以便其他車輛可以繼續停車。這樣便實現了基於信號量的多線程資源調度與同步。

總結


在Java多線程編程中,線程間的通信是實現協同工作和同步操作的關鍵環節。本文通過一系列實例和詳細説明,探討了多種有效的線程間通信方式。

  1. 鎖與同步
    鎖機制是Java中最基礎的同步手段,利用synchronized關鍵字或顯式Lock類確保同一時間只有一個線程訪問共享資源。代碼示例展示瞭如何使用對象鎖來確保線程A執行完畢後線程B再開始執行,從而達到線程間的有序執行。
  2. 等待/通知機制wait()notify() 方法提供了一種靈活的線程通信方式,允許線程在滿足特定條件時進入等待狀態,並在條件改變時被其他線程喚醒。通過實例演示了線程A和線程B如何交替打印數字,展示了等待/通知機制在線程協作中的應用。
  3. 管道通信
    Java提供的PipedInputStream和PipedOutputStream(或PipedReader和PipedWriter)實現了線程之間的數據流傳遞,特別適用於簡單的信息交換場景。案例中兩個線程通過管道完成字符流的讀寫操作,展現了管道通信的直觀效果。
  4. join方法Thread.join() 方法允許一個線程等待另一個線程終止後再繼續執行,保證了線程間的執行順序。示例代碼展示了主線程等待子線程計算完成後才繼續執行的操作。
  5. sleep方法Thread.sleep() 使當前線程暫停指定的時間,但它並不釋放鎖,主要用於簡單地延時線程執行。雖然在本討論中未給出具體示例,但其作用在於控制線程執行節奏。
  6. ThreadLocal類
    ThreadLocal提供了線程局部變量功能,每個線程擁有獨立的副本,解決了線程間共享變量的隔離問題。實例表明即使多個線程引用同一個ThreadLocal實例,它們各自存儲和獲取的數據互不影響。
  7. 信號量機制
    volatile關鍵字確保了變量在不同線程間的可見性,而Semaphore類則是一個更高級的信號量工具,用於管理併發訪問資源的線程數量。示例代碼模擬了一個停車場車位管理場景,通過Semaphore實現對有限資源的訪問控制。

未來隨着Java技術的發展和多核CPU普及,多線程通信的重要性日益凸顯。瞭解並掌握以上介紹的各種線程間通信方法,有助於開發者設計出更為高效、穩定且易於維護的併發程序。同時,後續章節將繼續深入講解volatile關鍵字的內存語義、信號量Semaphore在複雜場景下的運用以及更多基於JDK的線程通信工具類如CountDownLatch、CyclicBarrier等,進一步豐富和完善多線程編程的知識體系。

user avatar mannayang Avatar u_16297326 Avatar journey_64224c9377fd5 Avatar seazhan Avatar xuxueli Avatar lenglingx Avatar u_15702012 Avatar lenve Avatar boxuegu Avatar chaochenyinshi Avatar wuliaodechaye Avatar shanliangdeshou_ccwzfd Avatar
Favorites 40 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.