阿里畢玄-測(cè)試Java編程能力-我的回答(一)

畢玄老師發(fā)表了一篇公眾號(hào)文章:來(lái)測(cè)試下你的Java編程能力,本系列文章為其中問(wèn)題的個(gè)人解答。

第一個(gè)問(wèn)題:

基于BIO實(shí)現(xiàn)的Server端,當(dāng)建立了100個(gè)連接時(shí),會(huì)有多少個(gè)線程?如果基于NIO,又會(huì)是多少個(gè)線程? 為什么?

說(shuō)實(shí)話,如果面試被問(wèn)到這個(gè)問(wèn)題,也不敢保證能完全答對(duì)。那么就回爐重造一下吧。

最簡(jiǎn)單的BIO Server

服務(wù)端
package com.xetlab.javatest.question1;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.Charset;

public class ServerMain1 {

    private static final Logger logger = LoggerFactory.getLogger(ServerMain1.class);

    public static void main(String[] args) {
        logger.info("0.主線程啟動(dòng)");
        try {
            //服務(wù)端初始化,在9999端口監(jiān)聽
            ServerSocket serverSocket = new ServerSocket(9999);
            while (true) {
                //等待客戶端連接,如果沒(méi)有連接就阻塞當(dāng)前線程
                Socket clientSocket = serverSocket.accept();
                logger.info("1.客戶端 {}:{} 已連接", clientSocket.getInetAddress().getHostAddress(), clientSocket.getPort());

                //向客戶端發(fā)消息
                logger.info("2.向客戶端發(fā)歡迎消息");
                clientSocket.getOutputStream().write("你好,請(qǐng)報(bào)上名來(lái)!".getBytes("UTF8"));
                clientSocket.getOutputStream().flush();

                //從客戶端讀取消息
                StringBuffer msgBuf = new StringBuffer();
                byte[] byteBuf = new byte[1024];
                clientSocket.getInputStream().read(byteBuf);
                msgBuf.append(new String(byteBuf, "UTF8"));
                logger.info("5.收到客戶端消息:{}", msgBuf);

                //向客戶端發(fā)消息
                logger.info("6.向客戶端發(fā)退出消息");
                clientSocket.getOutputStream().write(String.format("退下,%s!", msgBuf.toString()).getBytes(Charset.forName("UTF8")));
                clientSocket.getOutputStream().flush();
            }
        } catch (IOException e) {
            logger.error("server error", e);
            System.exit(1);
        }
    }
}

客戶端
package com.xetlab.javatest.question1;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.Socket;

public class ClientMain1 {

    private static final Logger logger = LoggerFactory.getLogger(ClientMain1.class);

    public static void main(String[] args) {
        try {
            Socket socket = new Socket("127.0.0.1", 9999);
            while (true) {
                StringBuffer msgBuf = new StringBuffer();
                byte[] byteBuf = new byte[1024];
                socket.getInputStream().read(byteBuf);
                msgBuf.append(new String(byteBuf, "UTF8"));
                logger.info("3.收到服務(wù)端消息:{}", msgBuf);

                logger.info("4.向服務(wù)端發(fā)送名字消息");
                socket.getOutputStream().write("Mr Nobody.".getBytes("UTF8"));
                socket.getOutputStream().flush();

                msgBuf = new StringBuffer();
                byteBuf = new byte[1024];
                socket.getInputStream().read(byteBuf);
                msgBuf.append(new String(byteBuf, "UTF8"));
                logger.info("7.收到服務(wù)端消息:{}", msgBuf);
                if (msgBuf.toString().startsWith("退下")) {
                    socket.close();
                    logger.info("8.客戶端退出");
                    break;
                }
            }
        } catch (IOException e) {
            logger.error("client error", e);
            System.exit(1);
        }
    }
}

對(duì)應(yīng)的輸出(已按順序組織)
2019-03-23 23:36:39,480 [INFO] com.xetlab.javatest.question1.ServerMain1 [main] - 0.主線程啟動(dòng)
2019-03-23 23:36:44,883 [INFO] com.xetlab.javatest.question1.ServerMain1 [main] - 1.客戶端 127.0.0.1:7473 已連接
2019-03-23 23:36:44,884 [INFO] com.xetlab.javatest.question1.ServerMain1 [main] - 2.向客戶端發(fā)歡迎消息
2019-03-23 23:36:44,888 [INFO] com.xetlab.javatest.question1.ClientMain1 [main] - 3.收到服務(wù)端消息:你好,請(qǐng)報(bào)上名來(lái)!  
2019-03-23 23:36:44,891 [INFO] com.xetlab.javatest.question1.ClientMain1 [main] - 4.向服務(wù)端發(fā)送名字消息
2019-03-23 23:36:44,891 [INFO] com.xetlab.javatest.question1.ServerMain1 [main] - 5.收到客戶端消息:Mr Nobody.                     
2019-03-23 23:36:44,892 [INFO] com.xetlab.javatest.question1.ServerMain1 [main] - 6.向客戶端發(fā)退出消息
2019-03-23 23:36:44,892 [INFO] com.xetlab.javatest.question1.ClientMain1 [main] - 7.收到服務(wù)端消息:退下,Mr Nobody.                
2019-03-23 23:36:44,892 [INFO] com.xetlab.javatest.question1.ClientMain1 [main] - 8.客戶端退出

如果我們按上面的方式實(shí)現(xiàn)Server端,答案會(huì)是:BIO Server端,一個(gè)線程就夠了。我們來(lái)分析下這種實(shí)現(xiàn)方式的優(yōu)缺點(diǎn)。

優(yōu)點(diǎn)
  1. 簡(jiǎn)單,適合java socket編程入門。
  2. 好像只有簡(jiǎn)單了。
缺點(diǎn)
  1. 一次只能服務(wù)一個(gè)客戶端,別的客戶端只能等待,具體表現(xiàn)是:如果同時(shí)啟動(dòng)兩個(gè)慢客戶端,那么兩個(gè)客戶端的底層TCP連接是建立好的,先啟動(dòng)的客戶端會(huì)先得到服務(wù),但后啟動(dòng)的那個(gè)客戶端會(huì)在讀取數(shù)據(jù)時(shí)一直被阻塞,如下所示(windows):

    netstat -ano|find "9999"

     TCP    127.0.0.1:9999         127.0.0.1:29712        ESTABLISHED     16996
     TCP    127.0.0.1:9999         127.0.0.1:29740        ESTABLISHED     16996
    

    服務(wù)端輸出

    2019-03-24 10:47:48,881 [INFO] com.xetlab.javatest.question1.ServerMain1 [main] - 0.主線程啟動(dòng)
    2019-03-24 10:47:52,549 [INFO] com.xetlab.javatest.question1.ServerMain1 [main] - 1.客戶端 127.0.0.1:29712 已連接
    2019-03-24 10:47:52,550 [INFO] com.xetlab.javatest.question1.ServerMain1 [main] - 2.向客戶端發(fā)歡迎消息
    

    客戶端1收到消息后,休眠

    2019-03-24 10:47:52,555 [INFO] com.xetlab.javatest.question1.ClientMain1 [main] - 3.收到服務(wù)端消息:你好,請(qǐng)報(bào)上名來(lái)!
    

    客戶端2

    //客戶端2在此處被阻塞
    socket.getInputStream().read(byteBuf);
    
  1. 實(shí)現(xiàn)不了同時(shí)服務(wù)100個(gè)客戶端。

因此這種方式實(shí)現(xiàn)的Server端,只能用于入門示例,不能用于生產(chǎn)環(huán)境。另外BIO全稱是Blocking IO,即阻塞式IO,這個(gè)BIO體現(xiàn)在哪呢?體現(xiàn)在這兩處:

//1.當(dāng)客戶端沒(méi)發(fā)消息過(guò)來(lái)時(shí),此時(shí)服務(wù)端讀取消息時(shí)就會(huì)阻塞
//2.當(dāng)讀取的數(shù)據(jù)較多時(shí),線程沒(méi)有阻塞,但是讀取數(shù)據(jù)的耗時(shí)會(huì)挺久
clientSocket.getInputStream().read(bytes);
//當(dāng)給客戶端發(fā)送的數(shù)據(jù)較多時(shí),這里線程沒(méi)有阻塞,但是寫數(shù)據(jù)的耗時(shí)會(huì)挺久
clientSocket.getOutputStream().write(bytes);
Tips
  1. BIO其實(shí)包含兩層含義:讀取時(shí)數(shù)據(jù)未準(zhǔn)備好,當(dāng)前線程會(huì)阻塞;數(shù)據(jù)的讀寫是耗時(shí)的操作。
  2. server和client之間的通信通過(guò)socket的InputStream和OutputStream進(jìn)行。
  3. server和client之間的通信需要預(yù)先定義好通信協(xié)議(如示例中就隱含了一個(gè)規(guī)定,大家每次發(fā)送的消息不超過(guò)1024個(gè)字節(jié),讀取時(shí)也是讀取最多1024個(gè)字節(jié),如果違反了這個(gè)規(guī)定,要嗎數(shù)據(jù)亂了,要嗎server或client在讀取數(shù)據(jù)時(shí)被阻塞)。
  4. 寫數(shù)據(jù)時(shí)要記得flush一下,不然數(shù)據(jù)只是寫到緩存里,并沒(méi)有發(fā)送出去。

引入多線程

服務(wù)端
package com.xetlab.javatest.question1;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.Charset;

public class ServerMain2 {

    private static final Logger logger = LoggerFactory.getLogger(ServerMain2.class);

    public static void main(String[] args) {
        logger.info("0.主線程啟動(dòng)");
        try {
            //服務(wù)端初始化,在9999端口監(jiān)聽
            ServerSocket serverSocket = new ServerSocket(9999);
            while (true) {
                //等待客戶端連接,如果沒(méi)有連接就阻塞當(dāng)前線程
                Socket clientSocket = serverSocket.accept();
                String clientId = String.format("%s:%s", clientSocket.getInetAddress().getHostAddress(), clientSocket.getPort());
                logger.info("1.客戶端 {} 已連接", clientId);

                new Thread(new Handler(clientSocket), clientId).start();
            }
        } catch (IOException e) {
            logger.error("server error", e);
            System.exit(1);
        }
    }

    static class Handler implements Runnable {
        private Socket clientSocket;

        public Handler(Socket clientSocket) {
            this.clientSocket = clientSocket;
        }

        public void run() {
            try {
                //向客戶端發(fā)消息
                logger.info("2.向客戶端發(fā)歡迎消息");
                clientSocket.getOutputStream().write("你好,請(qǐng)報(bào)上名來(lái)!".getBytes("UTF8"));
                clientSocket.getOutputStream().flush();

                //從客戶端讀取消息
                StringBuffer msgBuf = new StringBuffer();
                byte[] byteBuf = new byte[1024];
                clientSocket.getInputStream().read(byteBuf);
                msgBuf.append(new String(byteBuf, "UTF8"));
                logger.info("5.收到客戶端消息:{}", msgBuf);

                //向客戶端發(fā)消息
                logger.info("6.向客戶端發(fā)退出消息");
                clientSocket.getOutputStream().write(String.format("退下,%s!", msgBuf.toString()).getBytes(Charset.forName("UTF8")));
                clientSocket.getOutputStream().flush();
            } catch (IOException e) {
                logger.error("io error", e);
            }
        }
    }
}

輸出

客戶端保持不變,只是把其中一個(gè)在回復(fù)名字前故意休眠很久,另一個(gè)保持正常。此時(shí)各端的輸出如下:

服務(wù)端

2019-03-24 12:50:56,514 [INFO] com.xetlab.javatest.question1.ServerMain2 [main] - 0.主線程啟動(dòng)
2019-03-24 12:51:02,613 [INFO] com.xetlab.javatest.question1.ServerMain2 [main] - 1.客戶端 127.0.0.1:44334 已連接
2019-03-24 12:51:02,613 [INFO] com.xetlab.javatest.question1.ServerMain2 [127.0.0.1:44334] - 2.向客戶端發(fā)歡迎消息
2019-03-24 12:51:08,331 [INFO] com.xetlab.javatest.question1.ServerMain2 [main] - 1.客戶端 127.0.0.1:44347 已連接
2019-03-24 12:51:08,331 [INFO] com.xetlab.javatest.question1.ServerMain2 [127.0.0.1:44347] - 2.向客戶端發(fā)歡迎消息
2019-03-24 12:51:08,339 [INFO] com.xetlab.javatest.question1.ServerMain2 [127.0.0.1:44347] - 5.收到客戶端消息:Mr Nobody.                                   
2019-03-24 12:51:08,339 [INFO] com.xetlab.javatest.question1.ServerMain2 [127.0.0.1:44347] - 6.向客戶端發(fā)退出消息

慢客戶端先連接,收到消息后,休眠

2019-03-24 12:51:02,619 [INFO] com.xetlab.javatest.question1.ClientMain1 [main] - 3.收到服務(wù)端消息:你好,請(qǐng)報(bào)上名來(lái)! 

正常客戶端后連接

2019-03-24 12:51:08,336 [INFO] com.xetlab.javatest.question1.ClientMain2 [main] - 3.收到服務(wù)端消息:你好,請(qǐng)報(bào)上名來(lái)!  
2019-03-24 12:51:08,338 [INFO] com.xetlab.javatest.question1.ClientMain2 [main] - 4.向服務(wù)端發(fā)送名字消息
2019-03-24 12:51:08,339 [INFO] com.xetlab.javatest.question1.ClientMain2 [main] - 7.收到服務(wù)端消息:退下,Mr Nobody.                   
2019-03-24 12:51:08,340 [INFO] com.xetlab.javatest.question1.ClientMain2 [main] - 8.客戶端退出

可以看到,引入多線程后,每個(gè)線程服務(wù)一個(gè)客戶端,可以同時(shí)服務(wù)100個(gè)連接了,如果這樣實(shí)現(xiàn)Server端,IO還是BIO,線程數(shù)需要101個(gè),一個(gè)線程用于接受客戶端連接,100個(gè)線程用于服務(wù)客戶端。同樣來(lái)分析下優(yōu)缺點(diǎn)。

優(yōu)點(diǎn)
  1. 簡(jiǎn)單,和最簡(jiǎn)單版本相比,只是把和客戶端IO相關(guān)的處理放到了線程里處理。
  2. 可以同時(shí)服務(wù)N個(gè)連接。
缺點(diǎn)
  1. 每個(gè)線程都要占用內(nèi)存,當(dāng)客戶端保持長(zhǎng)連接,數(shù)量越來(lái)越多達(dá)到一定值時(shí),就會(huì)出現(xiàn)錯(cuò)誤:OutOfMemoryError:unable to create new native thread。
  2. 一個(gè)客戶端分配一個(gè)線程,太浪費(fèi)資源了,因?yàn)锽IO的緣故,線程大部分時(shí)間都處于阻塞或等待讀寫狀態(tài)。
  3. 即使機(jī)器性能高,內(nèi)存大,當(dāng)線程很多時(shí),線程上下文切換也會(huì)帶來(lái)很大的開銷。
Tips

編寫多線程任務(wù)時(shí),可以把執(zhí)行任務(wù)的邏輯使用Runnable接口來(lái)實(shí)現(xiàn),這樣任務(wù)可以直接放到Thread線程對(duì)象里執(zhí)行,也可以提交到線程池中去執(zhí)行。

NIO上場(chǎng)

有沒(méi)有可能同時(shí)具備方式一和二的優(yōu)點(diǎn)呢,具體來(lái)說(shuō)就是,一個(gè)線程同時(shí)服務(wù)N個(gè)客戶端?Yes,NIO就可以!那什么是NIO?NIO即New IO,更多時(shí)候我們是看成Non blocking IO,就是非阻塞IO。

具體NIO如何實(shí)現(xiàn)一個(gè)線程服務(wù)N個(gè)客戶端,在深入代碼細(xì)節(jié)前,我們先理一理。

回顧上面的BIO實(shí)現(xiàn),我們知道有這幾個(gè)點(diǎn)會(huì)阻塞或者響應(yīng)慢:

  1. serverSocket.accept(),這里是服務(wù)端等待客戶端連接。
  2. clientSocket.getInputStream().read(),這里是等待客戶端傳送數(shù)據(jù)過(guò)來(lái)。
  3. clientSocket.getOutputStream().write(),這里是往客戶端寫數(shù)據(jù)。

由于會(huì)阻塞或者響應(yīng)慢BIO用了不同的線程去分別處理,如果可以只由一個(gè)線程去負(fù)責(zé)檢查是否有客戶端連接,客戶端的數(shù)據(jù)是否可讀,是否可以往客戶端寫數(shù)據(jù),當(dāng)有對(duì)應(yīng)的事件已經(jīng)準(zhǔn)備好時(shí),再由于當(dāng)前線程去處理相應(yīng)的任務(wù),那就完美了。

NIO里有個(gè)對(duì)象是Selector,這個(gè)Selector就是用于注冊(cè)事件,并檢查事件是否已準(zhǔn)備好?,F(xiàn)在來(lái)看下具體代碼。

package com.xetlab.javatest.question1;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ConcurrentHashMap;

public class ServerMain3 {

    private static final Logger logger = LoggerFactory.getLogger(ServerMain3.class);

    public static void main(String[] args) {
        logger.info("0.主線程啟動(dòng)");
        try {

            Map<SocketChannel, Queue> msgQueueMap = new ConcurrentHashMap<SocketChannel, Queue>();

            //創(chuàng)建channel管理器,用于注冊(cè)channel的事件
            Selector selector = Selector.open();

            //服務(wù)端初始化,在9999端口監(jiān)聽,保留BIO初始化方式用于參照
            //ServerSocket serverSocket = new ServerSocket(9999);
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            //設(shè)置非阻塞
            serverSocketChannel.configureBlocking(false);
            serverSocketChannel.socket().bind(new InetSocketAddress(9999));

            //注冊(cè)可accept事件
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

            while (true) {
                //NIO僅有的一個(gè)阻塞方法,當(dāng)有注冊(cè)的事件產(chǎn)生時(shí),才會(huì)返回
                selector.select();
                //產(chǎn)生事件的事件源列表
                Set<SelectionKey> readyKeys = selector.selectedKeys();
                Iterator<SelectionKey> keyItr = readyKeys.iterator();
                while (keyItr.hasNext()) {
                    SelectionKey readyKey = keyItr.next();
                    keyItr.remove();
                    if (readyKey.isAcceptable()) {
                        ServerSocketChannel serverChannel = (ServerSocketChannel) readyKey.channel();
                        //接受客戶端
                        SocketChannel clientChannel = serverChannel.accept();
                        String clientId = String.format("%s:%s", clientChannel.socket().getInetAddress().getHostAddress(), clientChannel.socket().getPort());
                        logger.info("1.客戶端 {} 已連接", clientId);

                        msgQueueMap.put(clientChannel, new ArrayBlockingQueue(100));
                        logger.info("2.向客戶端發(fā)歡迎消息");
                        //NIO發(fā)消息先放到消息隊(duì)列里,等可寫時(shí)再發(fā)
                        msgQueueMap.get(clientChannel).add("你好,請(qǐng)報(bào)上名來(lái)!");
                        
                        //設(shè)置非阻塞
                        clientChannel.configureBlocking(false);
                        //注冊(cè)可讀和可寫事件
                        clientChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
                    } else if (readyKey.isReadable()) {
                        SocketChannel clientChannel = (SocketChannel) readyKey.channel();
                        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                        int bytesRead = clientChannel.read(byteBuffer);
                        if (bytesRead <= 0) {
                            continue;
                        }
                        byteBuffer.flip();
                        byte[] msgByte = new byte[bytesRead];
                        byteBuffer.get(msgByte);
                        final String clientName = new String(msgByte, "UTF8");
                        logger.info("5.收到客戶端消息:{}", clientName);
                        msgQueueMap.get(clientChannel).add(String.format("退下!%s", clientName));
                    } else if (readyKey.isWritable()) {
                        SocketChannel clientChannel = (SocketChannel) readyKey.channel();
                        Queue<String> msgQueue = msgQueueMap.get(clientChannel);
                        String msg = msgQueue.poll();
                        if (msg != null) {
                            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                            byteBuffer.put(msg.getBytes("UTF8"));
                            byteBuffer.flip();
                            clientChannel.write(byteBuffer);
                            logger.info("6.向客戶端發(fā)退出消息");
                        }
                    }
                }

            }
        } catch (IOException e) {
            logger.error("server error", e);
            System.exit(1);
        }
    }

}

上面我們用NIO實(shí)現(xiàn)了和原來(lái)BIO一模一樣的邏輯,NIO確實(shí)是只用一個(gè)線程高效的解決了問(wèn)題,但是代碼看起來(lái)復(fù)雜多了。不過(guò)我們用偽代碼總結(jié)一下,會(huì)簡(jiǎn)單一點(diǎn):

  1. 準(zhǔn)備好Selector(源代碼注釋中叫channel多路復(fù)用器)。
  2. 準(zhǔn)備好ServerSocketChannel(對(duì)應(yīng)BIO里的ServerSocket)。
  3. ServerSocketChannel向Selector注冊(cè)accept事件(即客戶端連接就緒事件)
  4. 循環(huán)
    • 檢查Selector是否有新的就緒事件,如果沒(méi)有就阻塞等待,如果有就返回產(chǎn)生的就緒事件列表。
    • 如果是accept事件(客戶端連接就緒事件),就接受客戶端連接得到SocketChannel(對(duì)應(yīng)BIO中的Socket),SocketChannel向Selector注冊(cè)讀寫就緒事件。
    • 如果是讀就緒事件,那么讀取對(duì)應(yīng)SocketChannel的數(shù)據(jù),并進(jìn)行相應(yīng)的處理。
    • 如果是寫就緒事件,那么就把數(shù)據(jù)寫到對(duì)應(yīng)的SocketChannel。
Tips

NIO中,由于是單線程,不能在連接就緒,讀寫就緒之后的事件處理邏輯執(zhí)行耗時(shí)操作,那樣將會(huì)讓服務(wù)性能急劇下降,正確方法應(yīng)該是把耗時(shí)的邏輯放在獨(dú)立的線程中去執(zhí)行,或放到專門的worker線程池中執(zhí)行。

源代碼

https://github.com/huangyemin/javatest
https://gitee.com/huangyemin/javatest
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 在一個(gè)方法內(nèi)部定義的變量都存儲(chǔ)在棧中,當(dāng)這個(gè)函數(shù)運(yùn)行結(jié)束后,其對(duì)應(yīng)的棧就會(huì)被回收,此時(shí),在其方法體中定義的變量將不...
    Y了個(gè)J閱讀 4,576評(píng)論 1 14
  • NIO(Non-blocking I/O,在Java領(lǐng)域,也稱為New I/O),是一種同步非阻塞的I/O模型,也...
    Demon_code閱讀 473評(píng)論 0 0
  • NIO(Non-blocking I/O,在Java領(lǐng)域,也稱為New I/O),是一種同步非阻塞的I/O模型,也...
    閃電是只貓閱讀 3,281評(píng)論 0 7
  • 1. 前言 有一些概念總是Java I/O一塊出現(xiàn),比如同步與異步,阻塞與非阻塞,這些概念往往也是非常難以區(qū)分。在...
    WekingZhang閱讀 597評(píng)論 0 2
  • 前言:在之前的面試中,每每問(wèn)到關(guān)于Java I/O 方面的東西都感覺(jué)自己吃了大虧..所以這里搶救一下..來(lái)深入的了...
    我沒(méi)有三顆心臟閱讀 2,602評(píng)論 0 21

友情鏈接更多精彩內(nèi)容