IO流中「執行緒」模型總結

知了一笑發表於2023-04-07

IO流模組:經常看、經常用、經常忘;

一、基礎簡介

在IO流的網路模型中,以常見的「客戶端-服務端」互動場景為例;

客戶端與服務端進行通訊「互動」,可能是同步或者非同步,服務端進行「流」處理時,可能是阻塞或者非阻塞模式,當然也有自定義的業務流程需要執行,從處理邏輯看就是「讀取資料-業務執行-應答寫資料」的形式;

Java提供「三種」IO網路程式設計模型,即:「BIO同步阻塞」、「NIO同步非阻塞」、「AIO非同步非阻塞」;

二、同步阻塞

1、模型圖解

BIO即同步阻塞,服務端收到客戶端的請求時,會啟動一個執行緒處理,「互動」會阻塞直到整個流程結束;

這種模式如果在高併發且流程複雜耗時的場景下,客戶端的請求響應會存在嚴重的效能問題,並且佔用過多資源;

2、參考案例

服務端】啟動ServerSocket接收客戶端的請求,經過一系列邏輯之後,向客戶端傳送訊息,注意這裡執行緒的10秒休眠;

public class SocketServer01 {
    public static void main(String[] args) throws Exception {
        // 1、建立Socket服務端
        ServerSocket serverSocket = new ServerSocket(8080);
        // 2、方法阻塞等待,直到有客戶端連線
        Socket socket = serverSocket.accept();
        // 3、輸入流,輸出流
        InputStream inStream = socket.getInputStream();
        OutputStream outStream = socket.getOutputStream();
        // 4、資料接收和響應
        int readLen = 0;
        byte[] buf = new byte[1024];
        if ((readLen=inStream.read(buf)) != -1){
            // 接收資料
            String readVar = new String(buf, 0, readLen) ;
            System.out.println("readVar======="+readVar);
        }
        // 響應資料
        Thread.sleep(10000);
        outStream.write("sever-8080-write;".getBytes());
        // 5、資源關閉
        IoClose.ioClose(outStream,inStream,socket,serverSocket);
    }
}

客戶端】Socket連線,先向ServerSocket傳送請求,再接收其響應,由於Server端模擬耗時,Client處於長時間阻塞狀態;

public class SocketClient01 {
    public static void main(String[] args) throws Exception {
        // 1、建立Socket客戶端
        Socket socket = new Socket(InetAddress.getLocalHost(), 8080);
        // 2、輸入流,輸出流
        OutputStream outStream = socket.getOutputStream();
        InputStream inStream = socket.getInputStream();
        // 3、資料傳送和響應接收
        // 傳送資料
        outStream.write("client-hello".getBytes());
        // 接收資料
        int readLen = 0;
        byte[] buf = new byte[1024];
        if ((readLen=inStream.read(buf)) != -1){
            String readVar = new String(buf, 0, readLen) ;
            System.out.println("readVar======="+readVar);
        }
        // 4、資源關閉
        IoClose.ioClose(inStream,outStream,socket);
    }
}

三、同步非阻塞

1、模型圖解

NIO即同步非阻塞,服務端可以實現一個執行緒,處理多個客戶端請求連線,服務端的併發能力得到極大的提升;

這種模式下客戶端的請求連線都會註冊到Selector多路複用器上,多路複用器會進行輪詢,對請求連線的IO流進行處理;

2、參考案例

服務端】單執行緒可以處理多個客戶端請求,透過輪詢多路複用器檢視是否有IO請求;

public class SocketServer01 {
    public static void main(String[] args) throws Exception {
        try {
            //啟動服務開啟監聽
            ServerSocketChannel socketChannel = ServerSocketChannel.open();
            socketChannel.socket().bind(new InetSocketAddress("127.0.0.1", 8989));
            // 設定非阻塞,接受客戶端
            socketChannel.configureBlocking(false);
            // 開啟多路複用器
            Selector selector = Selector.open();
            // 服務端Socket註冊到多路複用器,指定興趣事件
            socketChannel.register(selector, SelectionKey.OP_ACCEPT);
            // 多路複用器輪詢
            ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
            while (selector.select() > 0){
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> selectionKeyIter = selectionKeys.iterator();
                while (selectionKeyIter.hasNext()){
                    SelectionKey selectionKey = selectionKeyIter.next() ;
                    selectionKeyIter.remove();
                    if(selectionKey.isAcceptable()) {
                        // 接受新的連線
                        SocketChannel client = socketChannel.accept();
                        // 設定讀非阻塞
                        client.configureBlocking(false);
                        // 註冊到多路複用器
                        client.register(selector, SelectionKey.OP_READ);
                    } else if (selectionKey.isReadable()) {
                        // 通道可讀
                        SocketChannel client = (SocketChannel) selectionKey.channel();
                        int len = client.read(buffer);
                        if (len > 0){
                            buffer.flip();
                            byte[] readArr = new byte[buffer.limit()];
                            buffer.get(readArr);
                            System.out.println(client.socket().getPort() + "埠資料:" + new String(readArr));
                            buffer.clear();
                        }
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

客戶端】每隔3秒持續的向通道內寫資料,服務端透過輪詢多路複用器,持續的讀取資料;

public class SocketClient01 {
    public static void main(String[] args) throws Exception {
        try {
            // 連線服務端
            SocketChannel socketChannel = SocketChannel.open();
            socketChannel.connect(new InetSocketAddress("127.0.0.1", 8989));
            ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
            String conVar = "client-hello";
            writeBuffer.put(conVar.getBytes());
            writeBuffer.flip();
            // 每隔3S傳送一次資料
            while (true) {
                Thread.sleep(3000);
                writeBuffer.rewind();
                socketChannel.write(writeBuffer);
                writeBuffer.clear();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

四、非同步非阻塞

1、模型圖解

AIO即非同步非阻塞,對於通道內資料的「讀」和「寫」動作,都是採用非同步的模式,對於效能的提升是巨大的;

這與常規的第三方對接模式很相似,本地服務在請求第三方服務時,請求過程耗時很大,會非同步執行,第三方第一次回撥,確認請求可以被執行;第二次回撥則是推送處理結果,這種思想在處理複雜問題時,可以很大程度的提高效能,節省資源:

2、參考案例

服務端】各種「accept」、「read」、「write」動作是非同步,透過Future來獲取計算的結果;

public class SocketServer01 {
    public static void main(String[] args) throws Exception {
        // 啟動服務開啟監聽
        AsynchronousServerSocketChannel socketChannel = AsynchronousServerSocketChannel.open() ;
        socketChannel.bind(new InetSocketAddress("127.0.0.1", 8989));
        // 指定30秒內獲取客戶端連線,否則超時
        Future<AsynchronousSocketChannel> acceptFuture = socketChannel.accept();
        AsynchronousSocketChannel asyChannel = acceptFuture.get(30, TimeUnit.SECONDS);

        if (asyChannel != null && asyChannel.isOpen()){
            // 讀資料
            ByteBuffer inBuffer = ByteBuffer.allocate(1024);
            Future<Integer> readResult = asyChannel.read(inBuffer);
            readResult.get();
            System.out.println("read:"+new String(inBuffer.array()));

            // 寫資料
            inBuffer.flip();
            Future<Integer> writeResult = asyChannel.write(ByteBuffer.wrap("server-hello".getBytes()));
            writeResult.get();
        }

        // 關閉資源
        asyChannel.close();
    }
}

客戶端】相關「connect」、「read」、「write」方法呼叫是非同步的,透過Future來獲取計算的結果;

public class SocketClient01 {
    public static void main(String[] args) throws Exception {
        // 連線服務端
        AsynchronousSocketChannel socketChannel = AsynchronousSocketChannel.open();
        Future<Void> result = socketChannel.connect(new InetSocketAddress("127.0.0.1", 8989));
        result.get();

        // 寫資料
        String conVar = "client-hello";
        ByteBuffer reqBuffer = ByteBuffer.wrap(conVar.getBytes());
        Future<Integer> writeFuture = socketChannel.write(reqBuffer);
        writeFuture.get();

        // 讀資料
        ByteBuffer inBuffer = ByteBuffer.allocate(1024);
        Future<Integer> readFuture = socketChannel.read(inBuffer);
        readFuture.get();
        System.out.println("read:"+new String(inBuffer.array()));

        // 關閉資源
        socketChannel.close();
    }
}

五、Reactor模型

1、模型圖解

這部分內容,可以參考「Doug Lea的《IO》」文件,檢視更多細節;

1.1 Reactor設計原理

Reactor模式基於事件驅動設計,也稱為「反應器」模式或者「分發者」模式;服務端收到多個客戶端請求後,會將請求分派給對應的執行緒處理;

Reactor:負責事件的監聽和分發;Handler:負責處理事件,核心邏輯「read讀」、「decode解碼」、「compute業務計算」、「encode編碼」、「send應答資料」;

1.2 單Reactor單執行緒

【1】Reactor執行緒透過select監聽客戶端的請求事件,收到事件後透過Dispatch進行分發;

【2】如果是建立連線請求事件,Acceptor透過「accept」方法獲取連線,並建立一個Handler物件來處理後續業務;

【3】如果不是連線請求事件,則Reactor會將該事件交由當前連線的Handler來處理;

【4】在Handler中,會完成相應的業務流程;

這種模式將所有邏輯「連線、讀寫、業務」放在一個執行緒中處理,避免多執行緒的通訊,資源競爭等問題,但是存在明顯的併發和效能問題;

1.3 單Reactor多執行緒

【1】Reactor執行緒透過select監聽客戶端的請求事件,收到事件後透過Dispatch進行分發;

【2】如果是建立連線請求事件,Acceptor透過「accept」方法獲取連線,並建立一個Handler物件來處理後續業務;

【3】如果不是連線請求事件,則Reactor會將該事件交由當前連線的Handler來處理;

【4】在Handler中,只負責事件響應不處理具體業務,將資料傳送給Worker執行緒池來處理;

【5】Worker執行緒池會分配具體的執行緒來處理業務,最後把結果返回給Handler做響應;

這種模式將業務從Reactor單執行緒分離處理,可以讓其更專注於事件的分發和排程,Handler使用多執行緒也充分的利用cpu的處理能力,導致邏輯變的更加複雜,Reactor單執行緒依舊存在高併發的效能問題;

1.4 主從Reactor多執行緒

【1】 MainReactor主執行緒透過select監聽客戶端的請求事件,收到事件後透過Dispatch進行分發;

【2】如果是建立連線請求事件,Acceptor透過「accept」方法獲取連線,之後MainReactor將連線分配給SubReactor;

【3】如果不是連線請求事件,則MainReactor將連線分配給SubReactor,SubReactor呼叫當前連線的Handler來處理;

【4】在Handler中,只負責事件響應不處理具體業務,將資料傳送給Worker執行緒池來處理;

【5】Worker執行緒池會分配具體的執行緒來處理業務,最後把結果返回給Handler做響應;

這種模式Reactor執行緒分工明確,MainReactor負責接收新的請求連線,SubReactor負責後續的互動業務,適應於高併發的處理場景,是Netty元件通訊框架的所採用的模式;

2、參考案例

服務端】提供兩個EventLoopGroup,「ParentGroup」主要是用來接收客戶端的請求連線,真正的處理是轉交給「ChildGroup」執行,即Reactor多執行緒模型;

@Slf4j
public class NettyServer {
    public static void main(String[] args) {
        // EventLoop組,處理事件和IO
        EventLoopGroup parentGroup = new NioEventLoopGroup();
        EventLoopGroup childGroup = new NioEventLoopGroup();
        try {
            // 服務端啟動引導類
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(parentGroup, childGroup)
                    .channel(NioServerSocketChannel.class).childHandler(new ServerChannelInit());

            // 非同步IO的結果
            ChannelFuture channelFuture = serverBootstrap.bind(8989).sync();
            channelFuture.channel().closeFuture().sync();
        } catch (Exception e){
            e.printStackTrace();
        } finally {
            parentGroup.shutdownGracefully();
            childGroup.shutdownGracefully();
        }
    }
}

class ServerChannelInit extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel socketChannel) {
        // 獲取管道
        ChannelPipeline pipeline = socketChannel.pipeline();
        // 編碼、解碼器
        pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
        pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
        // 新增自定義的handler
        pipeline.addLast("serverHandler", new ServerHandler());
    }
}

class ServerHandler extends ChannelInboundHandlerAdapter {
    /**
     * 通道讀和寫
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("Server-Msg【"+msg+"】");
        TimeUnit.MILLISECONDS.sleep(2000);
        String nowTime = DateTime.now().toString(DatePattern.NORM_DATETIME_PATTERN) ;
        ctx.channel().writeAndFlush("hello-client;time:" + nowTime);
        ctx.fireChannelActive();
    }
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

客戶端】透過Bootstrap類,與伺服器建立連線,服務端透過ServerBootstrap啟動服務,繫結在8989埠,然後服務端和客戶端進行通訊;

public class NettyClient {
    public static void main(String[] args) {
        // EventLoop處理事件和IO
        NioEventLoopGroup eventLoopGroup = new NioEventLoopGroup();
        try {
            // 客戶端通道引導
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(eventLoopGroup)
                    .channel(NioSocketChannel.class).handler(new ClientChannelInit());

            // 非同步IO的結果
            ChannelFuture channelFuture = bootstrap.connect("localhost", 8989).sync();
            channelFuture.channel().closeFuture().sync();
        } catch (Exception e){
            e.printStackTrace();
        } finally {
            eventLoopGroup.shutdownGracefully();
        }
    }
}

class ClientChannelInit extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel socketChannel) {
        // 獲取管道
        ChannelPipeline pipeline = socketChannel.pipeline();
        // 編碼、解碼器
        pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
        pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
        // 新增自定義的handler
        pipeline.addLast("clientHandler", new ClientHandler());
    }
}

class ClientHandler extends ChannelInboundHandlerAdapter {
    /**
     * 通道讀和寫
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("Client-Msg【"+msg+"】");
        TimeUnit.MILLISECONDS.sleep(2000);
        String nowTime = DateTime.now().toString(DatePattern.NORM_DATETIME_PATTERN) ;
        ctx.channel().writeAndFlush("hello-server;time:" + nowTime);
    }
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        ctx.channel().writeAndFlush("channel...active");
    }
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

六、參考原始碼

程式設計文件:
https://gitee.com/cicadasmile/butte-java-note

應用倉庫:
https://gitee.com/cicadasmile/butte-flyer-parent

相關文章