java nio学习

前言:

长文警告!
自己在学习的时候,对同步、异步、阻塞、非阻塞等等概念不太清晰。在网上看了很多,发现各有各的解释。所以我也来个自己的理解
看了许多文章吧,把引用拿上来
阻塞和非阻塞
java之NIO简介
Java NIO 概述
这是一份很全很全的IO基础知识与概念
这次,让我们捋清:同步、异步、阻塞、非阻塞

一、概念

1.同步和异步

同步和异步关注的是消息通信机制。
同步:就是在发出一个调用时,在没有得到结果之前,就一直等待结果返回。
异步:调用在发出之后,这个调用就直接返回了,不会等待执行结果,所以没有返回结果。

2.阻塞和非阻塞

关注的是程序在等待调用结果(消息,返回值)时的状态。
阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。
至此我们可以得到一个结论:
同步&异步指:当数据还未处理完成时,代码的逻辑处理方式不同。
阻塞&非阻塞指:当数据还未处理完成时(未就绪),线程的状态。

3.磁盘IO和网络IO

而在IO中分为磁盘IO网络IO,如果要了解这些,还需要明白用户空间内核空间。(越写越多,我也不想…)
IO:在计算机操作中,IO就是输入(Input)和输出(Output),也可以理解为对读(Read)、写(Write)操作。根据不同的方式,分为磁盘IO网络IO
内存空间分为用户空间和内核空间。用户空间用于运行我们自己写的代码程序。内核空间用于运行操作系统。这样做是为了保证操作系统安全,比如,在linux服务器中安装了mysql,不会因为mysql挂了,导致操作系统而不能正常运行。

内核空间的作用:
用户空间不能直接对磁盘和网络中的数据进行读写,只有通过调用内核空间的读写完成操作。

4.读操作和写操作

读操作:用户空间读取数据,会先到内核空间查看是否存在,如果存在,则copy到用户空间,供应用程序使用。如果不存在,则内核空间通过磁盘IO读取内核空间,然后再次copy到用户空间。而对于网络IO,应用程序需要等待客户端发送数据,如果客户端还没有发送数据,对应的应用程序将会被阻塞,直到客户端发送了数据,该应用程序才会被唤醒,从Socket协议找中读取客户端发送的数据到内核空间,然后把内核空间的数据copy到用户空间,供应用程序使用。

写操作:用户的应用程序将数据从用户空间copy到内核空间的缓冲区中(如果用户空间没有相应的数据,则需要从磁盘—>内核缓冲区—>用户缓冲区依次读取),这时对用户程序来说写操作就已经完成,至于什么时候再写到磁盘或通过网络发送出去,由操作系统决定。除非应用程序显示地调用了sync 命令,立即把数据写入磁盘,或执行flush()方法,通过网络把数据发送出去。

通过以下代码分析用户和内核空间切换的过程(java)

//内核空间
FileOutputStream fileOutputStream = new FileOutputStream("C:\\Users\\DELL\\Desktop\\新建文本文档(1).txt");
//用户空间
StringBuffer str =  new StringBuffer("I");
str.append(" love").append(" you");
//内核空间
fileOutputStream.write(str.toString().getBytes());
fileOutputStream.close();
//用户空间
str.append(" every day");

只有内核空间才能操作计算机硬件,所以在寻找操作文件的时候,会切换到内核空间。而在创建字符串对象,拼接字符串的时候,是在用户空间开辟一块内存进行操作的。需要写入文件时,就要切换到内核空间,用户空间不能直接写入。最后再次切换到用户空间对字符串操作。

5.阻塞IO和非阻塞IO

首先要明白IO操作分为两个步骤:
1.发起IO请求
2.进行实际的IO读写,读:从内核空间拷贝缓存到用户空间;写:从用户空间拷贝到内核空间。
所以阻塞IO和非阻塞IO指的就是用户线程是否被阻塞,就是发起IO请求时是否会被阻塞。
阻塞IO:指用户线程发起IO请求的时候,数据还未准备就绪,就会阻塞当前线程,让出CPU。
非阻塞IO:指用户发起请求的时候,数据还未准备就绪,也不会阻塞当前线程,可以继续执行后续的任务。

此时,有个问题,假设非阻塞IO后没有任何任务了,是否就和同步一样了。这样的话,因为等待数据的准备,如果不阻塞的话,会一直占用CPU的执行。阻塞了让出时间片,让其他任务去执行,是否会更好。如果上面猜想是正确的话,那是不是说明非阻塞不一定比阻塞更好,只不过是应用场景不同。

6.BIO、NIO、AIO

1.BIO 编程:Blocking IO: 同步阻塞的编程方式。

BIO编程方式通常是在JDK1.4版本之前常用的编程方式。编程实现过程为:首先在服务端启动一个ServerSocket来监听网络请求,客户端启动Socket发起网络请求,默认情况下ServerSocket回建立一个线程来处理此请求,如果服务端没有线程可用,客户端则会阻塞等待或遭到拒绝。且建立好的连接,在通讯过程中,是同步的。在并发处理效率上比较低。

同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。

2.NIO 编程:Unblocking IO(New IO): 同步非阻塞的编程方式。

NIO本身是基于事件驱动思想来完成的,其主要想解决的是BIO的大并发问题,NIO基于Reactor,当socket有流可读或可写入socket时,操作系统会相应的通知引用程序进行处理,应用再将流读取到缓冲区或写入操作系统。也就是说,这个时候,已经不是一个连接就要对应一个处理线程了,而是有效的请求,对应一个线程,当连接没有数据时,是没有工作线程来处理的。

NIO的最重要的地方是当一个连接创建后,不需要对应一个线程,这个连接会被注册到多路复用器上面,所以所有的连接只需要一个线程就可以搞定,当这个线程中的多路复用器进行轮询的时候,发现连接上有请求的话,才开启一个线程进行处理,也就是一个请求一个线程模式。

在NIO的处理方式中,当一个请求来的话,开启线程进行处理,可能会等待后端应用的资源(JDBC连接等),其实这个线程就被阻塞了,当并发上来的话,还是会有BIO一样的问题

3.AIO编程:Asynchronous IO: 异步非阻塞的编程方式。

与NIO不同,当进行读写操作时,只须直接调用API的read或write方法即可。这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。即可以理解为,read/write方法都是异步的,完成后会主动调用回调函数。在JDK1.7中,这部分内容被称作NIO.2,主要在java.nio.channels包下增加了下面四个异步通道:AsynchronousSocketChannel、AsynchronousServerSocketChannel、AsynchronousFileChannel、AsynchronousDatagramChannel

二、java nio介绍

NIO (New lO)也有人称之为java non-blocking lO是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java lO API。NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的、基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作。NIO可以理解为非阻塞IO,传统的IO的read和write只能阻塞执行,线程在读写IO期间不能干其他事情,比如调用socket.read()时,如果服务器一直没有数据传输过来,线程就一直阻塞,而NIO中可以配置socket为非阻塞模式。

  • NIO相关类都被放在java.nio包及子包下,并且对原java.io包中的很多类进行改写。
  • NIO有三大核心部分:Channel(通道),Buffer(缓冲区), Selector(选择器)
  • Java NlO的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。

模型如下图所示:
在这里插入图片描述

确实核心就是Selector,通过一个线程来管理多个连接请求,基于事件去判断是否为读写操作,如果不是,则不处理。

三、Buffer(缓冲区)、Channel(通道)、Selector(选择器)介绍

1.Buffer(缓冲区)

缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。相比较直接对数组的操作,Buffer APl更加容易操作和管理。

Buffer类底层是通过数组实现的,所以初始化后大小不可以改变。
Buffer的子类有:ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer
也就是对除了boolean以外的所有基本类型都做了处理。
本文中主要是以ByteBuffer来介绍

Buffer

创建1个ByteBuffer,allocate方法中的参数为数组长度,所以入参必须大于0,否则会拋异常。

ByteBuffer buffer = ByteBuffer.allocate(1024);

重要属性(斜体字为api中解释)
不变量
标记,位置,极限和容量值的以下不变量保持不变:
0 <= mark <= position <= limit <= capacity
新创建的缓冲区始终具有零位置和未定义的标记。 初始限制可以为零,或者可以是取决于缓冲器的类型和构造方式的某些其他值。 新分配的缓冲区的每个元素被初始化为零。

capacity(容量):Buffer作为一个缓存区,必须有容量大小的指定,不能为负数,创建后不可更改。
limit(限制):个人理解为表示缓存区可操作大小,默认等于容量大小。写模式下,为capacity大小。读模式下,为有效数据大小,也可以理解为position大小。
position(位置):下一个要被操作数据的位置,当写入(put)和读取(get)数据后,position发生变化。默认为0。
mark(标记):可以通过mark()方法标记一个position位置,然后通过reset()方法把标记mark设置为position。

  • 常用方法

put() :将给定数据写入当前位置(position为下标)的缓冲区,然后增加位置。
flip():翻转这个缓冲区。 该限制设置为当前位置,然后将该位置设置为零。 如果标记被定义,则它被丢弃。在通道读取或放置操作的序列之后,调用此方法来准备一系列通道写入或相对获取操作。
get():相对(position为下标)获取方法。 读取该缓冲区当前位置的字节,然后增加位置。
clear():清除此缓冲区。 位置设置为零,限制设置为容量,标记被丢弃。在使用一系列通道读取或放置操作填充此缓冲区之前调用此方法。
hasRemaining():告诉当前位置和限制(limit)之间是否存在任何元素。

  • 代码
//把字符串添加到Buffer中,然后再获取输出
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("museum".getBytes());
buffer.flip();
byte[] bytes = new byte[buffer.limit()];
buffer.get(bytes);
System.out.println(new String(bytes));

执行结果

ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("museum".getBytes());
//clear()只是把position设置为0,limit设置为capacity,mark取消标记设置为-1,并没有清除数据
buffer.clear();
System.out.println(new String(new byte[]{buffer.get()}));
buffer.put("film".getBytes());
//flip()只是把limit设置为position,position设置为0,mark取消标记设置为-1,并没有清除数据
buffer.flip();
byte[] bytes = new byte[buffer.limit()];
buffer.get(bytes);
System.out.println(new String(bytes));

执行结果

clear()方法并不会把数组中的数据置为0。所以在clear()方法之后,不可以读(get),只能写(put),因为get()方法会把clear()之前的数据获取到。
flip()方法之后,会把position赋值给limit,然后position设置为0,读取数据就是从0读到上次put到position的位置。所以flip()方法之后不可以写,会把上次写入数据覆盖。
rewind()方法可以重复读。在使用flip()方法读完一次之后position发生变化,rewind()会把position方法重新设置为0。

ByteBuffer buffer = ByteBuffer.allocate(1024);
System.out.println(buffer.isDirect());
buffer =  ByteBuffer.allocateDirect(1024);
System.out.println(buffer.isDirect());

执行结果

ByteBuffer.allocate()方法创建的对象在堆内存。
ByteBuffer.allocateDirect()方法创建的对象为直接内存,也就是堆外内存。
isDirect()方法判断如果是直接内存则为true,否则false;

又开始烧脑了 ~( ̄▽ ̄~)(~ ̄▽ ̄)~
有没有同学和我一样,看到直接内存(堆外内存),有点熟悉又有点懵!好像听说过,又好像没用过。
hotspot虚拟机在jdk1.8的时候将方法区名字定义为元空间(Meta Sapce),并且将其从堆内存中移动到了jvm之外的本地内存,也就是操作系统的内存之中。
既然可以创建堆内可以创建对象,为什么又要使用直接内存呢?
如果程序需要读取硬盘或者远程数据,会经过3次复制的过程。首先从加载到内核空间,然后加拷贝到堆外内存(堆外内存同样在java线程中),再拷贝到堆中。
为什么不直接拷贝到堆中呢?
因为堆内存是手GC管理的,而在拷贝需要提供对应的目的地址,而拷贝中可能会出现堆内存中有垃圾被清理回收而导致的内存地址发生变化,可能造成目的地址也发生变化,所以需要先拷贝到堆外,再拷贝至堆内。
而用NIO开辟直接内存的方式,则可以在堆外内存开辟位置,少了堆外到堆内的拷贝过程。同时可以利用mmap内存映射方式将堆外的部分内存和内核空间的部分内核进行映射(可以看成使用了同一个物理内存地址),这样又可以少了内核缓冲区到堆外内存拷贝的过程,这就是NIO目前用的比较多的直接内存零拷贝方式。

2.Channel(通道)

概述:通道表示与诸如硬件设备,文件,网络套接字或能够执行一个或多个不同I / O操作(例如读取或写入)的程序组件的实体的开放连接。
通道是打开的还是关闭的。 一个通道在创建时打开,一旦关闭,它仍然关闭。 一旦通道关闭,任何尝试调用I / O操作将导致抛出ClosedChannelException 。 通道是否打开可以通过调用其isOpen方法进行测试。
一般来说,通道旨在为多线程访问安全,如扩展和实现此接口的接口和类的规范中所述。

通道和流的区别
通道是双向的,可以写入缓存,也可以从缓存中读取数据。流是单向的,只能读或者写。

常用的Channel实现类
FileChannel:用于读取、写入、映射和操作文件的通道。
DatagramChannel:通过 UDP 读写网络中的数据通道。
SocketChannel:通过 TCP 读写网络中的数据。
ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的> 连接都会创建一个 SocketChannel。 【ServerSocketChanne 类似 ServerSocket , SocketChannel 类似 Socket】

FileChannel 类
获取通道的一种方式是对支持通道的对象调用getChannel() 方法。支持通道的类如下
FileInputStream
FileOutputStream
RandomAccessFile
DatagramSocket
Socket
ServerSocket
获取通道的其他方式是使用 Files 类的静态方法 newByteChannel() 获取字节通道。或者通过通道的静态方法 open() 打开并返回指定通道

FileChannel常用方法
int read(ByteBuffer dst) :从Channel 到 中读取数据到 ByteBuffer
long read(ByteBuffer[] dsts) : 将Channel中的数据“分散”到 ByteBuffer[]
int write(ByteBuffer src) :将 ByteBuffer中的数据写入到 Channel
long write(ByteBuffer[] srcs) :将 ByteBuffer[] 到 中的数据“聚集”到 Channel
long position() :返回此通道的文件位置
FileChannel position(long p) :设置此通道的文件位置
long size() :返回此通道的文件的当前大小
FileChannel truncate(long s) :将此通道的文件截取为给定大小
void force(boolean metaData) :强制将所有对此通道的文件更新写入到存储设备中

  • 代码
    将数据写入文件
public static void write() {
    try (FileChannel channel = new FileOutputStream("C:\\Users\\DELL\\Desktop\\新建文本文档(1).txt").getChannel()){
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        for (int i = 0; i < 5; i++) {
            buffer.clear();
            buffer.put(("第" + i + "次写入数据..." + "\r\n").getBytes());
            buffer.flip();
            channel.write(buffer);
        }
    }catch (IOException ioException){
        ioException.printStackTrace();
    }
}

读取文件内容

public static void read() throws IOException {
    ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
    FileInputStream fileInputStream = new FileInputStream("C:\\Users\\DELL\\Desktop\\新建文本文档(1).txt");
    FileChannel channel = fileInputStream.getChannel();
    //读取
    while (channel.read(buffer) != -1) {
    	//反转
        buffer.flip();
        //判断是否存在数据,存在则读取
        while (buffer.hasRemaining()) {
            byte b = buffer.get();
            //读取倒1行以后,把所有数据输出清除缓存
            if (b == 10 || b == 13) {
                byteBuffer.flip();
                String line = StandardCharsets.UTF_8.decode(byteBuffer).toString();
                System.out.println(line);
                byteBuffer.clear();
            } else {
                if (byteBuffer.hasRemaining()) {
                    byteBuffer.put(b);
                } else {
                    byteBuffer = reAllocate(byteBuffer);
                    byteBuffer.put(b);
                }
            }
        }
        buffer.clear();
    }
    channel.close();
}
//扩容
public static ByteBuffer reAllocate(ByteBuffer buffer) {
    int capacity = buffer.capacity();
    ByteBuffer byteBuffer = ByteBuffer.allocate(capacity * 2);
    buffer.flip();
    byteBuffer.put(buffer);
    return byteBuffer;
}

更多案例

3.Selector(选择器)

选择器(Selector)概述
选择器(Selector)是SelectableChannle对象的多路复用器,Selector可以同时监控多个SelectableChannel的IO状况,也就是说,利用Selector可使一个单独的线程管理多个Channel。Selector是非阻塞IO的核心。

  • Java 的 NIO,用非阻塞的 IO 方式。可以用一个线程,处理多个的客户端连接,就会使用到 Selector(选择器)
  • Selector 能够检测多个注册的通道上是否有事件发生(注意:多个 Channel 以事件的方式可以注册到同一个(Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管
    理多个通道,也就是管理多个连接和请求。
  • 只有在连接/通道真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程
  • 避免了多线程之间的上下文切换导致的开销
//1. 获取通道
ServerSocketChannel ssChannel = ServerSocketChannel.open();
//2. 切换非阻塞模式
ssChannel.configureBlocking(false);
//3. 绑定连接
ssChannel.bind(new InetSocketAddress(9898));
//4. 获取选择器
Selector selector = Selector.open();
//5. 将通道注册到选择器上, 并且指定“监听接收事件”
ssChannel.register(selector, SelectionKey.OP_ACCEPT);

当调用 register(Selector sel, int ops) 将通道注册选择器时,选择器对通道的监听事件,需要通过第二个参数 ops 指定。可以监听的事件类型(用 可使用 SelectionKey 的四个常量 表示):
读 : SelectionKey.OP_READ (1)
写 : SelectionKey.OP_WRITE (4)
连接 : SelectionKey.OP_CONNECT (8)
接收 : SelectionKey.OP_ACCEPT (16)
若注册时不止监听一个事件,则可以使用“位或”操作符连接。

四、总结

没有总结!!!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值