07.NIO&BIO

第五章(NIO&BIO)

5.1NIO概述

BIO - JDK1.0 - 同步阻塞式IO - BlockingIO
在执行ACCEPT CONNECT READ WRITE 时都会产生阻塞
在平常开发当中并不是问题 甚至因为这样的模型直观而简单 应用的场景非常广泛
但是在高并发的场景下 这样的阻塞式IO可能会造成问题
在服务器开发中 需要在服务器端通过少量线程处理多个客户端请求 这就要求 在少量的线程应该可以灵
活的切换处理不同客户端 但传统的BIO阻塞式的工作方式 一旦阻塞了线程 线程就被挂起 无法继续执行
无法实现这样的功能
在这里插入图片描述
NIO - JDK4.0 - 同步非阻塞式IO - NonBlockingIO/NewIO
和传统的BIO比起来最主要的特点是 在执行ACCEPT CONNECT READ WRITE 操作时是非阻塞的
非常便于实现 在服务器开发中 用少量的线程来处理多个客户端请求 由于以上四种操作都是非阻塞的 可
以随时让线程切换所处理的客户端 从而可以实现高并发服务器的开发

  1. 特点
    BIO:同步阻塞式IO 面向流 操作字节或字符 单向传输数据
    NIO:同步非阻塞式IO 面向通道 操作缓冲区 双向传输数据

5.2Buffer概述

缓冲区,本质上就是一段连续的内存空间,用来临时存放大量指定类型的数据

java.nio.Buffer
ByteBuffer, CharBuffer, DoubleBuffer,
FloatBuffer, IntBuffer, LongBuffer, ShortBuffer

  1. Buffer中的重要概念
    在这里插入图片描述

a.
int capacity() 返回此缓冲区的容量。
position - 当前位置,初始值为0,指定Buffer进行读写操作时操作位置,每当操作过后position自动+
1指向下一个位置
b.
int position() 返回此缓冲区的位置。
Buffer position(int newPosition) 设置此缓冲区的位置。
c.
limit - 限制位,初始值等于capacity,position永远小于等于limit c.
int limit() 返回此缓冲区的限制。
Buffer limit(int newLimit) 设置此缓冲区的限制。

5.2.1创建Buffer

创建出来的Buffer默认 capacity等于容量 position为0 limit等于capacity

  1. 没有构造方法 不能直接new
  2. 可以直接使用如下方法创建指定大小的空的缓冲区 其中的capacity参数就是缓冲区的容量的大小
    缓冲区容量大小 只能再创建时指定 之后无法进行修改
static ByteBufferallocate(int capacity) 分配一个新的字节缓冲区。

3.将一个已经存在的byte数组 包装为一个ByteBuffer 其中的数据会被保留 ByteBuffer的容量 等于
byte数据中获取到的数据的大小

static ByteBufferwrap(byte[] array) 将 byte 数组包装到缓冲区中。
static ByteBufferwrap(byte[] array, int offset, int length) 将 byte 数组包装到缓冲区中。

5.2.2写入数据到buffer

position指向写入数据数据的位置,每当写入一个数据,position自动+1指向下一个位置,position不可大于
limit,如果一直写入,达到limit大小,再写入会抛出异常
通过putXxx()可以向缓冲区中写入不同类型的数据 要注意 无论用的是哪个方法 put的是什么类型的数据
存到缓冲区里都是字节数据

abstract
ByteBuffer
put(byte b) 相对 put 方法(可选操作)。
ByteBufferput(byte[] src) 相对批量 put 方法(可选操作)。
ByteBufferput(ByteBuffer src) 相对批量 put 方法(可选操作)。
abstract
ByteBuffer
put(int index, byte b) 绝对 put 方法(可选操作)。
abstract
ByteBuffer
putChar(char value) 用来写入 char 值的相对 put 方法(可选操作)。
abstract
ByteBuffer
putChar(int index, char value) 用于写入 char 值的绝对 put 方法(可选操
作)。
abstract
ByteBuffer
putDouble(double value) 用于写入 double 值的相对 put 方法(可选操
作)。
abstract
ByteBuffer
putDouble(int index, double value) 用于写入 double 值的绝对 put 方法
(可选操作)。
abstract
ByteBuffer
putFloat(float value) 用于写入 float 值的相对 put 方法(可选操作)。
abstract
ByteBuffer
putFloat(int index, float value) 用于写入 float 值的绝对 put 方法(可选
操作)。
abstract
ByteBuffer
putInt(int value) 用于写入 int 值的相对 put 方法(可选操作)。
abstract
ByteBuffer
putInt(int index, int value) 用于写入 int 值的绝对 put 方法(可选操
作)。
abstract
ByteBuffer
putLong(int index, long value) 用于写入 long 值的绝对 put 方法(可选操
作)。
abstract
ByteBuffer
putLong(long value) 用于写入 long 值(可先操作) 的相对 put 方法。
abstract
ByteBuffer
putShort(int index, short value) 用于写入 short 值的绝对 put 方法(可选
操作)。
abstract
ByteBuffer
putShort(short value) 用于写入 short 值的相对 put 方法(可选操作)。

5.2.3从buffer中获取数据

position指向读取数据的位置,每当读到一个数据,position自动+1指向下一个位置,position不可大于limit,
如果一直读取,达到limit大小,再读取会抛出异常
通过getXxx()可以向缓冲区中写入不同类型的数据

abstract byteget() 相对 get 方法。
ByteBufferget(byte[] dst) 相对批量 get 方法。
ByteBufferget(byte[] dst, int offset, int length) 相对批量 get 方法。
abstract byteget(int index) 绝对 get 方法。
abstract chargetChar() 用于读取 char 值的相对 get 方法。
abstract chargetChar(int index) 用于读取 char 值的绝对 get 方法。
abstract doublegetDouble() 用于读取 double 值的相对 get 方法。
abstract doublegetDouble(int index) 用于读取 double 值的绝对 get 方法。
abstract floatgetFloat() 用于读取 float 值的相对 get 方法。
abstract floatgetFloat(int index) 用于读取 float 值的绝对 get 方法。
abstract intgetInt() 用于读取 int 值的相对 get 方法。
abstract intgetInt(int index) 用于读取 int 值的绝对 get 方法。
abstract longgetLong() 用于读取 long 值的相对 get 方法。
abstract longgetLong(int index) 用于读取 long 值的绝对 get 方法。
abstract shortgetShort() 用于读取 short 值的相对 get 方法。
abstract shortgetShort(int index) 用于读取 short 值的绝对 get 方法。

5.2.4反转缓冲区

在写入缓冲区完成,想要从中读取数据之前需要先进行反转缓冲区操作,本质上就是将 limit设置为当前
position的值,再将position设置为0的过程

Bufferflip() 反转此缓冲区。

等价于:
buf.limit(buf.position());
buf.position(0);

5.2.5判断边界

这个方法可以返回 limit - position的值,通常用来获取读写时是距离边界的距离

intremaining() 返回当前位置与限制之间的元素数。

这个方法可以返回 limit-position>0 的值,通常用来判断度写时是否到达了边界

booleanhasRemaining() 告知在当前位置和限制之间是否有元素。

5.2.6重绕缓冲区

这个方法将将position置为0 ,可以重新进行读写操作

Bufferrewind() 重绕此缓冲区。

5.2.7设置/重置标记

可以在读写缓冲区的过程中 通过mark()方法设置一个临时的指针mark指向当前position的值 之后 在任
何时候 可以调用reset()方法 使position重新指向mark指定的值 从而 恢复到上一次mark位进行读写操

Buffermark() 在此缓冲区的位置设置标记。
Bufferreset() 将此缓冲区的位置重置为以前标记的位置。

5.2.8清空缓冲区

此方法用来清空缓冲区,但是它并不会真的取清除缓冲区中的数据,而只是修改响应buffer的参数.抛弃
mark 将limit设置为capacity 将position设置为0,效果上等价于清空数据.

Bufferclear() 清除此缓冲区。

5.3Channel详解

5.3.1通道概述

NIO中的基本概念,类似于BIO中的流,不同的是,操作的是缓冲区,且可以双向传输数据
AbstractInterruptibleChannel implements Channel
|
|-FileChannel
|
|-SelectableChannel
|
|-AbstractSelectableChannel
|
|-SocketChannel
|-ServerSocketChannel
|-DatagramChannel

5.3.2ServerSocketChannel

代表tcp通信中的服务器端
构造方法被保护起来 无法直接使用

构造方法摘 要
protectedServerSocketChannel(SelectorProvider provider) 初始化此类的一个新实 例。

可以通过如下静态方法 得到ServerSocketChannel对象

可以通过如下静态方法 得到ServerSocketChannel对象open() 打开服务器套接字通道。

获取到底层ServerSocketChannel底层对应的真正的Socket套接字对象

abstract ServerSocketabstract ServerSocket

绑定监听端口

abstract ServerSocketabstract ServerSocket

jdk7才开始有这个方法 在这以前 需要先获取socket通过socket来绑定端口
配置通道的阻塞模式,默认是阻塞模式,通过传入false可以改为非阻塞模式

SelectableChannelconfigureBlocking(boolean block) 调整此通道的阻塞模式。

等待客户端连接,在阻塞模式下会一直阻塞直到客户端连接,返回一个代表链接的SockentChannel对象.在
非阻塞模式下,此方法不会阻塞,直接执行下去,如果没有得到一个新的连接,此方法返回null

abstract SocketChannelaccept() 接受到此通道套接字的连接。

在非阻塞模式下,ACCEPT操作没有阻塞,无论是否收到一个连接,都直接执行下去,此时即使ACCEPT方法执
行成功,也无法确认连接完成.此时应该自己通过代码来控制实现连接,或者,通过选择器来实线选择操作.

SocketChannel sc = null;
while(sc == null){
	sc = ssc.accept();
}

关闭通道

voidclose关闭通道

5.3.3SocketChannel

代表tcp通信中的客户端
构造方法被保护起来 无法直接使用

构造方法摘要
protectedSocketChannel(SelectorProvider provider) 初始化此类的一个新实例。

可以通过如下静态方法 得到SocketChannel对象

static SocketChannelopen() 打开套接字通道。

配置通道的阻塞模式,默认是阻塞模式,通过传入false可以改为非阻塞模式

SelectableChannelconfigureBlocking(boolean block) 调整此通道的阻塞模式。

命令客户端连接指定服务器地址端口 如果通道处于阻塞模式 则此方法会一直阻塞 直到 连接成功 而如果
通道处于非阻塞模式 此方法 将仅仅尝试着去连接 如果连接成功则返回true 如果连接一时间没有结束 也
不阻塞程序 此方法返回false 程序继续执行 此时需要在后续调用finishConnection方法来完成连接

abstract booleanconnect(SocketAddress remote) 连接此通道的套接字。

connect(SocketAddress remote) 连接此通道的套接字。

abstract booleanfinishConnect() 完成套接字通道的连接过程。

在非阻塞模式下,CONNECT操作没有阻塞,无论是否完成一个连接,都直接执行下去,此时即使CONNECT方
法执行成功,也无法确认连接完成.此时应该自己通过代码来控制实现连接,或者,通过选择器来实线选择操
作.

boolean isConn = sc.connect(new InetSocketAddress("127.0.0.1", 44444));
	if(!isConn){
		while(!sc.finishConnect()){
	}
}

从通道中读取数据到指定的缓冲区内,在阻塞模式下,如果没有读取到数据或者读取到的数据不够填满缓
冲区,此方法将会阻塞,直到读取到的数据填满了缓冲区阻塞才会被放开.在非阻塞模式下,此方法只是尝试
读取数据到缓冲区,无论是否读到或者是否读满缓冲区,都不会产生阻塞

abstract intread(ByteBuffer dst) 将字节序列从此通道中读入给定的缓冲区。

在非阻塞模式下,read方法执行过后,并不能保证真的读到了数据,或读全了数据,此时只能自己写代码来控
制读取的过程.

ByteBuffer buf = ByteBuffer.allocate(5);
while(buf.hasRemaining()){
	sc.read(buf);
}

ByteBuffer buf = ByteBuffer.allocate(5);
while(buf.hasRemaining()){
sc.read(buf);
}

abstract intwrite(ByteBuffer src) 将字节序列从给定的缓冲区中写入此通道。

在非阻塞模式下,write方法执行过后,并不能保证写出了所有的数据,此时只能自己写代码来控制写出的过

while(buf.hasRemaining()){
	sc.write(buf);
}

关闭通道

voidclose()关闭通道

5.4案例 - NIO实现TCP通信

服务端:

package cn.tedu.nio.channel;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
public class ServerSocketChannelDemo01 {
	public static void main(String[] args) throws Exception {
		//1.创建ServerSockentChannel对象
		ServerSocketChannel ssc = ServerSocketChannel.open();
		//2.绑定指定端口
		ssc.bind(new InetSocketAddress(44444));
		//3.设置非阻塞模式
		ssc.configureBlocking(false);
		//4.接收客户端连接
		SocketChannel sc = null;
		while(sc == null){
		sc = ssc.accept();
		}
		sc.configureBlocking(false);
		//5.读取数据
		ByteBuffer buf = ByteBuffer.allocate(5);
		while(buf.hasRemaining()){
		sc.read(buf);
		}
		//6.获取数据打印
		byte[] arr = buf.array();
		String str = new String(arr);
		System.out.println(str);
		//5.关闭通道
		sc.close();
		ssc.close();
	}
}

客户端:

package cn.tedu.nio.channel;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class SocketChannelDemo01 {
	public static void main(String[] args) throws Exception {
		//1.创建客户端SocketChannel
		SocketChannel sc = SocketChannel.open();
		//2.配置启用非阻塞模式
		sc.configureBlocking(false);
		//3.连接服务器
		boolean isConn = sc.connect(new InetSocketAddress("127.0.0.1", 44444));
		if(!isConn){
		while(!sc.finishConnect()){
		}
		}
		//4.发送数据到服务器
		ByteBuffer buf = ByteBuffer.wrap("abcde".getBytes());
		while(buf.hasRemaining()){
		sc.write(buf);
		}
		//5.关闭通道
		sc.close();
	}
}

5.5案例 - 实现少量线程 处理多个客户端请求

目标
利用Selector+channel+Buffer实现 少量线程处理多个客户端请求
客户端

package cn.tedu.nio.selector;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class SocketChannelDemo01 {
	public static void main(String[] args) throws Exception {
	//0.创建选择器
	Selector selc = Selector.open();
	//1.创建SocketChannel
	SocketChannel sc = SocketChannel.open();
	//2.设定非阻塞模式
	sc.configureBlocking(false);
	//3.连接服务端
	sc.connect(new InetSocketAddress("127.0.0.1", 44444));
	sc.register(selc, SelectionKey.OP_CONNECT);
	//4.通过选择器实行选择操作
	while(true){
		selc.select();//选择器尝试选择就绪的键 选不到就阻塞 选择到就返回就绪的键的数量
		//5.得到并遍历就绪的键们
		Set<SelectionKey> keys = selc.selectedKeys();
		Iterator<SelectionKey> it = keys.iterator();
		while(it.hasNext()){
			//6.得到每一个就绪的键
			SelectionKey key = it.next();
			//7.获取就绪的键 对应的 操作 和 通道
			if(key.isAcceptable()){
			
			}else if(key.isConnectable()){
				//--是通道的Connect操作
				//--获取通道
				SocketChannel scx = (SocketChannel) key.channel();
				//--完成连接
				if(!scx.isConnected()){
					while(!scx.finishConnect()){};
				}
				//--将通道再次注册到selc中 关注WRITE操作
				scx.register(selc, SelectionKey.OP_WRITE);
			}else if(key.isReadable()){
			
			}else if(key.isWritable()){
				//--发现是Write操作就绪
				//--获取通道
				SocketChannel scx = (SocketChannel) key.channel();
				//--写出数据
				ByteBuffer buf = ByteBuffer.wrap("hello nio~ hello java~".getBytes());
				while(buf.hasRemaining()){
					scx.write(buf);
				}
				//--取消掉当前通道 在选择器中的注册 放置重复写出
				key.cancel();
			}else{
				throw new RuntimeException("未知的键,见了鬼了~");
			}
			//8.移除就绪键
			it.remove();
		}
	}
	}
}

服务端

package cn.tedu.nio.selector;
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.Set;
public class ServerSocketDemo01 {
	public static void main(String[] args) throws Exception {
		//0.创建选择器
		Selector selc = Selector.open();
		//1.创建代表服务器的ServerSocketChannel对象
		ServerSocketChannel ssc = ServerSocketChannel.open();
		//2.设置为非阻塞模式
		ssc.configureBlocking(false);
		//3.设置监听的端口
		ssc.bind(new InetSocketAddress(44444));
		//4.将ssc注册到选择器中关注ACCEPT操作
		ssc.register(selc, SelectionKey.OP_ACCEPT);
		//5.通过选择器选择就绪的键
		while(true){
			selc.select();
			//尝试到注册的键集中来寻找就绪的键 如果一个就绪的键都找不到 就进入阻塞 直到找到就绪的键
			返回就绪的键的个数
			//6.获取就绪的键的集合
			Set<SelectionKey> keys = selc.selectedKeys();
			//7.遍历处理就绪的键 代表的操作
			Iterator<SelectionKey> it = keys.iterator();
			while(it.hasNext()){
			//--获取到就绪的键 根据键代表的操作的不同 来进行不同处理
			SelectionKey key = it.next();
			if(key.isAcceptable()){
			//--发现了Accept操作
			//--获取通道
			ServerSocketChannel sscx = (ServerSocketChannel) key.channel();
			//--完成Accept操作
			SocketChannel sc = sscx.accept();
			//--在sc上注册读数据的操作
			sc.configureBlocking(false);
			sc.register(selc, SelectionKey.OP_READ);
			}else if(key.isConnectable()){
			}else if(key.isWritable()){
			}else if(key.isReadable()){
			//--发现了Read操作
			//--获取就绪的通道
			SocketChannel scx = (SocketChannel) key.channel();
			//--完成读取数据的操作
			ByteBuffer buf = ByteBuffer.allocate(10);
			while(buf.hasRemaining()){
				scx.read(buf);
			}
			String msg = new String(buf.array());
			System.out.println("[收到来自客户端的消息]:"+msg);
			}else{
				throw new RuntimeException("未知的键,见了鬼了~");
			}
			//8.移除处理完的键
			it.remove();
			}
		}
	}
}

5.6案例-通过自定义协议完成任意长度数据通信

服务端:

package cn.tedu.nio.nb;
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.Set;
public class ServerSocketChannelDemo01 {
public static void main(String[] args) throws Exception {
System.err.println("服务端启动...");
//0.创建选择器
Selector selc = Selector.open();
//1.创建ssc
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
//2.绑定端口
ssc.bind(new InetSocketAddress(44444));
//3.注册ACCEPT
ssc.register(selc, SelectionKey.OP_ACCEPT);
//4.选择器执行选择操作 对就绪的键进行处理
while(true){
//--进行选择操作
selc.select();
//--获取选择到的键
Set<SelectionKey> keys = selc.selectedKeys();
//--遍历选择到的键 进行处理
Iterator<SelectionKey> it = keys.iterator();
while(it.hasNext()){
SelectionKey key = it.next();
if(key.isAcceptable()){
//--发现了Accept操作
//--获取通道
ServerSocketChannel sscx = (ServerSocketChannel) key.channel();
//--完成Accept操作
SocketChannel sc = sscx.accept();
sc.configureBlocking(false);
//--注册sc 到选择器 关注 read操作
sc.register(selc,SelectionKey.OP_READ);
}else if(key.isConnectable()){
}else if(key.isReadable()){
//--发现了Read操作
//--获取通道
SocketChannel scx = (SocketChannel) key.channel();
//--完成Read
//----根据协议 数据的结构 为 [长度\r\n内容]
//----先读取长度
ByteBuffer tmp = ByteBuffer.allocate(1);
String line = "";
while(!line.endsWith("\r\n")){
scx.read(tmp);
line += new String(tmp.array());
tmp.clear();
}
int len = Integer.parseInt(line.substring(0, line.length()-2));
//----读取后续len个字节 就是当前这段数据
ByteBuffer buf = ByteBuffer.allocate(len);
while(buf.hasRemaining()){
scx.read(buf);
}
String msg = new String(buf.array());
System.out.println("收到了来自客户端的消息:["+msg+"]");
//--将当前通道注册 关注Write事件
scx.register(selc, SelectionKey.OP_WRITE);
}else if(key.isWritable()){
//--发现了Write操作
//--获取通道
SocketChannel scx = (SocketChannel) key.channel();
//--完成Write
String str = "来自服务器的相应消息:[你好,客户端]";
String data = str.getBytes().length+"\r\n"+str;
ByteBuffer buf = ByteBuffer.wrap(data.getBytes());
while(buf.hasRemaining()){
scx.write(buf);
}
//--取消Write注册 防止重复写出
key.cancel();
}else{
throw new RuntimeException("未知的键~!");
}
//--移除处理完成的键
it.remove();
}
}
}
}

客户端:

package cn.tedu.nio.nb;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class SocketChannelDemo01 {
public static void main(String[] args) throws Exception {
System.err.println("客户端启动...");
//0.创建选择器
Selector selc = Selector.open();
//1.创建sc
SocketChannel sc = SocketChannel.open();
sc.configureBlocking(false);
//2.注册Connect操作
sc.connect(new InetSocketAddress("127.0.0.1", 44444));
sc.register(selc, SelectionKey.OP_CONNECT);
//3.选择器执行选择操作 对就绪的键进行处理
while(true){
//--进行选择操作
selc.select();
//--获取到就绪的键
Set<SelectionKey> keys = selc.selectedKeys();
//--遍历就绪的键 依次处理
Iterator<SelectionKey> it = keys.iterator();
while(it.hasNext()){
SelectionKey key = it.next();
if(key.isAcceptable()){
}else if(key.isConnectable()){
//--发现Connect操作
//--获取通道
SocketChannel scx = (SocketChannel) key.channel();
//--完成Connect操作
if(!scx.isConnected()){
while(!scx.finishConnect()){}
}
//--注册 sc 到选择器 关注Write操作
scx.register(selc, SelectionKey.OP_WRITE);
}else if(key.isReadable()){
//--发现了Read操作
//--获取通道
SocketChannel scx = (SocketChannel) key.channel();
//--完成Read
//----根据协议 数据的结构 为 [长度\r\n内容]
//----先读取长度
ByteBuffer tmp = ByteBuffer.allocate(1);
String line = "";
while(!line.endsWith("\r\n")){
scx.read(tmp);
line += new String(tmp.array());
tmp.clear();
}
int len = Integer.parseInt(line.substring(0, line.length()-2));
//----读取后续len个字节 就是当前这段数据
ByteBuffer buf = ByteBuffer.allocate(len);
while(buf.hasRemaining()){
scx.read(buf);
}
String msg = new String(buf.array());
System.out.println("收到了来自服务器的响应:["+msg+"]");
}else if(key.isWritable()){
//--发现了Write操作
//--获取通道
SocketChannel scx = (SocketChannel) key.channel();
//--完成Write操作
String str = "hello java hello nio hello China~";
String data = str.getBytes().length+"\r\n"+str;
ByteBuffer buf = ByteBuffer.wrap(data.getBytes());
while(buf.hasRemaining()){
scx.write(buf);
}
//--注册Read操作 接收服务器返回的数据
scx.register(selc, SelectionKey.OP_READ);
}else{
throw new RuntimeException("未知的键~!");
}
//--清除处理完成的键
it.remove();
}
}
}
}

5.7作业

掌握NIO相关概念和原理
理解NIO相关代码 不需要掌握
了解常见的NIO框架
了解三种IO机制的区别
预习Concurrent

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

XYDrestart

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值