阻塞与非阻塞:
阻塞:在处理一个请求时,服务器端会分配一个线程,在处理请求过程中并不是全程都会用到 CPU 的,阻塞式就是即使没有用到 CPU,请求也会占用线程 CPU 直到请求全部执行完成。
非阻塞:在请求未用到 CPU 时,当前线程会去执行其他操作,并且会轮询判断该请求是否需要用到 CPU 了,需要用到时再回来处理请求。
同步与异步:
同步:指多个线程按既定的流程执行,一段代码一段时间只会有一个线程执行。
异步:在发送请求后,会立刻返回,然后当前线程可以处理其他操作,而返回结果会在执行完成后通过回调函数的形式返回。
关于阻塞、非阻塞与同步、异步的联系:
阻塞与非阻塞侧重于 CPU 是否在执行当前操作过程中执行了其他操作,而同步与异步侧重于发送请求后是否立刻返回结果返回
。阻塞与非阻塞本质就是属于同步的范畴,因为其都是在等待一次返回的结果。为了更好的解释,改编一下
知乎上的一篇回答
:
阻塞就是打电话给图书馆,在询问书名后,店员寻找,
此时你是一直处于等待状态
,直到确定有无该书后通知你。
非阻塞就是在等待时你
先干其他事
,并且每隔一段时间再询问一下查看是否完成(主动寻求结果)。
同步就是电话未挂断,
直到通知你结果
,此期间你是否干其他事都行,但是不能挂电话(也就是不能获取到结果(同步代码的执行权))。
异步就是店主
先通知
你说有其他事,先挂电话,等他找到后
再通知你。
阻塞与非阻塞都是在等待第一次电话返回的结果,属于用户线程主动获取返回结果,所以其都是属于同步的,而异步是二次电话。
同步阻塞式 IO 就是接通电话后你就一直等待店主那边通知,期间你不做其他事。
同步非阻塞式 IO 就是接通电话你先干其他事,并且每隔一段时间询问店主是否完成。
异步 IO 就是店主先挂电话,随后你忙其他事,店主打来通知电话再来接。
由于异步是通过回调返回结果的,所以异步不存在阻塞非阻塞的概念,都是非阻塞的。
IO 类型
BIO:同步阻塞式 IO ,效率低下,因为是同步阻塞式的,所以一个线程只能处理一个线程请求,当一个连接内没有IO操作时还是会占用线程,这样会严重影响效率。且数据是以流形式进行传输,在高并发场景下效率极低。方向:输入流,输出流(
单向
)。适用场景:连接数少且连接时间长的架构。
NIO:
同步非阻塞式IO,一个线程可以处理多个连接请求。具体过程是当连接请求传来时,服务端会为其创建
通道
(Channel,双向),然后由
选择器
(Selector )进行判断选择需要IO操作的请求分配线程去执行,且数据是以
缓冲
(Buffer ,多组数据,双向)形式进行传输的。所以NIO是
双向
的。适用场景:连接数多且连接时间较短。
AIO:异步非阻塞式 IO。由于在 Linux 中其底层也是基于 epoll 实现的,所以其效率并没有比 NIO 高多少,再加上 Linux 对 aio 没有优化好,在一些场景中效率甚至比如 NIO ,所以目前使用的并不多。
NIO 三大组件
缓冲区Buffer
本质上就是一个可以读写数据的内存块,可以理解成是一个容器对象,底层包含了一个byte数组用于保存字节数据。也正是因为缓冲区的存在使得NIO有了异步这一特性,因为其他连接在进行IO操作时,可以将当前的IO操作数据暂存在缓冲区,下次再被selector侦测到进行数据传输(从 buffer 微观上看是异步的,但从宏观上总体过程还是同步的)
。
jdk 实现:
Buffer 本身是一个抽象类,一共有七种实现,分别是 IntBuffer、FloatBuffer、CharBuffer、DoubleBuffer、ShortBuffer、LongBuffer、ByteBuffer(常用),各自对应着各种的类型数组。
buffer及其实现类中有四个重要属性:1、capacity:能装载的最大容量 2、limit:缓冲区的当前最大终点位置,在 buffer 里填写的数据不能超过 limit 规定的值,可变。 3、position:下一个要读(写)元素的索引位置 4、mark:当前位置的标记
红色代表常用
clear():重置 buffer 底层的标记。一般在循环读取 buffer 的数据时调用,如果没有重置,那么在缓冲区容量刚好和读取数据容量一致时position与limit就会相等,那么下一次循环read就会等于0,从而死循环,一次性读完是-1
ByteBuffer
wrap:直接根据数据的字节数组大小来创建一个ByteBuffer
通道 Channel
channel 是 buffer 进行传输的通道,与 BIO 的流不同,它是双向的,既可以读,也可以写。在 JDK 中,channel 常见的实现类有
FileChannel(文件读写),DatagramChannel(UDP读写),ServerSocketChannel和SocketChannel(TCP读写)
常用方法,以 FileChannel 举例
注意:在 linux 环境中,执行一此 transferTo 方法就可以完成传输,因为传输数据大小是没有限制的;而在 windows 环境中 transferTo 方法一次性只能发送 8M,所以需要分段多次传输文件。
选择器 Selector
selector是用来处理多个客户端的连接,它会检测多个注册的通道上是否有
事件
发生(
多个Channel以事件的方式可以注册到同一个selector上(新连接进来也算是事件)
),如果有事件发生,便获取事件然后针对每个事件进行相应的操作,这样就可以达到只用一个单线程去管理多个通道,实现多路复用。
常用方法:
多路复用的原理:
1、服务器端启动时,会创建一个ServerSocketChannel对象(
这个对象需要注册到
selector
上
),每个客户端首先会创建一个SocketChannel对象,然后通过进行连接。
2、一个selector就管理多个连接,管理方式就是将这些连接生成的SocketChannel注册到selector上。
3、注册后会为这个连接返回一个SelectionKey,会和该Selector关联(集合方式)
4、selector进行监听(通过select方法),返回有事件发生的通道的个数。
5、进一步获取到有事件发生的连接的SelectionKey(通过Selector的selectedKeys方法返回SelectionKey类型的set集合)
6、通过SelectionKey反向获取SocketChannel(通过SelectionKey的channel方法)以及ByteBuffer
7、通过Channel对象进行IO操作
SelectionKey
的四种事件类型
1、SelectionKey.OP_ACCEPT —— 接收连接进行事件,表示服务器监听到了客户连接,服务器可以接收这个连接了(第一次连接时的事件)
2、SelectionKey.OP_CONNECT —— 连接就绪事件,表示客户与服务器的连接已经建立成功
3、SelectionKey.OP_READ —— 读就绪事件,表示通道中已经有了可读的数据,可以执行读操作了(通道目前有数据,可以进行读操作了)
4、SelectionKey.OP_WRITE —— 写就绪事件,表示已经可以向通道写数据了(通道目前可以用于写操作)
SelectionKey 相关方法:
selector.keys():获取当前selector所监听的所有连接对应的SelectionKey集合,
selector.selectedKeys():获取当前selector监听中含有事件的连接对应的SelectionKey集合
一个线程对应一个 Selector,一个 Selector 管理多个 Channel,一个 Channel 对应一个 Buffer,其中 Channel 与 Buffer 都是双向的,可以高效地进行 IO 操作。
首先要明确,
零拷贝并不是没有数据拷贝的发生,而是在拷贝过程中没有用到 CPU
,因为 CPU 是程序执行的重要资源,没有占用 CPU,就增强了程序的执行效率。零拷贝是在 Linux 环境下对 NIO IO 过程的一种优化。在 NIO 的 channel 实现类的 transferTo 方法就实现了零拷贝。
传统 IO
在传统 IO 过程中,会经历三次状态切换和四次拷贝
1、线程在用户空间发起 read() 方法,线程从用户态转成内核态
2、DMA将磁盘数据拷贝到内核缓存后,CPU又将数据从内核缓存拷贝到用户缓存,这时线程从内核态又切换为用户态
3、这时候知道了数据要往哪里写,CPU将数据从用户缓存拷贝至socket缓存,线程又从用户态切换为内核态
4、最后DMA将数据从内核缓存拷贝至协议栈,read()调用结束返回,线程又从内核态切换为用户态。
DMA :直接内存拷贝(不经过 CPU)
MMAP 优化
通过内存映射,使用mmap()函数将用户空间映射到内核缓冲区,用户空间共享内核空间的数据,所以在拷贝时就减少了上面第二步的cpu拷贝,直接从内核缓存拷贝到socket缓存,但是状态切换还是三次。
sendFile优化
Linux2.1引入了sendFile函数,直接摒弃了与用户空间的交互,相比于mmap减少一次状态的切换
linux2.4对sendFile函数做了一些修改,将内核缓存直接拷贝到协议栈,从而取消了仅有的一次CPU拷贝(还是有一次基本信息的socket缓存CPU拷贝的,但是消耗很低。)
mmap
与
sendFile
区别:
1、mmap适合小数据量读写,sendFile适合大文件传输
2、mmap需要4次上下文切换(这里算上了切换为初始状态),3次数据拷贝;sendFile需要3次上下文切换,最少2次数据拷贝(零拷贝优化)
3、sendFile可以利用DMA方式,减少CPU拷贝,mmap则不能(必须从内核拷贝到Socket缓冲区)
1、Put、Get 使用
ByteBuffer 支持类型化的 put 和 get,put 和 get 的顺序、类型必须一致,否则会抛出异常。
public class BufferPutGet {
public static void main(String[] args) {
//创建一个Buffer
ByteBuffer buffer = ByteBuffer.allocate(64);
//类型化方式放入数据
buffer.putInt(100);
buffer.putLong(9);
buffer.putChar('尚');
buffer.putShort((short) 4);
//取出前需要翻转
buffer.flip();
System.out.println();
System.out.println(buffer.getInt());
System.out.println(buffer.getLong());
System.out.println(buffer.getInt()); //当数据顺序类型匹配不上时就会抛出异常
System.out.println(buffer.getShort());
2、只读 Buffer
可以将普通的 buffer 转成只读 buffer。转成只读 buffer 后再 put 数据就会抛 ReadOnlyBufferException 异常。
public class BufferReadOnly {
public static void main(String[] args) {
//创建一个buffer
ByteBuffer buffer = ByteBuffer.allocate(64);
for(int i = 0; i < 64; i++) {
buffer.put((byte)i);
//读取
buffer.flip();
//得到一个只读的Buffer
ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer();
System.out.println(readOnlyBuffer.getClass());
//读取
while (readOnlyBuffer.hasRemaining()) {
System.out.println(readOnlyBuffer.get());
readOnlyBuffer.put((byte)100); //ReadOnlyBufferException
3、文件拷贝
public class NioFileChannelCopy {
//普通拷贝
@Test
public void test01() throws IOException {
FileInputStream fileInputStream = new FileInputStream("d://text01.txt");
FileChannel inputStreamChannel = fileInputStream.getChannel();
FileOutputStream fileOutputStream = new FileOutputStream("d://text02.txt");
FileChannel outputStreamChannel = fileOutputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
while(true){
byteBuffer.clear(); //重置buffer底层的标记,如果没有重置,那么在缓冲区容量刚好和读取数据容量一致时,position与limit就会相等,那么下一次循环read就会等于0,从而死循环
int read = inputStreamChannel.read(byteBuffer);
if(read==-1){
break ;
byteBuffer.flip();
outputStreamChannel.write(byteBuffer);
fileInputStream.close();
fileOutputStream.close();
//通过FileChannel的方法直接拷贝
@Test
public void test02() throws IOException {
FileInputStream fileInputStream = new FileInputStream("d://text01.txt");
FileChannel inputStreamChannel = fileInputStream.getChannel();
FileOutputStream fileOutputStream = new FileOutputStream("d://text03.txt");
FileChannel outputStreamChannel = fileOutputStream.getChannel();
//进行拷贝
outputStreamChannel.transferFrom(inputStreamChannel,0,inputStreamChannel.size());
outputStreamChannel.close();
inputStreamChannel.close();
fileOutputStream.close();
fileInputStream.close();
4、CS零拷贝测试
// 传统方式传输
public class OldIOClient {
public static void main(String[] args) throws Exception {
Socket socket = new Socket("localhost", 7001);
String fileName = "protoc-3.6.1-win32.zip";
InputStream inputStream = new FileInputStream(fileName);
DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream());
byte[] buffer = new byte[4096];
long readCount;
long total = 0;
long startTime = System.currentTimeMillis();
while ((readCount = inputStream.read(buffer)) >= 0) {
total += readCount;
dataOutputStream.write(buffer);
System.out.println("发送总字节数: " + total + ", 耗时: " + (System.currentTimeMillis() - startTime));
dataOutputStream.close();
socket.close();
inputStream.close();
public class OldIOServer {
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(7001);
while (true) {
Socket socket = serverSocket.accept();
DataInputStream dataInputStream = new DataInputStream(socket.getInputStream());
try {
byte[] byteArray = new byte[4096];
while (true) {
int readCount = dataInputStream.read(byteArray, 0, byteArray.length);
if (-1 == readCount) {
break;
} catch (Exception ex) {
ex.printStackTrace();
// 零拷贝
public class NewIOClient {
public static void main(String[] args) throws IOException {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost",7001));
String fileName="protoc-3.6.1-win32.zip";
//得到一个文件channel
FileChannel fileChannel = new FileInputStream(fileName).getChannel();
long startTime = System.currentTimeMillis();
//在linux下一个transferTo方法就可以完成传输
//但是在windows下一次调用transferTo 只能发送8M,就需要分段传输文件,而且要注意传输的位置
//transferTo方法底层实现了零拷贝
long transferCount = fileChannel.transferTo(0, fileChannel.size(), socketChannel);
System.out.println("发送的总字节数="+transferCount+"耗时:"+(System.currentTimeMillis()-startTime));
//关闭通道
fileChannel.close();
public class NewIOServer {
public static void main(String[] args) throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
ServerSocket serverSocket = serverSocketChannel.socket();
serverSocket.bind(new InetSocketAddress(7001));
//创建buffer
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
while(true){
SocketChannel socketChannel = serverSocketChannel.accept();
int readCount=0;
while(readCount!=-1){
readCount=socketChannel.read(byteBuffer);
byteBuffer.rewind();
5、综合聊天室
public class GroupChatClient {
private SocketChannel socketChannel;
private static final int PORT=7777;
private Selector selector;
private final String HOST="localhost";
private String name;
public GroupChatClient() {
try {
selector = Selector.open();
socketChannel=SocketChannel.open(new InetSocketAddress(HOST,PORT));
socketChannel.configureBlocking(false);
socketChannel.register(selector,SelectionKey.OP_READ);
name=socketChannel.getLocalAddress().toString();
System.out.println(name+"is OK ...");
} catch (IOException e) {
e.printStackTrace();
//向服务器发送消息
public void senMsg(String msg){
msg=name+"说:"+msg;
try {
socketChannel.write(ByteBuffer.wrap(msg.getBytes()));
} catch (IOException e) {
e.printStackTrace();
//读取消息
public void readMsg(){
try {
int selectCount = selector.select(); //阻塞等待连接
if(selectCount>0){
Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
while(keyIterator.hasNext()){
SelectionKey selectionKey = keyIterator.next();
if(selectionKey.isReadable()){
SocketChannel channel = (SocketChannel) selectionKey.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
channel.read(byteBuffer);
String msg=new String(byteBuffer.array());
System.out.println(msg.trim());
keyIterator.remove();
}else{
// System.out.println("没有新连接");
} catch (IOException e) {
e.printStackTrace();
public static void main(String[] args){
GroupChatClient groupChatClient = new GroupChatClient();
Scanner scanner = new Scanner(System.in);
new Thread() {
@Override
public void run() {
while (true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
groupChatClient.readMsg();
}.start();
while(true){
while(scanner.hasNext()){
String line = scanner.nextLine();
groupChatClient.senMsg(line);
public class GroupChatServer {
private Selector selector;
private ServerSocketChannel listenChannel;
private static final int PORT=7777;
public GroupChatServer(){
try {
selector=Selector.open();
listenChannel=ServerSocketChannel.open();
listenChannel.socket().bind(new InetSocketAddress(PORT));
listenChannel.configureBlocking(false);
listenChannel.register(selector,SelectionKey.OP_ACCEPT);
} catch (IOException e) {
e.printStackTrace();
public void listen() throws IOException {
while(true){
if(selector.select(1000)==0){
// System.out.println("没有事件");
continue;
Iterator<SelectionKey> keyIterator= selector.selectedKeys().iterator(); //获取有事件的selectionKeys集合对应的迭代器
while (keyIterator.hasNext()){
SelectionKey selectionKey = keyIterator.next();
if(selectionKey.isAcceptable()){ //如果当前事件是新连接事件
SocketChannel socketChannel = listenChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector,SelectionKey.OP_READ);
System.out.println(socketChannel.getRemoteAddress()+" 上线 ");
if(selectionKey.isReadable()){ //事件是read事件,即通道是可读的状态
getMsg(selectionKey);
keyIterator.remove();
public void getMsg(SelectionKey selectionKey){
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
try {
int count = socketChannel.read(byteBuffer);
if(count>0){ //如果读取的数据量不为0就输出并转发给其他客户端
String msg=new String(byteBuffer.array());
System.out.println("from客户端:"+msg);
sendToOthers(msg,socketChannel);
} catch (IOException e) {
try {
System.out.println(socketChannel.getRemoteAddress()+"离线了");
//取消注册
selectionKey.cancel();
//关闭通道
socketChannel.close();
} catch (IOException e1) {
e1.printStackTrace();
e.printStackTrace();
public void sendToOthers(String msg,SocketChannel socketChannel){
System.out.println("消息转发给客户端线程:"+Thread.currentThread().getName());
Iterator<SelectionKey> allKeyIterator = selector.keys().iterator();
while (allKeyIterator.hasNext()){
SelectionKey key = allKeyIterator.next();
Channel channel = key.channel();
if(channel instanceof SocketChannel && channel!=socketChannel){
SocketChannel channel1=(SocketChannel)channel;
ByteBuffer byteBuffer = ByteBuffer.wrap(msg.getBytes());
try {
channel1.write(byteBuffer);
} catch (IOException e) {
e.printStackTrace();
public static void main(String[] args){
GroupChatServer groupChatServer = new GroupChatServer();
try {
groupChatServer.listen();
} catch (IOException e) {
e.printStackTrace();