阻塞VS非阻塞
阻塞
-
阻塞模式下,相关方法都会导致线程暂停。
-
ServerSocketChannel.accept() 会在没有建立连接的时候让线程暂停
-
SocketChannel.read()会在没有数据的时候让线程暂停。
-
阻塞的表现就是线程暂停了,暂停期间不会占用CPU,但线程相当于闲置。
-
-
单线程下,阻塞方法之间相互影响,几乎不能正常工作,需要多线程支持。
-
但多线程下又有新问题。
-
32 位 jvm 一个线程 320k,64 位 jvm 一个线程 1024k,如果连接数过多,必然导致 OOM,并且线程太多,反而会因为频繁上下文切换导致性能降低
-
可以采用线程池技术来减少线程数和线程上下文切换,但治标不治本,如果有很多连接建立,但长时间 inactive,会阻塞线程池中所有线程,因此不适合长连接,只适合短连接
-
服务器端
这个代码只能每次在连接到时候读取一次连接事件,进行遍历,其余事件阻塞,即使有其他的读写事件也没有反回应。
java
public class TestSocketChannel {
public static void main(String[] args) throws IOException {
//开启服务
ServerSocketChannel ssc = ServerSocketChannel.open();
//ban
ssc.bind(new InetSocketAddress(8080));
List<SocketChannel> channels = new ArrayList<>();
while (true) {
//等待建立连接
SocketChannel sc = ssc.accept();
channels.add(sc);
Iterator<SocketChannel> iterator = channels.iterator();
while (iterator.hasNext()) {
SocketChannel channel = iterator.next();
ByteBuffer buffer = ByteBuffer.allocate(10);
//读取数据
int len = channel.read(buffer);
ByteBufferUtil.debugAll(buffer);
buffer.clear();
log.debug("after read...{}", channel);
}
}
}
}
客户端
java
public class SocketChannelClient {
public static void main(String[] args) throws IOException {
SocketChannel sc = SocketChannel.open();
sc.connect(new InetSocketAddress("localhost", 8080));
System.out.println("waiting...");
}
}
非阻塞
-
非阻塞模式下,相关的方法都不会让线程暂停
-
在ServerSocketChannel.accept()在没有建立连接时,会返回null.
-
SocketChannel.read在没有数据可读时返回0,但线程不必阻塞,可以执行其他SocketChannel的read或者ServerSocketChannel.accept
-
写数据的时候,知识等待数据写入channel即可,无需等Channel通过网络把数据发出去。
-
-
非阻塞模式下,即使没有连接建立和可读数据,线程任然在不断运行,拜拜浪费CPU
-
数据复制过程中,线程实际还是阻塞的。(AIO改进的地方)
服务端代码
java
package com.aqiuo.socketchannel;
import com.aqiuo.buffer.ByteBufferUtil;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
@Slf4j
public class TestSocketChannel {
public static void main(String[] args) throws IOException {
//开启服务
ServerSocketChannel ssc = ServerSocketChannel.open();
//绑定端口
ssc.bind(new InetSocketAddress(8080));
ssc.configureBlocking(false); //非阻塞模式
//连接的集合
List<SocketChannel> channels = new ArrayList<>();
while (true) {
//等待建立连接非阻塞,线程还好继续向下运行,没有连接返回null
SocketChannel sc = ssc.accept();
if(sc!=null){
log.info("connected...{}",sc);
sc.configureBlocking(false);//非阻塞模式
channels.add(sc);
}
Iterator<SocketChannel> iterator = channels.iterator();
while (iterator.hasNext()) {
SocketChannel channel = iterator.next();
ByteBuffer buffer = ByteBuffer.allocate(10);
//非阻塞读取数据
//接受客户端发送的数据。没有读到数据返回0
int len = channel.read(buffer);
if(len>0){
buffer.flip();
ByteBufferUtil.debugAll(buffer);
buffer.clear();
log.info("after read...{}",channel);
}
}
}
}
}
多路复用
单线程可以搭配Selector完成对多个channel可读可写事件的监控,称之为多路复用
-
多路复用仅仅针对网络IO,普通文件IO无法利用多路复用。
-
如果不用Selector的非阻塞模式,线程大部分事件都在做无用功,而Selector能够保证。
-
有连接事件时采去连接
-
有可读事件才去读取。
-
有可写事件才去写入。
- 限于网络传输能力,Channel 未必时时可写,一旦 Channel 可写,会触发 Selector 的可写事件
-
Selector
创建
java
Selector selector = Selector.open();
绑定 Channel 事件
也称之为注册事件,绑定的事件 selector 才会关心
java
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, 绑定事件);
-
channel 必须工作在非阻塞模式
-
FileChannel 没有非阻塞模式,因此不能配合 selector 一起使用
-
绑定的事件类型可以有
-
connect - 客户端连接成功时触发
-
accept - 服务器端成功接受连接时触发
-
read - 数据可读入时触发,有因为接收能力弱,数据暂不能读入的情况
-
write - 数据可写出时触发,有因为发送能力弱,数据暂不能写出的情况
-
监听 Channel 事件
可以通过下面三种方法来监听是否有事件发生,方法的返回值代表有多少 channel 发生了事件
方法1,阻塞直到绑定事件发生
java
int count = selector.select();
方法2,阻塞直到绑定事件发生,或是超时(时间单位为 ms)
java
int count = selector.select(long timeout);
方法3,不会阻塞,也就是不管有没有事件,立刻返回,自己根据返回值检查是否有事件
java
int count = selector.selectNow();
💡 select 何时不阻塞
事件发生时
客户端发起连接请求,会触发 accept 事件
客户端发送数据过来,客户端正常、异常关闭时,都会触发 read 事件,另外如果发送的数据大于 buffer 缓冲区,会触发多次读取事件
channel 可写,会触发 write 事件
在 linux 下 nio bug 发生时
调用 selector.wakeup()
调用 selector.close()
selector 所在线程 interrupt