1、同步、异步、阻塞、非阻塞
同步与异步:
同步就是一个任务的完成需要依赖另外一个任务时,只有等待被依赖的任务完成后,依赖的任务才能算完成,这是一种可靠的任务序列。要么成功都成功,失败都失败,两个任务的状态可以保持一致。而异步是不需要等待被依赖的任务完成,只是通知被依赖的任务要完成什么工作,依赖的任务也立即执行,只要自己完成了整个任务就算完成了。至于被依赖的任务最终是否真正完成,依赖它的任务无法确定,所以它是不可靠的任务序列。我们可以用打电话和发短信来很好的比喻同步与异步操作。
阻塞与非阻塞:
阻塞与非阻塞主要是从 CPU 的消耗上来说的,阻塞就是 CPU 停下来等待一个慢的操作完成 CPU 才接着完成其它的事。非阻塞就是在这个慢的操作在执行时 CPU 去干其它别的事,等这个慢的操作完成时,CPU 再接着完成后续的操作。虽然表面上看非阻塞的方式可以明显的提高 CPU 的利用率,但是也带了另外一种后果就是系统的线程切换增加。增加的 CPU 使用时间能不能补偿系统的切换成本需要好好评估。
2、BIO
BIO,一般称之为阻塞(block)IO、基本(basic)IO。就是传统的http://java.io包,它是基于流模型实现的,交互的方式是同步、阻塞方式,主要应用于文件IO和网络IO。
文件IO就是 InputStream、OutputStream
基于字节操作的 IO、Writer、Reader
基于字符操作的 IO,网络IO就是 Socket
基于网络操作的 IO。文件IO就是读写操作,就不记录了,这里记录一下网络IO吧。
在JDK1.4之前,建议网络连接的时候只能采用BIO,需要先在服务端启动一个ServerSocket,然后在客户端启动ocket来对服务端进行通信。默认情况下服务端需要对每个请求建立一堆线程等待请求,而客户端发送请求后,会先咨询服务端是否有线程响应,如果没有则会一直等待或者遭到拒绝请求,如果有的话客户端线程会等待请求结束后才继续执行,这就是阻塞IO。
服务端代码:
/**
* 服务端
*/
public class BioServer {
public static void main(String[] args) throws Exception{
// 创建ServerSocket对象,绑定端口:9000
ServerSocket serverSocket = new ServerSocket(9000);
for (;;){
System.out.println("1");
// 监听客户端程序,看是否有客户端来连接、发消息等(阻塞方式,如果客户端没有连接,会一直阻塞在这里,不会往下执行)
Socket accept = serverSocket.accept();
System.out.println("2");
// 从连接中取出输入流来接收消息
InputStream inputStream = accept.getInputStream();
byte[] bytes = new byte[10];
inputStream.read(bytes);
// 打印接收到的消息
System.out.println("对方说" + new String(bytes));
// 回答消息
OutputStream outputStream = accept.getOutputStream();
System.out.println("请输入:");
Scanner scanner = new Scanner(System.in);
String data = scanner.nextLine();
outputStream.write(data.getBytes());
// 关闭
// serverSocket.close();
}
}
}
客户端代码:
/**
* 客户端
*/
public class BioClient {
public static void main(String[] args) throws Exception{
for (;;){
// 创建Socket对象
Socket socket = new Socket("127.0.0.1", 9000);
// 从连接中取出输出流并发送消息
OutputStream outputStream = socket.getOutputStream();
System.out.println("请输入:");
Scanner scanner = new Scanner(System.in);
String data = scanner.nextLine();
outputStream.write(data.getBytes());
System.out.println("3");
// 从连接中取出输入流接收并回答(这里也是阻塞,如果没有接收到对方回复的消息,这里会一直阻塞下去)
InputStream inputStream = socket.getInputStream();
byte[] bytes = new byte[10];
inputStream.read(bytes);
System.out.println("4");
System.out.println("对方说:" + new String(bytes).trim());
// 关闭
// socket.close();
}
}
}
- 1、首先启动服务端,控制台只打印了1,并没有继续往下执行:
- 2、启动客户端,输入aaa,如下图,服务端控制台继续执行了下去,但是可以看到客户端也阻塞了,控制台只打印了3:
- 3、服务端回复bbb,客户端打印出4并循环执行:
BIO 就是传统的 http:// java.io 包,它是基于流模型实现的,交互的方式是同步、阻塞方式,也就是说在读入输入流或者输出流时,在读写动作完成之前,线程会一直阻塞在那里,它们之间的调用时可靠的线性顺序。它的有点就是代码比较简单、直观;缺点就是 IO 的效率和扩展性很低,容易成为应用性能瓶颈。
3、NIO
NIO,一般称之为新(new)IO、非阻塞(non-blocking)IO。就是java.nio包,是从JDK1.4开始
提供的新api,Java提供了一系列改进的输入/输出的新特性,统称为NIO。新增了许多用于处理输入输出的类,这些类都被放在java.nio包及子包下,并且对原java.io包中很多类进行改写,新增了满足NIO的功能,交互的方式是同步、非阻塞方式。
NIO和BIO有相同的目的和作用,但是他们的实现方式完全不同,BIO是以流
的方式处理数据,而NIO是以块
的方式处理数据,块IO的效率要比流IO高很多。另外,NIO是非阻塞式
的,这一点也和BIO很不相同,使用NIO可以提供非阻塞式的高伸缩性网络。
NIO主要有三大核心部分:Channel(通道)、Buffer(缓冲区)、Selector(选择器)。传统的BIO是基于字节流和字符流进行操作
,而NIO基于Channel和Buffer进行操作
,数据总是从通道读取到缓冲区,或者从缓冲区写入通道中。Selector(选择器)用于监听多个通道的事件(比如:连接打开、数据到达)。因此使用单个线程就可以监听多个数据通道。
前面说了NIO和BIO有相同的目的和作用,那么NIO的主要应用也是文件IO和网络IO。下面先解释一下NIO的核心部分概述和API,然后通过代码来看一下。
缓冲区(Buffer):
实际上是一个容器,是一个特殊的数组,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由Buffer。
在NIO中,Buffer是一个顶层父类,它也是一个抽象类,常用的有以下子类:
对于Java中的基本数据类型,都有一个具体的Buffer类型与之对应,最常用的自然是ByteBuffer类(二进制数据),该类的主要方法有以下几个:
- public abstract ByteBuffer put(byte b):存储字节数据到缓冲区。
- public abstract byte get():从缓冲区获得字节数据。
- public final byte[] array():把缓冲区数据转换成字节数组。
- public static ByteBuffer allocate(int capacity):设置缓冲区初识容量。
- piblic static ByteBuffer wrap(byte[] array):把一个线程的数组放到缓冲区中使用。
- public final Buffer flip():反转缓冲区,充值位置到初始位置。
通道(Channel):
类似于BIO中的stream,用来建立到目标的一个连接,但是需要注意
:BIO中的stream是单向的,例如FileInputStream对象只能进行读取数据的操作,而NIO中的通道是双向的,既可以用来进行读操作,也可以用来进行写操作。常用的Channel类有:FileChannel、DatagramChannel、ServerSocketChannel、SocketChannel。FileChannel用于文件的数据读写,DatagramChannel用于UDP的数据读写,ServerSocketChannel和SocketChannel用于TCP的数据读写。
FileChannel常用方法:
- public int read(ByteBuffer dst):读取数据并放到缓冲区中。
- public int write(ByteBuffer src):把缓冲区的数据写到通道中。
- public long transferFrom(ReadableByteChannel src, long position, long count):从目标通道中复制数据。
- public long transferTo(long position, long count, WritableByteChannel target):把数据从当前通道复制给目标通道。
示例:通过NIO往本地文件中写数据:
public class NioWrite {
public static void main(String[] args) throws Exception{
// 创建输出流
FileOutputStream fileOutputStream = new FileOutputStream("test.txe");
// 从流中得到一个通道
FileChannel channel = fileOutputStream.getChannel();
// 提供一个缓冲区
ByteBuffer allocate = ByteBuffer.allocate(1024);
// 往缓冲区存入数据
String string = "我是要输入的数据";
allocate.put(string.getBytes());
// 翻转缓冲区,将位置设为初始位置
allocate.flip();
// 把缓冲区写到通道中
channel.write(allocate);
// 关闭
fileOutputStream.close();
}
}
可以看到上面代码有个翻转缓冲区的代码,当你把这一行代码去掉之后你会发现,test.txe文件内显示的内容是空的,这是为啥呢?
原因看下图:
缓冲区存数据就是从头然后到尾,而1和2表示的就是指针位置,当我们把我是要输入的数据
这一段话存到缓冲区中的时候,指针就到了2的位置,然后开始往通道写数据,实际上就是从2的位置开始往下写,而下面内容明显是空的,所以test.txe文件内显示的内容是空的,而缓冲区的flip()方法,就是将指针从2翻转到初始位置,也就是1的位置,再往通道写数据,这时候就有了内容。
示例:通过NIO从本地文件中读取数据:
public class NioRead {
public static void main(String[] args) throws Exception{
File file = new File("test.txe");
// 创建输入流
FileInputStream fileInputStream = new FileInputStream(file);
// 得到一个通道
FileChannel channel = fileInputStream.getChannel();
// 准备一个缓冲区
ByteBuffer allocate = ByteBuffer.allocate((int) file.length());
// 从通道里读取数据并存到缓冲区
channel.read(allocate);
// 打印数据
System.out.println(new String(allocate.array()));
// 关闭
fileInputStream.close();
}
}
选择器(Selector):
选择器,能够检测多个注册的通道上是否有时间发生,如果有时间发生,便获取事件然后针对每个事件进行相应的响应处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接。这样使得只有在连接真正的读写事件发生时,才会调用函数来进行读写,就大大的减少了系统开销
,并且不必为每个连接都创建一个线程,不用去维护多个线程,并且避免了多线程之间的上下文切换导致的开销。
常用方法
:
- public static Selector open():得到一个选择器对象。
- public int select(long timeout):监控所有注册的channel,当其中有注册的IO操作可以进行时,将对应的SelectionKey加入到内部集合中并返回,参数用来设置超时时间。
- public Set SelectionKeys():从内部集合中得到所有的SelectionKey。
2、SelectionKey
:代表了Selector和serverSocketChannel的注册关系,一共四种:
- int OP_ACCEPT:有新的网络连接可以accept,值为16。
- int OP_CONNECT:代表连接已经建立,值为8。
- int OP_READ和int OP_WRITE:代表了读、写操作,值为1和4。
常用方法
:
- public abstract Selector selector():得到与之关联的Selector对象。
- public abstract SelectableChannel channel():得到与之关联的通道。
- public final Object attachment():得到与之关联的共享数据。
- public abstract SelectionKey interestOps(int ops):设置或改变监听事件。
- public final boolean isAccepyable():是否可以accept。
- public final boolean isReadable():是否可以读。
- public final boolean isWritable():是否可以写。
3、ServerSocketChannel
:用来在服务器端监听新的客户端Socket连接。
常用方法
:
- public static ServerSocketChannel open():得到一个ServerSocketChannel通道。
- public final ServerSocketChannel bind(SocketAddress local):设置服务器端口号。
- public final SelectableChannel configureBlocking(boolean block):设置阻塞或非阻塞模式,取值false表示采用非阻塞模式。
- public Socketchannel accept():接收一个连接,返回代表这个连接的通道对象。
- public final SelectionKey register(Selector sel, int ops):注册一个选择器并设置监听事件。
4、SocketChannel
:网络IO通道,具体负责进行读写操作。NIO总是把缓冲区的数据写入通道,或者把通道里的数据读出到缓冲区。
常用方法
:
- public static SocketChannel open():得到一个SocketChannel通道。
- public final SelectableChannel configureBlocking(boolean block):设置阻塞或非阻塞模式,取值false表示采用非阻塞模式。
- public boolean connect(SocketAddress remote):连接服务器。
- public boolean finishConnect():如果上面的方法连接失败了,接下来就要通过该方法完成连接操作。
- public int write(ByteBuffer src):往通道写数据。
- public int read(ByteBuffer dst):从通道读数据。
- public final SelectionKey register(Selector sel, int ops, Object att):注册一个选择器并设置监听时间,最后一个参数可以设置共享数据。
- public final void close():关闭通道。
示例:NIO实现服务器端和客户端之间的数据通信(非阻塞、基础版):
客户端代码:
public class NioClient {
public static void main(String[] args) throws Exception{
// 得到一个网络通道
SocketChannel socketChannel = SocketChannel.open();
// 设置非阻塞模式
socketChannel.configureBlocking(false);
// 提供服务器端的ip地址和端口号
InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 9001);
// 连接服务器
if (!socketChannel.connect(inetSocketAddress)) {
// 循环-直到某一时刻连接到(在连接的时候可以做别的事情,比如下面打印数据,这就是NIO的优势)
while (!socketChannel.finishConnect()){
System.out.println("还没有连接到。。。");
}
System.out.println("连接到了。。。");
}
// 得到一个缓冲区并存入数据
String string = "NIO测试,我是客户端传的数据";
ByteBuffer writeBuffer = ByteBuffer.wrap(string.getBytes());
// 发送数据
socketChannel.write(writeBuffer);
// 这里关闭通道,服务器端会报异常,所以这里就先设置阻塞
System.in.read();
}
}
服务器端代码:
public class NioServer {
public static void main(String[] args) throws Exception{
// 得到一个ServerSocketChannel对象
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 得到一个Selector对象
Selector selector = Selector.open();
// 绑定一个端口号
serverSocketChannel.bind(new InetSocketAddress(9001));
// 设置非阻塞式
serverSocketChannel.configureBlocking(false);
// 把ServerSocketChannel对象注册给Selector对象
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// 做事
for (;;){
// 监控客户端
if (selector.select(2000) == 0){
System.out.println("没有客户端连接我,我可以干别的事情");
continue;
}
// 得到SelectionKey,判断通道里的事件
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
// 如果是客户端连接事件
if (key.isAcceptable()){
System.out.println("连接事件");
// 接收连接
SocketChannel socketChannel = serverSocketChannel.accept();
// 设置非阻塞式
socketChannel.configureBlocking(false);
// 注册
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
// 读取客户端数据事件
if (key.isReadable()){
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
channel.read(buffer);
System.out.println("客户端发来数据:" + new String(buffer.array()));
}
// 手动从集合中移除当前key,防止重复处理
iterator.remove();
}
}
}
}
1、先启动服务端,看到下图内容,没有客户端连接的时候,NIO可以做别的事情,而不像BIO阻塞在那里,这就是NIO的优势。
2、启动客户端程序,服务端控制台可以看到下图内容,服务端接收到了客户端发送来的数据。(先连接,才能读写数据,所以最开始发生的事件一定是连接事件,连接成功才会有读写事件
)
先写到这里吧,都是些基础的内容,之后的文章会写比较复杂的NIO案例、Netty框架等!
结尾
如果你觉得我的文章对你有帮助话,欢迎关注我的微信公众号:"一个快乐又痛苦的程序员"(无广告,单纯分享原创文章、已pj的实用工具、各种Java学习资源,期待与你共同进步)