我是陈皮,一个在互联网 Coding 的 ITer,微信搜索「陈皮的 JavaLib」第一时间阅读最新文章,回复【资料】,即可获得我精心整理的技术资料,电子书籍,一线大厂面试资料和优秀简历模板。
Redis 为何能支持高并发?
Redis 底层采用NIO中的多路IO复用的机制
,对多个不同的连接(TCP)实现 IO 复用,很好地支持高并发
,并且能实现线程安全
。
Redis 官方没有 windows 版本,只有 Linux 版本。
NIO 在不同的操作系统上实现的方式有所不同,在 Windows 操作系统使用select
实现轮训,而且还存在空轮训的情况,效率非常低。时间复杂度是为O(n)
。其次默认对轮训的数据有一定限制,所以难于支持上万的 TCP 连接。在 Linux 操作系统采用epoll
实现事件驱动回调,不会存在空轮训的情况,只对活跃的 socket 连接实现主动回调,这样在性能上有大大的提升,时间复杂度是为O(1)
。
Windows 操作系统是没有 epoll,只有 Linux 系统才有 epoll。
这就是为什么 nginx、redis 都能够非常好的支持高并发,最终都是 Linux 中的 IO 多路复用机制 epoll。
阻塞和非阻塞
阻塞和非阻塞通常形容多线程间的相互影响
。比如一个线程占用了临界区资源,那么其它所有需要这个资源的线程就必须在这个临界区中进行等待,等待会导致线程挂起。这种情况就是阻塞。此时,如果占用资源的线程一直不愿意释放资源,那么其它所有阻塞在这个临界区上的线程都不能工作。而非阻塞允许多个线程同时进入临界区。
阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。
BIO NIO AIO 概念
BIO(blocking IO)
:就是传统的 java.io 包
,它是基于流模型实现的,交互的方式是同步、阻塞方式,也就是说在读入输入流或者输出流时,在读写动作完成之前,线程会一直阻塞在那里,它们之间的调用是可靠的线性顺序。优点是代码比较简单、直观;缺点是 IO 的效率和扩展性很低,容易成为应用性能瓶颈。NIO(non-blocking IO)
:Java 1.4 引入的 java.nio 包
,提供了 Channel、Selector、Buffer 等新的抽象,可以构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层高性能的数据操作方式。AIO(Asynchronous IO)
:是 Java 1.7 之后引入的包,是 NIO 的升级版本,提供了异步非堵塞的 IO 操作方式,所以人们叫它 AIO(Asynchronous IO),异步 IO 是基于事件和回调机制
实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
NIO 讲解
我们知道,BIO 是阻塞式 IO,是面向于流传输也即是根据每个字节实现传输,效率比较低;而 NIO 是同步非阻塞式的,式面向于缓冲区的,它的亮点是IO多路复用
。我们可以这样理解 IO 多路复用,多路可以指有多个不同的 TCP 连接,复用是一个线程来维护多个不同的 IO 操作。所以它的好处是占用 CPU 资源非常小,而且线程安全。
NIO 核心组件
管道 channel:数据传输都是经过管道的。channel 都是统一注册到 Selector 上的。选择器 Selector:也可称为多路复用器。可以在单线程的情况下维护多个 Channel,也可以维护多个连接。
BIO 和 NIO 代码演示
传统的 BIO 阻塞式 Socket 过程:
先启动一个 Socket 服务端,此时控制台会输出开始等待接收数据中...
,并等待客户端连接。
package com.nobody;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @author Mr.nobody
* @Description
* @date 2020/7/4
*/
public class SocketTcpBioServer {
private static byte[] bytes = new byte[1024];
public static void main(String[] args) {
try {
// 创建ServerSocket
final ServerSocket serverSocket = new ServerSocket();
// 绑定监听端口号
serverSocket.bind(new InetSocketAddress(8080));
while (true) {
System.out.println("开始等待接收数据中...");
Socket accept = serverSocket.accept();
int read = 0;
read = accept.getInputStream().read(bytes);
String result = new String(bytes);
System.out.println("接收到数据:" + result);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
复制代码
再启动一个 Socket 客户端,先不进行输入。
package com.nobody;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
/**
* @author Mr.nobody
* @Description
* @date 2020/7/4
*/
public class ClientTcpSocket {
public static void main(String[] args) {
Socket socket = new Socket();
try {
// 与服务端建立连接
SocketAddress socketAddress = new InetSocketAddress(InetAddress.getLocalHost(), 8080);
socket.connect(socketAddress);
while (true) {
Scanner scanner = new Scanner(System.in);
socket.getOutputStream().write(scanner.next().getBytes());
}
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
复制代码
再启动另外一个 Socket 客户端 02,输入client02
。
package com.nobody;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
/**
* @author Mr.nobody
* @Description
* @date 2020/7/4
*/
public class ClientTcpSocket02 {
public static void main(String[] args) {
Socket socket = new Socket();
try {
// 与服务端建立连接
SocketAddress socketAddress = new InetSocketAddress(InetAddress.getLocalHost(), 8080);
socket.connect(socketAddress);
while (true) {
Scanner scanner = new Scanner(System.in);
socket.getOutputStream().write(scanner.next().getBytes());
}
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
复制代码
此时可以看到服务端没有接收到数据,因为 Socket 客户端 01 先连接,但是还未输入数据,所以服务端一直等待客户端 01 的输入,导致客户端 02 阻塞。
如果我们这时在客户端 01 输入 client01,服务端控制台显示如下,先输出客户端 01 的数据,完成后才能输出客户端 02 的数据。
当然,如果不想后连接的客户端不阻塞,可以使用多线程实现伪异步 IO,只需将服务端代码修改为如下:
public static void main(String[] args) {
try {
// 创建ServerSocket
final ServerSocket serverSocket = new ServerSocket();
// 绑定监听端口号
serverSocket.bind(new InetSocketAddress(8080));
while (true) {
System.out.println("开始等待接收数据中...");
Socket accept = serverSocket.accept();
new Thread(new Runnable() {
@Override
public void run() {
int read = 0;
try {
read = accept.getInputStream().read(bytes);
} catch (IOException e) {
e.printStackTrace();
}
String result = new String(bytes);
System.out.println("接收到数据:" + result);
}
}).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
复制代码
当然上面代码有个缺点是创建的线程会频繁创建和销毁,频繁进行 CPU 调度,并且也消耗内存资源,可使用线程池机制优化。
NIO 非阻塞式 Socket 过程:前面两个客户端代码不变,服务端代码如下:
package com.nobody.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
/**
* @author Mr.nobody
* @Description
* @date 2020/7/4
*/
public class NioServer {
private Selector selector;
public void iniServer() {
try {
// 创建管道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 设置管道为非阻塞
serverSocketChannel.configureBlocking(false);
// 将管道绑定到8080端口
serverSocketChannel.bind(new InetSocketAddress(8080));
// 创建一个选择器
this.selector = Selector.open();
// 将管道注册到选择器上,注册为SelectionKey.OP_ACCEPT事件,
// 当事件到达后,selector.select()会返回,否则改方法会一直阻塞。
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
e.printStackTrace();
}
}
public void listen() throws IOException {
System.out.println("服务端启动成功...");
// 轮询访问Selector
while (true) {
// 当事件到达后,selector.select()会返回,否则改方法会一直阻塞。
int select = selector.select(10);
// 没有发送消息,跳过
if (0 == select) {
continue;
}
// selector中选中的注册事件
Iterator<SelectionKey> iterator = this.selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
// 删除已选中的key,避免重复处理
iterator.remove();
if (key.isAcceptable()) { // 客户端连接事件
ServerSocketChannel server = (ServerSocketChannel) key.channel();
// 获得与客户端连接的管道
SocketChannel socketChannel = server.accept();
// 设置管道为非阻塞
socketChannel.configureBlocking(false);
// 与客户端连接后,为了能接收到客户端的消息,为管道设置可读权限
socketChannel.register(this.selector, SelectionKey.OP_READ);
} else if (key.isReadable()) { // 可读事件
// 创建读取数据的缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(512);
SocketChannel channel = (SocketChannel) key.channel();
channel.read(byteBuffer);
byte[] bytes = byteBuffer.array();
String msg = new String(bytes).trim();
System.out.println("服务端收到消息:" + msg);
ByteBuffer outByteBuffer = ByteBuffer.wrap(msg.getBytes(StandardCharsets.UTF_8));
// 回应消息给客户端
channel.write(outByteBuffer);
}
}
}
}
public static void main(String[] args) throws IOException {
NioServer nioServer = new NioServer();
nioServer.iniServer();
nioServer.listen();
}
}
复制代码
启动服务端,然后再启动两个客户端,两个客户端都不会阻塞。
评论