前言
在日常的开发中,IO是无处不在的,但是真真正正做IO相关的开发,应该是比较少的,今天我们来了解一下
同步/异步
同步指两个或两个以上的事物随时间变化保持一定的相对关系,在计算机的世界里,我们可以把事物看成请求。在同步的场景中,请求必须等待返回结果才能往下执行,多个请求必须逐个逐个执行。异步跟同步相反,请求不需要等到返回结果就可以往下执行,多个请求可以同时执行。比如:你去一个饭店吃饭,同步的场景下,你点完一个菜,必须等厨师洗菜,煮菜,上菜才能点下一个菜。异步的场景下,你点完一个菜,厨师就可以干活了,你也可以接着点下一个菜
阻塞/非阻塞
阻塞指有障碍不能通过,非阻塞自然是指没有障碍能通过了。我们可以用堵车来形容阻塞跟非阻塞,阻塞的时候,车是不能动的,非阻塞的时候,车是能动的,在线程中,不能动的状态是线程挂起,能动的状态是线程运行
差异
乍一看,同步/异步,阻塞/非阻塞很像,其实两者的关注点不同 同步/异步关注的是多个请求能不能同时进行 阻塞/非阻塞关注的是线程执行时的状态
BIO
如上图
BIO(Blocking I/O):同步阻塞IO,每当有一个客户端连接服务端,服务端就启动一个线程处理,如果这个连接不做任何事情,会造成不必要的线程开销
代码示例
public class BIOServer {
public static void main(String[] args) throws IOException {
// BIO的场景下,每当有一个客户端连接,服务端就需要启动一个线程,所以使用线程池的方式优化
ExecutorService executorService = Executors.newCachedThreadPool();
ServerSocket serverSocket = new ServerSocket(6666);
System.out.println("服务器启动了");
while (true) {
// 监听、等待客户端连接
final Socket socket = serverSocket.accept();
System.out.println("连接到一个客户端");
// 创建一个线程进行通讯
executorService.execute(() -> handle(socket));
}
}
public static void handle(Socket socket) {
System.out.println("线程信息 ID = " + Thread.currentThread().getId() + ", 名字 = " + Thread.currentThread().getName());
try {
byte[] b = new byte[1024];
InputStream stream = socket.getInputStream();
while (true) {
int read = stream.read(b);
if (read != -1) {
System.out.println(new String(b, 0, read));
} else {
break;
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
问题
- 每个客户端的请求都需要创建单独的线程处理,当并发数较大时,服务端需要创建大量的线程,占用大量的资源
- 连接建立后,如果当前线程没有数据可读,线程会阻塞在read操作上,造成资源浪费
NIO
因为BIO的种种问题,Java做了一系列的改进,即NIO,是同步非阻塞的
我们先从一个整体的角度看看NIO
- NIO中有三大核心,分别是Selector、Channel、Buffer,每个Channel都对应一个Buffer,多个Channel可以注册到同一个Selector中,一个Selector对应一个线程
- NIO是面向缓冲区的编程,同一个Buffer,既可以读,也可以写,这跟BIO不同,BIO是面向流的编程,要么是输入流,要么是输出流
- NIO是非阻塞的,一个线程可以处理多个通道,当前通道没有读写操作,并不会阻塞在当前通道,而是会处理有读写操作的通道
Buffer
Buffer底层就是数组,在这个数组中,有三个特别重要的变量,分别是position、limit、capacity
- position:指向下一个将要被写入或者读取的元素索引
- limit:指向缓冲区的终点,
- capacity:容量,表示缓冲区的最大容量
新建一个Buffer时,position、limit、capacity指针的指向如下
这个Buffer新增一个元素时,position、limit、capacity指针的指向如下
这个Buffer新增第五个元素时,position、limit、capacity指针的指向如下
当我们要对缓冲区进行读取时,需要进行缓冲区翻转,翻转后position、limit、capacity指针的指向如下
示例代码
public class BufferTest {
public static void main(String[] args) {
IntBuffer intBuffer = IntBuffer.allocate(10);
for (int i = 0; i < 10; i++) {
int temp = new Random().nextInt(100);
System.out.println(temp);
intBuffer.put(temp);
}
// 进行Buffer的翻转
intBuffer.flip();
System.out.println("-------------------");
while (intBuffer.hasRemaining()) {
System.out.println(intBuffer.get());
}
}
}
源码
常用Buffer子类,分别代表了存储的数据类型,见名知意,很好懂
ByteBuffer、IntBuffer、LongBuffer、ShortBuffer、CharBuffer、DoubleBuffer、FloatBuffer
当我们使用IntBuffer.allocate(10)这个方法时,源码如下
public static IntBuffer allocate(int capacity) {
if (capacity < 0)
throw new IllegalArgumentException();
// IntBuffer是个抽象类,分配空间的时候实际上创建的是HeapIntBuffer对象
return new HeapIntBuffer(capacity, capacity);
}
// 进行Buffer的翻转时,只是改变了指针的指向
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
Channel
Channel类似流,但是跟流不太一样,流是单向的,逆流而上的水大家应该没有看过吧,像FileInputStream只能用来读取,Channel是双向的,既可以读,也可以写,看看下面一段代码,感受一下Channel的作用
public class ChannelTest {
public static void main(String[] args) throws IOException {
FileInputStream fileInputStream = new FileInputStream("input.txt");
FileChannel inputStreamChannel = fileInputStream.getChannel();
FileOutputStream fileOutputStream = new FileOutputStream("output.txt");
FileChannel outputStreamChannel = fileOutputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(64);
while (true) {
// 重置数组指针,这步操作非常重要
byteBuffer.clear();
int read = inputStreamChannel.read(byteBuffer);
// -1表示读到文件结尾
if (read == -1) {
break;
}
byteBuffer.flip();
outputStreamChannel.write(byteBuffer);
}
inputStreamChannel.close();
outputStreamChannel.close();
}
}
三大组件配合使用
Selector、Channel、Buffer三大组件配合使用
public class NIOServer {
public static void main(String[] args) throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
Selector selector = Selector.open();
// 监听端口6666
serverSocketChannel.socket().bind(new InetSocketAddress(6666));
serverSocketChannel.configureBlocking(false);
// channel注册到selector中,关心事件为 OP_ACCEPT
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
if (selector.select(1000) == 0) {
System.out.println("服务器等待一秒,无连接");
continue;
}
// 如果返回的 >0,就获取相关的SelectionKey集合
// 通过SelectionKey反向获取通道
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
// 获取到SelectionKey
SelectionKey selectionKey = iterator.next();
// 不同事件,做不同的处理
// 如果是acceptable,说明有新的客户端连接
if (selectionKey.isAcceptable()) {
// 该客户端生成一个SocketChannel
SocketChannel socketChannel = serverSocketChannel.accept();
System.out.println("客户端连接成功,生成了一个socketChannel, socketChannel :" + socketChannel.hashCode());
socketChannel.configureBlocking(false);
// channel注册到selector中,并且关联一个buffer
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
if (selectionKey.isReadable()) {
// 通过key反向获取channel
SocketChannel channel = (SocketChannel) selectionKey.channel();
// 获取到该channel关联的buffer
ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
channel.read(buffer);
System.out.println("form 客户端 " + new String(buffer.array()));
}
iterator.remove();
}
}
}
}