传统的java.io包基于流模型实现,提供了一些 如File抽象,输入输出流等基本功能,交互方式是同步、阻塞的方式(Blocking IO)。也就是说,在读取输入流或者写入输出流时,在读、写操作完成之前,线程会一直阻塞在那里,他们之间的调用是可靠的线性顺序。传统的Java.io的优势在于简单直观,但IO效率不高,其扩展性受限,很容易成为项目的瓶颈。
另外,Java.net中的一些网络API也可以归结到同步阻塞的IO类库,比如Socket、ServerSocket、HttpURLConnection。因为网络通信同样也是IO行为。
在Java1.4版本中引入了NIO框架(java.nio),提供了Channel、Buffer、Selector等新的抽象,可以用以构建多路复用、同步非阻塞的IO程序。
在Java7中的NIO2(AIO)框架引入了异步、非阻塞的交互方式,异步IO操作基于事件和回调机制,应用操作直接返回而不会阻塞在那里,当后台处理完后,操作系统会通知相应的线程进行后续工作。
-
java.io不仅仅是对文件的操作,网络通信中,比如Socket通信,都是典型的IO操作的目标。
-
输入流/输出流(InputStream/OutputStream):是用来读取或者写入字节的,例如操作图片文件。
-
Reader/Writer:是用来操作字符的,增加了字符的编解码功能,适用于从文件中读取或者写入文本信息。本质上计算机操作的都是字节,不管是文件读取还是网络通信,Reader和Writer构建了应用逻辑和原始数据之间的桥梁。
-
带缓冲区的读写操作,例如BufferedInputStream、BufferedOutputStream,可以避免频繁的磁盘读写,这种设计利用了缓冲区,一次操作批量数据。在使用中千万不能忘了flash。
-
参考下面简化类图,很多IO工具类都实现了Closeable接口,对资源进行释放。
实例:实现一个服务器应用,简单要求:能够为多个客户端提供服务。
- 使用java.io和java.net中的同步、阻塞式API来简单实现:
1.服务端启动一个ServerSocket,端口0表示自动绑定一个空闲的端口。
2.调用accept方法,阻塞等待客户端连接。一旦连接成功,服务端的accept会返回一个负责传输数据的已连接socket。(一般,连接建立成功后,双方就可以开始通过read和write函数来读写数据)
3.连接建立成功后,启动一个单独线程回复客户端的请求。
4.利用Socket模拟一个简单的客户端,只进行连接、读取、打印。
相关模拟代码:
package Test;
import java.io.*;
import java.net.*;
public class DemoServer extends Thread {
private ServerSocket serverSocket;
public int getPort() {
return serverSocket.getLocalPort();
}
public void run() {
try {
//服务端启动一个ServerSocket,端口0表示自动绑定一个空闲的端口
//这里的ServerSocket是一个监听的socket
serverSocket = new ServerSocket(0);
while (true) {
//调用accept方法,阻塞等待客户端连接
//这里的Socket是一个用于连接的socket,负责传输数据
Socket socket = serverSocket.accept();
//当连接建立后单独启动一个线程响应客户端的请求
RequestHandler requestHandler = new RequestHandler(socket);
requestHandler.start();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (serverSocket != null) {
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws IOException {
DemoServer server = new DemoServer();
server.start();
//利用socket模拟一个简单的客户端,只进行连接、读取、打印
try (Socket client = new Socket(InetAddress.getLocalHost(), server.getPort())) {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(client.getInputStream()));
bufferedReader.lines().forEach(s -> System.out.println(s));
}
}
}
//响应客户端请求的线程
package Test;
import java.io.PrintWriter;
import java.net.Socket;
// 简化实现,不做读取,直接发送字符串
class RequestHandler extends Thread {
private Socket socket;
RequestHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try (PrintWriter out = new PrintWriter(socket.getOutputStream())) {
out.println("Hello world!");
out.flush();
} catch (Exception e) {
e.printStackTrace();
}
}
}
使用简单的java.io与java.net实现的同步、阻塞式API存在的潜在问题:Java目前的线程实现是比较重量级的,创建或者销毁一个线程有明显的开销,每个线程都有单独的线程栈结构,需要占用比较明显的内存,所以,每一个client启动一个线程,挺浪费的。
解决方案:引入线程池来避免浪费——通过一个固定大小的线程池,来负责管理响应客户端请求的工作线程,避免频繁创建、销毁线程的开销,这是构建并发服务的典型。参考下图进行理解:
相关代码:
package Test;
import java.io.*;
import java.net.*;
import java.util.concurrent.*;
public class DemoServer extends Thread {
private ServerSocket serverSocket;
public int getPort() {
return serverSocket.getLocalPort();
}
public void run() {
try {
//服务端启动一个ServerSocket,端口0表示自动绑定一个空闲的端口
//这里的ServerSocket是一个监听的socket
serverSocket = new ServerSocket(0);
//创建一个指定工作数量的线程池
ExecutorService executor = Executors.newFixedThreadPool(8);
while (true) {
//调用accept方法,阻塞等待客户端连接
// 这里的Socket是一个用于连接的socket,负责传输数据
Socket socket = serverSocket.accept();
//创建一个响应客户端的请求的工作线程提交到线程池中
RequestHandler requestHandler = new RequestHandler(socket);
executor.execute(requestHandler);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (serverSocket != null) {
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws IOException {
//服务端启动ServerSocket
DemoServer server = new DemoServer();
server.start();
//利用socket模拟一个简单的客户端,只进行连接、读取、打印
try (Socket client = new Socket(InetAddress.getLocalHost(), server.getPort())) {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(client.getInputStream()));
bufferedReader.lines().forEach(s -> System.out.println(s));
}
}
}
线程池解决 重量级线程创建与销毁的开销大 的方法只在普通应用——仅几百个连接的情况下,可以工作得很好。一旦连接数量变得超级多,线程上下文切换的开销会在高并发是变得非常明显,这是同步阻塞方式的劣势——低扩展性。
前两个示例,IO都是同步阻塞模式,需要利用多线程来实现多任务处理。而NIO多路复用利用了单线程轮询事件的机制,通过定位就绪的Channel来决定做什么,仅仅在select阶段是阻塞的,可以有效避免当大量的客户端连接时,频繁的线程切换带来的开销。如下图所示。
- NIO引入多路复用机制
1.通过Selector.open()创建一个selector,作为类似调度员的角色。
2.通过ServerSocketChannel.open()创建一个Channel,明确配置其为非阻塞模式。
3.Selector阻塞在select操作,当有Channel发生连接请求时,Selector被唤醒。
4.通过SocketChannel和Buffer进行数据操作,发送字符串。
相关代码:
package Test;
import java.io.*;
import java.net.*;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.util.*;
public class NIOServer extends Thread {
public void run() {
// 通过Selector.open()创建一个selector,作为类似调度员的角色
//通过ServerSocketChannel.open()创建一个channel
try (Selector selector = Selector.open();
ServerSocketChannel serverSocket = ServerSocketChannel.open();) {
//绑定ip和端口
serverSocket.bind(new InetSocketAddress(InetAddress.getLocalHost(), 8888));
//需要明确配置是 非阻塞模式,因为在阻塞模式下,注册操作是不允许的,会抛出IllegalBlockingModeException异常
serverSocket.configureBlocking(false);
// 注册到 Selector,并指定SelectionKey.OP_ACCEPT,告诉Selector,它关注的是最新连接请求
serverSocket.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
//Selector阻塞在select()请求,当有Channel发生连接请求,就会被唤醒
// 阻塞等待就绪的 Channel,这是关键点之一
selector.select();
//采用单线程轮询事件的机制,通过高效确定就绪的Channel,来决定做什么
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iter = selectedKeys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
// 生产系统中一般会额外进行就绪状态检查
sayHelloWorld((ServerSocketChannel) key.channel());
iter.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
//通过SocketChannel和Buffer进行数据操作,发送字符串
private void sayHelloWorld(ServerSocketChannel server) throws IOException {
try (SocketChannel client = server.accept();) {
client.write(Charset.defaultCharset().encode("Hello world!"));
}
}
public static void main(String[] args) throws IOException {
NIOServer nioserver = new NIOServer();
nioserver.start();
//利用socket模拟一个简单的客户端,只进行连接、读取、打印
try (Socket client = new Socket(InetAddress.getLocalHost(), 8888)) {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(client.getInputStream()));
bufferedReader.lines().forEach(s -> System.out.println(s));
}
}
}