java

位置:IT落伍者 >> java >> 浏览文章

IO的阻塞与非阻塞、同步与异步以及Java网络IO交互方式


发布日期:2019年04月20日
 
IO的阻塞与非阻塞、同步与异步以及Java网络IO交互方式

最近工作中接触到了Java网络编程方面的东西SocketNIOMongoDB等也看了tomcat的源码也加强了线程方面的知识也使用了MINA这样的框架感觉获益良多原本技术上的薄弱环节也在慢慢提高很多想写的东西也在慢慢规划整理无奈最近在筹备婚礼的事情显得有些耽搁

想了很久决定先写写IO中经常被提到的概念——同步与异步阻塞与非阻塞以及在Java网络编程中的简单运用

想达到的目的有两个

深入的理解同步与异步阻塞与非阻塞这看似烂大街的词汇很多人已经习惯不停的说但却说不出其中的所以然包括我

理解各种IO模型在Java网络IO中的运用能够根据不同的应用场景选择合适的交互方式了解不同的交互方式对IO性能的影响

前提

首先先强调上下文下面提到了同步与异步阻塞与非阻塞的概念都是在IO的场合下它们在其它场合下有着不同的含义比如操作系统中通信技术上

然后借鑒下《Unix网络编程卷》中的理论

IO操作中涉及的个主要对象为程序进程系统内核以读操作为例当一个IO读操作发生时通常经历两个步骤

等待数据准备

将数据从系统内核拷贝到操作进程中

例如在socket上的读操作步骤会等到网络数据包到达到达后会拷贝到系统内核的缓沖区步骤会将数据包从内核缓沖区拷贝到程序进程的缓沖区中

阻塞(blocking)与非阻塞(nonblocking)IO

IO的阻塞非阻塞主要表现在一个IO操作过程中如果有些操作很慢比如读操作时需要准备数据那么当前IO进程是否等待操作完成还是得知暂时不能操作后先去做别的事情?一直等待下去什么事也不做直到完成这就是阻塞抽空做些别的事情这是非阻塞

非阻塞IO会在发出IO请求后立即得到回应即使数据包没有准备好也会返回一个错误标识使得操作进程不会阻塞在那里操作进程会通过多次请求的方式直到数据准备好返回成功的标识

想象一下下面两种场景

A 小明和小刚两个人都很耿直内向一天小明来找小刚借书小刚啊你那本XXX借我看看 于是小刚就去找书小明就等着找了半天找到了把书给了小明

B 小明和小刚两个人都很活泼外向一天小明来找小刚借书嘿小刚你那本XXX借我看看 小刚说我得找一会小明就去打球去了过会又来这次书找到了把书给了小明

结论A是阻塞的B是非阻塞的

从CPU角度可以看出非阻塞明显提高了CPU的利用率进程不会一直在那等待但是同样也带来了线程切换的增加增加的 CPU 使用时间能不能补偿系统的切换成本需要好好评估

同步(synchronous)与异步(asynchronous)IO

先来看看正式点的定义POSIX标准将IO模型分为了两种同步IO和异步IORichard Stevens在《Unix网络编程卷》中也总结道

A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;

An asynchronous I/O operation does not cause the requesting process to be blocked;

可以看出判断同步和异步的标准在于一个IO操作直到完成是否导致程序进程的阻塞如果阻塞就是同步的没有阻塞就是异步的这里的IO操作指的是真实的IO操作也就是数据从内核拷贝到系统进程(读)的过程

继续前面借书的例子异步借书是这样的

C 小明很懒一天小明来找小刚借书嘿小刚你那本XXX借我看看 小刚说我得找一会小明就出去打球了并且让小刚如果找到了就把书拿给他小刚是个负责任的人找到了书送到了小明手上

A和B的借书方式都是同步的有人要问了B不是非阻塞嘛怎么还是同步?

前面说了IO操作的个步骤准备数据和把数据从内核中拷贝到程序进程映射到这个例子书即是准备的数据小刚是内核小明是程序进程小刚把书给小明这是拷贝数据在B方式中小刚找书这段时间小明的确是没闲着该干嘛干嘛但是小刚找到书把书给小明的这个过程也就是拷贝数据这个步骤小明还是得乖乖的回来候着小刚把书递手上所以这里就阻塞了根据上面的定义所以是同步

在涉及到 IO 处理时通常都会遇到一个是同步还是异步的处理方式的选择问题同步能够保证程序的可靠性而异步可以提升程序的性能小明自己去取书不管等着不等着迟早拿到书指望小刚找到了送来万一小刚忘了或者有急事忙别的了那书就没了

讨论

说实话网上关于同步与异步阻塞与非阻塞的文章多之又多大部分是拷贝的也有些写的非常好的参考了许多也借鑒了许多也经过自己的思考

同步与异步阻塞与非阻塞之间确实有很多相似的地方很容易混淆wiki更是把异步与非阻塞画上了等号更多的人还是认为他们是不同的原因可能有很多每个人的知识背景不同设定的上下文也不同

我的看法是在IO中根据上面同步异步的概念也可以看出来同步与异步往往是通过阻塞非阻塞的形式来表达的并且是通过一种中间处理机制来达到异步的效果同步与异步往往是IO操作请求者和回应者之间在IO实际操作阶段的协作方式而阻塞非阻塞更确切的说是一种自身状态当前进程或者线程的状态

在发出IO读请求后阻塞IO会一直等待有数据可读当有数据可读时会等待数据从内核拷贝至系统进程而非阻塞IO都会立即返回至于数据怎么处理是程序进程自己的事情无关同步和异步

两种方式的组合

组合的方式当然有四种分别是同步阻塞同步非阻塞异步阻塞异步非阻塞

Java网络IO实现和IO模型

不同的操作系统上有不同的IO模型《Unix网络编程卷》将unix上的IO模型分为blocking I/Ononblocking I/OI/O multiplexing (select and poll)signal driven I/O (SIGIO)以及asynchronous I/O (the POSIX aio_functions)具体可参考《Unix网络编程卷章节

在windows上IO模型也是有select WSAAsyncSelectWSAEventSelectOverlapped I/O 事件通知以及IOCP具体可参考windows五种IO模型

Java是平台无关的语言在不同的平台上会调用底层操作系统的不同的IO实现下面就来说一下Java提供的网络IO的工具和实现为了扩大阻塞非阻塞的直观感受我都使用了长连接

阻塞IO

同步阻塞最常用的一种用法使用也是最简单的但是 I/O 性能一般很差CPU 大部分在空闲状态下面是一个简单的基于TCP的同步阻塞的Socket服务端例子

@Test

public void testJIoSocket() throws Exception

{

ServerSocket serverSocket = new ServerSocket();

Socket socket = null;

try

{

while (true)

{

socket = serverSocketaccept();

Systemoutprintln(socket连接 + socketgetRemoteSocketAddress()toString());

BufferedReader in = new BufferedReader(new InputStreamReader(socketgetInputStream()));

while(true) {

String readLine = inreadLine();

Systemoutprintln(收到消息 + readLine);

if(endequals(readLine))

{

break; }

//客户端断开连接 socketsendUrgentData(xFF);

}

}

}

catch (SocketException se)

{ Systemoutprintln(客户端断开连接);

}

catch (IOException e)

{

eprintStackTrace();

}

finally

{

Systemoutprintln(socket关闭 + socketgetRemoteSocketAddress()toString());

socketclose();

}

}

使用SocketTest作为客户端工具进行测试同时开启个客户端连接Server端并发送消息如下图

再看下后台的打印

socket连接/:收到消息hello!收到消息my name is client

由于服务器端是单线程的在第一个连接的客户端阻塞了线程后第二个客户端必须等待第一个断开后才能连接当输入end字符串断开客户端这时候看到后台继续打印

socket连接/:收到消息hello!收到消息my name is client收到消息endsocket关闭/:socket连接/:收到消息hello!收到消息my name is client

所有的客户端连接在请求服务端时都会阻塞住等待前面的完成即使是使用短连接数据在写入 OutputStream 或者从 InputStream 读取时都有可能会阻塞这在大规模的访问量或者系统对性能有要求的时候是不能接受的

阻塞IO + 每个请求创建线程/线程池

通常解决这个问题的方法是使用多线程技术一个客户端一个处理线程出现阻塞时只是一个线程阻塞而不会影响其它线程工作为了减少系统线程的开销采用线程池的办法来减少线程创建和回收的成本模式如下图

简单的实现例子如下使用一个线程(Accptor)接收客户端请求为每个客户端新建线程进行处理(Processor)线程池的我就不弄了

public class MultithreadJIoSocketTest{

@Test

public void testMultithreadJIoSocket() throws Exception

{

ServerSocket serverSocket = new ServerSocket(

Thread thread = new Thread(new Accptor(serverSocket))

threadstart()

Scanner scanner = new Scanner(Systemin)

scannernext()

}

public class Accptor implements Runnable

{

private ServerSocket serverSocket;

public Accptor(ServerSocket serverSocket)

{

thisserverSocket = serverSocket;

}

public void run()

{

while (true)

{

Socket socket = null;

try

{

socket = serverSocketaccept()

if(socket != null)

{

Systemoutprintln(收到了socket: + socketgetRemoteSocketAddress()toString())

Thread thread = new Thread(new Processor(socket))

threadstart()

}

}

catch (IOException e)

{

eprintStackTrace()

}

}

}

}

public class Processor implements Runnable

{

private Socket socket;

public Processor(Socket socket)

{

thissocket = socket;

}

@Override

public void run()

{

try

{

BufferedReader in = new BufferedReader(new InputStreamReader(socketgetInputStream()))

String readLine;

while(true)

{

readLine = inreadLine()

Systemoutprintln(收到消息 + readLine)

if(endequals(readLine))

{

break;

}

//客户端断开连接

socketsendUrgentData(xFF)

Threadsleep(

}

}

catch (InterruptedException e)

{

eprintStackTrace()

}

catch (SocketException se)

{

Systemoutprintln(客户端断开连接

}

catch (IOException e)

{

eprintStackTrace()

}

finally {

try

{

socketclose()

}

catch (IOException e)

{

eprintStackTrace()

}

}

}

}}

使用个客户端连接这次没有阻塞成功的收到了个客户端的消息

收到了socket:/:收到了socket:/:收到消息hello!收到消息hello!

在单个线程处理中我人为的使单个线程read后阻塞就像前面说的出现阻塞也只是在单个线程中没有影响到另一个客户端的处理

这种阻塞IO的解决方案在大部分情况下是适用的在出现NIO之前是最通常的解决方案Tomcat里阻塞IO的实现就是这种方式但是如果是大量的长连接请求呢?不可能创建几百万个线程保持连接再退一步就算线程数不是问题如果这些线程都需要访问服务端的某些竞争资源势必需要进行同步操作这本身就是得不偿失的

非阻塞IO + IO multiplexing

Java从开始提供了NIO工具包这是一种不同于传统流IO的新的IO方式使得Java开始对非阻塞IO支持NIO并不等同于非阻塞IO只要设置Blocking属性就可以控制阻塞非阻塞至于NIO的工作方式特点原理这里一概不说以后会写模式如下图

下面是简单的实现

public class NioNonBlockingSelectorTest{

Selector selector;

private ByteBuffer receivebuffer = ByteBufferallocate(

@Test

public void testNioNonBlockingSelector()

throws Exception

{

selector = Selectoropen()

SocketAddress address = new InetSocketAddress(

ServerSocketChannel channel = ServerSocketChannelopen()

channelsocket()bind(address)

nfigureBlocking(false)

channelregister(selector SelectionKeyOP_ACCEPT)

while(true)

{

selectorselect()

Iterator iterator = selectorselectedKeys()iterator()

while (iteratorhasNext()) {

SelectionKey selectionKey = iteratornext()

iteratorremove()

handleKey(selectionKey)

}

}

}

private void handleKey(SelectionKey selectionKey) throws IOException

{

ServerSocketChannel server = null;

SocketChannel client = null;

if(selectionKeyisAcceptable())

{

server = (ServerSocketChannel)selectionKeychannel()

client = serveraccept()

Systemoutprintln(客户端 + clientsocket()getRemoteSocketAddress()toString())

nfigureBlocking(false)

clientregister(selector SelectionKeyOP_READ)

}

if(selectionKeyisReadable())

{

client = (SocketChannel)selectionKeychannel()

receivebufferclear()

int count = clientread(receivebuffer)

if (count > ) {

String receiveText = new String( receivebufferarray()count)

Systemoutprintln(服务器端接受客户端数据: + receiveText)

clientregister(selector SelectionKeyOP_READ)

}

}

}

}

Java NIO提供的非阻塞IO并不是单纯的非阻塞IO模式而是建立在Reactor模式上的IO复用模型在IO multiplexing Model中对于每一个socket一般都设置成为nonblocking但是整个用户进程其实是一直被阻塞的只不过进程是被select这个函数阻塞而不是被socket IO给阻塞所以还是属于非阻塞的IO

这篇文章中把这种模式归为了异步阻塞我其实是认为这是同步非阻塞的可能看的角度不一样

异步IO

Java中提供了异步IO的支持暂时还没有看过所以以后再讨论

网络IO优化

对于网络IO有一些基本的处理规则如下

减少交互的次数比如增加缓存合并请求

减少传输数据大小比如压缩后传输约定合理的数据协议

减少编码比如提前将字符转化为字节再传输

根据应用场景选择合适的交互方式同步阻塞同步非阻塞异步阻塞异步非阻塞

就说到这里吧感觉有点乱有些地方还是找不到更贴切的语言来描述

上一篇:用JAVA做的一个石头,剪刀,布的游戏

下一篇:Java Swing中的Accelerator Key