开发必学,io和nio

作为一个程序开发人员,不可避免的要与io打交道,通常我们也都会在简历栏目上写上熟悉or了解io,那么你是否真的了解io与nio的区别呢?【划重点:面试官常问点】

首先,在详细描述io与nio的区别之前我们要先意识到

所有的系统I/O都分为两个阶段:等待就绪和操作。
并且等待就绪的阻塞是不使用CPU的,是在“空等”;

而真正的读写操作的阻塞是使用CPU的,真正在"干活",也意味着会消耗cpu资源。

为了让大家有个更加直观的感受,这里先抛出基于io和基于nio实现的例子。

io实现

package test;

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

public class IOServer {
    public static void main(String[] args) throws Exception {

        init();
    }

    private static void init() throws IOException {
        ServerSocket serverSocket = new ServerSocket(8000);
        new Thread(() -> {
            while (true) {
                // 阻塞点,获取新的连接
                Socket socket = null;
                try {
                    socket = serverSocket.accept();
                } catch (IOException e) {
                    e.printStackTrace();
                }

                // 创建线程点,给每一个新的连接都创建一个线程
                Socket finalSocket = socket;
                new Thread(() -> {
                    int len;
                    byte[] data = new byte[1024];
                    InputStream inputStream = null;
                    try {
                        inputStream = finalSocket.getInputStream();
                        // 面向流,按字节流方式读取数据
                        while ((len = inputStream.read(data)) != -1) {
                            System.out.println(new String(data, 0, len));
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    }

                }).start();
            }
        }).start();
    }
}

可以从上面的例子看出Server 端首先创建了一个serverSocket来监听 8000 端口,然后创建一个线程,线程里面死循环不断调用阻塞方法 serversocket.accept();获取新的客户端连接,当获取到新的连接之后,给每条连接创建一个新的线程,这个线程负责从该连接中读取数据然后读取数据是以字节流的方式。

这里的弊端是极其明显的,如

  • 线程资源受限:上面每个客户端都会构建一个线程去做读取操作,而线程是操作系统中非常宝贵的资源,同一时刻有大量的线程处于阻塞状态是非常严重的资源浪费,操作系统耗不起。

  • 线程切换效率低下:线程爆炸之后带来的副作用便是操作系统频繁进行线程切换,应用性能急剧下降。

nio实现

package test;

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.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;

public class NIOServer {
    public static void main(String[] args) throws IOException {
        init();
    }

    private static void init() throws IOException {
        
        // 构建Selector
        Selector serverSelector = Selector.open();
        Selector clientSelector = Selector.open();
        
        // 开启一个线程处理客户端的连接
        new Thread(() -> {
            try {
                // 此处对应IO编程中服务端启动
                ServerSocketChannel listenerChannel = ServerSocketChannel.open();
                listenerChannel.socket().bind(new InetSocketAddress(8000));
                listenerChannel.configureBlocking(false);
                listenerChannel.register(serverSelector, SelectionKey.OP_ACCEPT);

                while (true) {
                    // 使用Selector来监测是否有新的连接,这里的1指的是阻塞的时间为 1ms
                    if (serverSelector.select(1) > 0) {
                        Set<SelectionKey> set = serverSelector.selectedKeys();
                        Iterator<SelectionKey> keyIterator = set.iterator();
                        
                        while (keyIterator.hasNext()) {
                            SelectionKey key = keyIterator.next();

                            if (key.isAcceptable()) {
                                try {
                                    // 同io不同的地方,每来一个新连接,没有再创建一个线程,而是直接注册到clientSelector
                                    SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
                                    clientChannel.configureBlocking(false);
                                    clientChannel.register(clientSelector, SelectionKey.OP_READ);
                                } finally {
                                    keyIterator.remove();
                                }
                            }

                        }
                    }
                }
            } catch (IOException ignored) {
            }

        }).start();

        // 开启一个线程处理多个客户端线程的数据读取
        new Thread(() -> {
            try {
                while (true) {
                    // 轮询是否有哪些连接有数据可读,这里的1指的是阻塞的时间为 1ms
                    if (clientSelector.select(1) > 0) {
                        Set<SelectionKey> set = clientSelector.selectedKeys();
                        Iterator<SelectionKey> keyIterator = set.iterator();

                        while (keyIterator.hasNext()) {
                            SelectionKey key = keyIterator.next();

                            if (key.isReadable()) {
                                try {
                                    SocketChannel clientChannel = (SocketChannel) key.channel();
                                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                                    // 同io不同的地方,面向 Buffer
                                    clientChannel.read(byteBuffer);
                                    byteBuffer.flip();
                                    System.out.println(Charset.defaultCharset().newDecoder().decode(byteBuffer)
                                            .toString());
                                } finally {
                                    keyIterator.remove();
                                    key.interestOps(SelectionKey.OP_READ);
                                }
                            }

                        }
                    }
                }
            } catch (IOException ignored) {
            }
        }).start();
    }
}

可以通过上面代码看出,

NIO 实现给出了两个线程,每个线程都绑定一个轮询器 selector ,如serverSelector负责轮询是否有新的连接,而clientSelector负责轮询判断客户端的连接是否有数据可读,和io实现的区别点有:

  • 在服务端监测到新的连接之后,不会很sb的创建一个新的线程,而是直接将新连接绑定到clientSelector上,这样就不会消耗那么多线程资源了。

  • clientSelector则会进行判断如果在某一时刻有多条连接有数据可读,那么通过 clientSelector.select(1)方法可以轮询出来,进而处理客户端数据。

  • 数据的读写面向 Buffer。

因此我们可以得出结论:io是面向流的阻塞io,而nio是面向缓冲区的非阻塞io

所谓的阻塞io和非阻塞io:

  • 阻塞io意味着当线程调用read()或write()时,该线程将会被阻塞,直到有一些数据要读取,或者数据被完全写入。而在此期间,被阻塞的线程将无法执行任何其他操作。
  • 非阻塞io指的是允许线程请求从通道读取数据,如果通道没有数据可以读取的时候,线程可以继续使用其他内容,而不是在数据可供读取之前保持阻塞状态。

所谓的面向流和面向缓冲区:

  • 面向流的Java IO意味着可以从流中一次读取一个或多个字节,它们不会缓存在任何地方。此外,我们无法在流中的数据中前后移动。如果需要在从流中读取的数据中进行前后移动,则需要先将其缓存在缓冲区中。
  • 面向缓冲区的Java-NIO则是将数据读入缓冲区。我们可以根据需要在缓冲区中进行前后移动。这可以让我们在处理过程中更具灵活性。但是,我们还需要检查缓冲区是否包含完整处理所需的所有数据。当然了,还需要确保在将更多数据读入缓冲区时,不会出现bug,比如覆盖了尚未处理的缓冲区中的数据。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,544评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,430评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,764评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,193评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,216评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,182评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,063评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,917评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,329评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,543评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,722评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,425评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,019评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,671评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,825评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,729评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,614评论 2 353