首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java 管道流(PipedInputStream/PipedOutputStream)實現線程間通訊完整指南

     編輯:Java 相關知識

前言

在 Java 多線程開發中,線程間通訊是核心課題之一。除了常見的 wait()/notify()、BlockingQueue 等機制,Java IO 包還提供了一種獨特的通訊方式——管道流。本文將通過完整代碼示例,深入解析 PipedInputStream 與 PipedOutputStream 以及字符管道流 PipedReader/PipedWriter 的工作原理、使用細節與常見坑點。

一、位元組管道流:PipedInputStream 與 PipedOutputStream

1. 核心概念

  • PipedOutputStream:管道輸出流,負責寫入數據。
  • PipedInputStream:管道輸入流,負責讀取數據。
  • 綁定關係:兩者必須配對使用,通過 connect() 方法建立連接,形成「寫入端 → 管道緩衝區 → 讀取端」的通道。

2. 工作流程

  1. 線程 A 向 PipedOutputStream 寫入數據。
  2. 數據自動流入與之綁定的 PipedInputStream 內部緩衝區(預設 1024 位元組)。
  3. 線程 B 從 PipedInputStream 讀取數據,完成通訊。

3. 完整代碼示例

包含三個類:Sender(發送者)、Receiver(接收者)、PipedStreamTest(測試主類)。

Receiver.java(接收線程)

import java.io.IOException;
import java.io.PipedInputStream;

@SuppressWarnings("all")
public class Receiver extends Thread {
    private PipedInputStream in = new PipedInputStream();

    public PipedInputStream getInputStream() {
        return in;
    }

    @Override
    public void run() {
        readMessageOnce(); // 或 readMessageContinued()
    }

    // 單次讀取(最多 1024 位元組)
    public void readMessageOnce() {
        byte[] buf = new byte[2048];
        try {
            int len = in.read(buf);
            System.out.println(new String(buf, 0, len));
            in.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 循環讀取直到超過 1024 位元組
    public void readMessageContinued() {
        int total = 0;
        while (true) {
            byte[] buf = new byte[1024];
            try {
                int len = in.read(buf);
                total += len;
                System.out.println(new String(buf, 0, len));
                if (total > 1024) break;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        try { in.close(); } catch (IOException e) { e.printStackTrace(); }
    }
}

Sender.java(發送線程)

import java.io.IOException;
import java.io.PipedOutputStream;

@SuppressWarnings("all")
public class Sender extends Thread {
    private PipedOutputStream out = new PipedOutputStream();

    public PipedOutputStream getOutputStream() {
        return out;
    }

    @Override
    public void run() {
        writeShortMessage(); // 或 writeLongMessage()
    }

    // 寫入短訊息
    private void writeShortMessage() {
        String strInfo = "this is a short message";
        try {
            out.write(strInfo.getBytes());
            out.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 寫入長訊息(1046 位元組)
    private void writeLongMessage() {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 102; i++) sb.append("0123456789"); // 1020 位元組
        sb.append("abcdefghijklmnopqrstuvwxyz"); // 26 位元組
        String str = sb.toString(); // 總長 1046
        try {
            out.write(str.getBytes());
            out.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

PipedStreamTest.java(主測試類)

import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;

@SuppressWarnings("all")
public class PipedStreamTest {
    public static void main(String[] args) {
        Sender sender = new Sender();
        Receiver receiver = new Receiver();

        PipedOutputStream out = sender.getOutputStream();
        PipedInputStream in = receiver.getInputStream();

        try {
            // 管道連接:out.connect(in) 等價於 in.connect(out)
            in.connect(out);

            sender.start();
            receiver.start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

4. 關鍵實驗與現象解析

實驗一:發送長訊息(1046 位元組)但接收端單次讀取

  • 現象:控制台僅輸出前 1024 位元組,後 22 位元組丟失。
  • 原因:PipedInputStream 內部緩衝區預設大小為 1024 位元組(DEFAULT_PIPE_SIZE)。寫入端嘗試寫入 1046 位元組時,前 1024 位元組填滿緩衝區,後續寫入會阻塞,直到讀取端騰出空間。但接收端調用 read(buf) 後直接關閉流,導致剩餘數據無法被讀取。

實驗二:發送長訊息 + 接收端循環讀取

  • 修改:Receiver.run() 改為調用 readMessageContinued()。
  • 結果:完整輸出 1046 位元組數據。
  • 原理:讀取端循環讀取並釋放緩衝區空間,寫入端得以繼續寫入剩餘數據,雙方協作完成大數據傳輸。

二、字符管道流:PipedReader 與 PipedWriter

字符管道流用法與位元組管道流幾乎一致,差別在於處理單位為 char 而非 byte,適合文本數據傳輸。

核心類結構

  • PipedWriter:字符管道輸出流,繼承 Writer。
  • PipedReader:字符管道輸入流,繼承 Reader。

關鍵差異:寫入阻塞行為

實驗發現:PipedWriter.write() 內部會逐字符調用 PipedReader.receive(),每寫入一個字符就會阻塞等待讀取端消費(源碼中 while (in == out) 邏輯)。這導致:

  • 若發送 1046 字符,但接收端單次讀取 1024 字符後關閉流 → 發送端第 1025 字符寫入時拋出 IOException: Pipe closed。
  • 解決方案同樣是:接收端改為循環讀取(readMessageContinued()),保持管道開放直到所有數據讀完。

三、源碼級原理深度剖析

1. 管道連接機制

// PipedInputStream.connect(PipedOutputStream src)
public void connect(PipedOutputStream src) throws IOException {
    src.connect(this); // 實際委託給 PipedOutputStream
}

// PipedOutputStream.connect(PipedInputStream snk)
public void connect(PipedInputStream snk) throws IOException {
    this.sink = snk;
    snk.in = -1; snk.out = 0; // 初始化緩衝區指針
}

雙向綁定:out.sink = in 且 in.source = out,形成雙向引用。

2. 寫入流程

// PipedOutputStream.write(byte[] b, int off, int len)
sink.receive(b, off, len); // 委託給輸入流的 receive()

// PipedInputStream.receive(byte[] b, int off, int len)
// 將數據拷貝到內部 buffer,處理環狀緩衝區邏輯
// buffer 預設大小 1024 (DEFAULT_PIPE_SIZE)

3. 讀取流程

// PipedInputStream.read(byte[] b, int off, int len)
// 從 buffer 讀取數據,更新 in/out 指針,喚醒等待的寫入線程

四、最佳實踐與避坑指南

  1. 線程啟動順序無關緊要:管道內部有同步機制,先啟動讀或寫均可。
  2. 大數據傳輸必須循環讀寫:單次 read()/write() 受限於緩衝區大小(預設 1KB)。
  3. 及時關閉流:發送端寫完調用 close(),接收端讀到 -1 後關閉,否則可能導致線程永久阻塞。
  4. 字符流逐字符阻塞:PipedWriter 效率較低,大文本傳輸建議用位元組流 + 指定編碼。
  5. 緩衝區大小可定製:new PipedInputStream(pipeSize) 可調大緩衝區,減少阻塞切換開銷。

五、總結

管道流是 Java 提供的輕量級線程間通訊機制,適合生產者-消費者模式、流式數據傳遞場景。掌握其緩衝區限制、阻塞協作機制、位元組與字符流差異是正確使用的關鍵。實際項目中,若需高性能或複雜協作,仍建議優先考慮 BlockingQueue、Disruptor 或 Reactive Streams 等更高級組件。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved