Java 多執行緒NIO學習

ZhouCong發表於2019-04-14

IO模型

  1. 阻塞IO 如果資料沒有準備就緒,就一直等待,直到資料準備就緒;整個程式會被阻塞。
  2. 非阻塞IO 需不斷詢問核心是否已經準備好資料,非阻塞雖然不用等待但是一直佔用CPU。
  3. 多路複用IO NIO 多路複用IO,會有一個執行緒不斷地去輪詢多個socket的狀態,當socket有讀寫事件的時候才會呼叫IO讀寫操作。 用一個執行緒管理多個socket,是通過selector.select()查詢每個通道是否有事件到達,如果沒有事件到達,則會一直阻塞在那裡,因此也會帶來執行緒阻塞問題。
  4. 訊號驅動IO模型 在訊號驅動IO模型中,當使用者發起一個IO請求操作時,會給對應的socket註冊一個訊號函式,執行緒會繼續執行,當資料準備就緒的時候會給執行緒傳送一個訊號,執行緒接受到訊號時,會在訊號函式中進行IO操作。 非阻塞IO、多路複用IO、訊號驅動IO都不會造成IO操作的第一步,檢視資料是否準備就緒而帶來的執行緒阻塞,但是在第二步,對資料進行拷貝都會使執行緒阻塞。
  5. 非同步IO jdk7AIO 非同步IO是最理想的IO模型,當執行緒發出一個IO請求操作時,接著就去做自己的事情了,核心去檢視資料是否準備就緒和準備就緒後對資料的拷貝,拷貝完以後核心會給執行緒傳送一個通知說整個IO操作已經完成了,資料可以直接使用了。 同步的IO操作在第二個階段,對資料的拷貝階段,都會造成執行緒的阻塞,非同步IO則不會。

非同步IO在IO操作的兩個階段,都不會使執行緒阻塞。 Java 的 I/O 依賴於作業系統的實現。

Java NIO的工作原理

Java 多執行緒NIO學習

  1. 由一個專門的執行緒(Selector)來處理所有的IO事件,並負責分發。
  2. 事件驅動機制:事件到的時候觸發,而不是同步的去監視事件。
  3. 執行緒通訊:執行緒之間通過 wait,notify 等方式通訊。保證每次上下文切換都是有意義的。減少無謂的執行緒切換。

三大基本元件

Channel

  1. FileChannel, 從檔案中讀寫資料。
  2. DatagramChannel,通過UDP讀寫網路中的資料。
  3. SocketChannel,通過TCP讀寫網路中的資料。
  4. ServerSocketChannel,可以監聽新進來的TCP連線,對每一個新進來的連線都會建立一個SocketChannel。

Java NIO 的通道類似流,但又有些不同:

  1. 既可以從通道中讀取資料,又可以寫資料到通道。但流的讀寫通常是單向的。
  2. 通道可以非同步地讀寫。
  3. 通道中的資料總是要先讀到一個 Buffer,或者總是要從一個 Buffer 中寫入。

Buffer

關鍵的Buffer實現 ByteBuffer,CharBuffer,DoubleBuffer,FloatBuffer,IntBuffer,LongBuffer,ShortBuffer

Buffer兩種模式、三個屬性:

Java 多執行緒NIO學習

capacity
作為一個記憶體塊,Buffer有一個固定的大小值,也叫“capacity”.你只能往裡寫capacity個byte、long,char等型別。一旦Buffer滿了,需要將其清空(通過讀資料或者清除資料)才能繼續寫資料往裡寫資料。

position
當你寫資料到Buffer中時,position表示當前的位置。初始的position值為0.當一個byte、long等資料寫到Buffer後, position會向前移動到下一個可插入資料的Buffer單元。position最大可為capacity – 1. 當讀取資料時,也是從某個特定位置讀。當將Buffer從寫模式切換到讀模式,position會被重置為0. 當從Buffer的position處讀取資料時,position向前移動到下一個可讀的位置。

limit
在寫模式下,Buffer的limit表示你最多能往Buffer裡寫多少資料。 寫模式下,limit等於Buffer的capacity。 當切換Buffer到讀模式時, limit表示你最多能讀到多少資料。因此,當切換Buffer到讀模式時,limit會被設定成寫模式下的position值。換句話說,你能讀到之前寫入的所有資料(limit被設定成已寫資料的數量,這個值在寫模式下就是position)

參考連結:Buffer原理 www.cnblogs.com/chenpi/p/64…

Selector

Selector(選擇器)是Java NIO中能夠檢測一到多個NIO通道,並能夠知曉通道是否為諸如讀寫事件做好準備的元件。這樣,一個單獨的執行緒可以管理多個channel,從而管理多個網路連線。

監聽四種事件

  1. SelectionKey.OP_CONNECT
  2. SelectionKey.OP_ACCEPT
  3. SelectionKey.OP_READ
  4. SelectionKey.OP_WRITE

select()方法
select()阻塞到至少有一個通道在你註冊的事件上就緒了。 select(long timeout)和select()一樣,除了最長會阻塞timeout毫秒(引數)。
selectedKeys()方法
呼叫selector的selectedKeys()方法,訪問“已選擇鍵集(selected key set)”中的就緒通道。

參考連結:作業系統層面分析Selector原理 zhhphappy.iteye.com/blog/203289…

NIO實現

服務端

public class NIOServerSocket {
 
    //儲存SelectionKey的佇列
    private static List<SelectionKey> writeQueue = new ArrayList<SelectionKey>();
    private static Selector selector = null;
 
    //新增SelectionKey到佇列
    public static void addWriteQueue(SelectionKey key){
        synchronized (writeQueue) {
            writeQueue.add(key);
            //喚醒主執行緒
            selector.wakeup();
        }
    }
 
    public static void main(String[] args) throws IOException {
 
        // 1.建立ServerSocketChannel
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 2.繫結埠
        serverSocketChannel.bind(new InetSocketAddress(60000));
        // 3.設定為非阻塞
        serverSocketChannel.configureBlocking(false);
        // 4.建立通道選擇器
        selector = Selector.open();
        /*
         * 5.註冊事件型別
         *
         *  sel:通道選擇器
         *  ops:事件型別 ==>SelectionKey:包裝類,包含事件型別和通道本身。四個常量型別表示四種事件型別
         *  SelectionKey.OP_ACCEPT 獲取報文      SelectionKey.OP_CONNECT 連線
         *  SelectionKey.OP_READ 讀           SelectionKey.OP_WRITE 寫
         */
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        while (true) {
            System.out.println("伺服器端:正在監聽60000埠");
            // 6.獲取可用I/O通道,獲得有多少可用的通道
            int num = selector.select();
            if (num > 0) { // 判斷是否存在可用的通道
                // 獲得所有的keys
                Set<SelectionKey> selectedKeys = selector.selectedKeys();
                // 使用iterator遍歷所有的keys
                Iterator<SelectionKey> iterator = selectedKeys.iterator();
                // 迭代遍歷當前I/O通道
                while (iterator.hasNext()) {
                    // 獲得當前key
                    SelectionKey key = iterator.next();
                    // 呼叫iterator的remove()方法,並不是移除當前I/O通道,標識當前I/O通道已經處理。
                    iterator.remove();
                    // 判斷事件型別,做對應的處理
                    if (key.isAcceptable()) {
                        ServerSocketChannel ssChannel = (ServerSocketChannel) key.channel();
                        SocketChannel socketChannel = ssChannel.accept();
 
                        System.out.println("處理請求:"+ socketChannel.getRemoteAddress());
                        // 獲取客戶端的資料
                        // 設定非阻塞狀態
                        socketChannel.configureBlocking(false);
                        // 註冊到selector(通道選擇器)
                        socketChannel.register(selector, SelectionKey.OP_READ);
                    } else if (key.isReadable()) {
                        System.out.println("讀事件");
                        //取消讀事件的監控
                        key.cancel();
                        //呼叫讀操作工具類
                        NIOHandler.read(key);
                    } else if (key.isWritable()) {
                        System.out.println("寫事件");
                        //取消讀事件的監控
                        key.cancel();
                        //呼叫寫操作工具類
                        NIOHandler.write(key);
                    }
                }
            }else{
                synchronized (writeQueue) {
                    while(writeQueue.size() > 0){
                        SelectionKey key = writeQueue.remove(0);
                        //註冊寫事件
                        SocketChannel channel = (SocketChannel) key.channel();
                        Object attachment = key.attachment();
                        channel.register(selector, SelectionKey.OP_WRITE,attachment);
                    }
                }
            }
        }
    }
 
}
複製程式碼

訊息處理

public class NIOHandler {
 
    //構造執行緒池
    private static ExecutorService executorService  = Executors.newFixedThreadPool(10);
 
    public static void read(final SelectionKey key){
        //獲得執行緒並執行
        executorService.submit(new Runnable() {
 
            @Override
            public void run() {
                try {
                    SocketChannel readChannel = (SocketChannel) key.channel();
                    // I/O讀資料操作
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    ByteArrayOutputStream baos = new ByteArrayOutputStream();
                    int len = 0;
                    while (true) {
                        buffer.clear();
                        len = readChannel.read(buffer);
                        if (len == -1) break;
                        buffer.flip();
                        while (buffer.hasRemaining()) {
                            baos.write(buffer.get());
                        }
                    }
                    System.out.println("伺服器端接收到的資料:"+ new String(baos.toByteArray()));
                    //將資料新增到key中
                    key.attach(baos);
                    //將註冊寫操作新增到佇列中
                    NIOServerSocket.addWriteQueue(key);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });
    }
 
    public static void write(final SelectionKey key) {
        //拿到執行緒並執行
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    // 寫操作
                    SocketChannel writeChannel = (SocketChannel) key.channel();
                    //拿到客戶端傳遞的資料
                    ByteArrayOutputStream attachment = (ByteArrayOutputStream)key.attachment();
                    System.out.println("客戶端傳送來的資料:"+new String(attachment.toByteArray()));
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    String message = "你好,我是伺服器!!";
                    buffer.put(message.getBytes());
                    buffer.flip();
                    writeChannel.write(buffer);
                    writeChannel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });
    }
}
複製程式碼

客戶端

public class NIOClientSocket {
 
    public static void main(String[] args) throws IOException {
        //使用執行緒模擬使用者 併發訪問
        for (int i = 0; i < 1; i++) {
            new Thread(){
                public void run() {
                    try {
                        //1.建立SocketChannel
                        SocketChannel socketChannel=SocketChannel.open();
                        //2.連線伺服器
                        socketChannel.connect(new InetSocketAddress("localhost",60000));
                        //寫資料
                        String msg="我是客戶端"+Thread.currentThread().getId();
                        ByteBuffer buffer=ByteBuffer.allocate(1024);
                        buffer.put(msg.getBytes());
                        buffer.flip();
                        socketChannel.write(buffer);
                        socketChannel.shutdownOutput();
                        //讀資料
                        ByteArrayOutputStream bos = new ByteArrayOutputStream();
                        int len = 0;
                        while (true) {
                            buffer.clear();
                            len = socketChannel.read(buffer);
                            if (len == -1)
                                break;
                            buffer.flip();
                            while (buffer.hasRemaining()) {
                                bos.write(buffer.get());
                            }
                        }
                        System.out.println("客戶端收到:"+new String(bos.toByteArray()));
                        socketChannel.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                };
            }.start();
        }
    }
}
複製程式碼

多執行緒NIO Tips

  1. 示例程式碼僅供學習參考。對於一個已經被監聽到的事件,處理前先取消事件(SelectionKey .cancel())監控。否則selector.selectedKeys()會一直獲取到該事件,但該方法比較粗暴,並且後續register會產生多個SelectionKey。推薦使用selectionKey.interestOps()改變感興趣事件。
  2. Selector.select()和Channel.register()需同步。
  3. 當Channel設定為非阻塞(Channel.configureBlocking(false))時,SocketChannel.read 沒讀到資料也會返回,返回引數等於0。
  4. OP_WRITE事件,寫緩衝區在絕大部分時候都是有空閒空間的,所以如果你註冊了寫事件,這會使得寫事件一直處於就就緒,選擇處理現場就會一直佔用著CPU資源。參考下面的第二個連結。
  5. 粘包問題。

參考連結:SocketChannel.read blog.csdn.net/cao47820824…
參考連結:NIO坑 www.jianshu.com/p/1af407c04…

相關文章