まず第一に、これらの概念は非常に混同されやすいですが、NIO にも関係しているため、まとめてみましょう [1]。
同期: API 呼び出しが返されると、呼び出し元は操作の結果 (実際に読み書きされたバイト数) を知ります。
非同期: 同期と比較すると、API 呼び出しが返されたときに呼び出し元は操作の結果を知りません。結果は後でコールバックによって通知されます。
ブロッキング: 読み取るデータがない場合、またはすべてのデータを書き込むことができない場合、現在のスレッドは一時停止されて待機します。
ノンブロッキング: 読み取り時は、できるだけ多くのデータを読み取ってから戻ります。書き込み時は、できるだけ多くのデータを書き込んでから戻ります。
Oracle の公式 Web サイトのドキュメントによると、I/O 操作の場合、同期と非同期の分類基準は「呼び出し元が I/O 操作の完了を待つ必要があるかどうか」です。 /O 操作を完了する」は、必ず完了する必要があるという意味ではありません。 データの読み取りまたはすべてのデータの書き込みは、TCP/IP プロトコル間でデータが送信されるときなど、I/O 操作が実際に実行されるときに呼び出し側が待機する必要があるかどうかを指します。スタックバッファとJVMバッファ。
つまり、一般的に使用される read() メソッドと write() メソッドは同期 I/O であり、データが検出されない場合、同期 I/O はブロッキング モードとノンブロッキング モードの 2 つのモードに分けられます。実際に I/O 操作を実行せずに直接戻ります。
要約すると、Java には実際には同期ブロッキング I/O、同期非ブロッキング I/O、および非同期 I/O の 3 つのメカニズムしかありません。これは JDK 1.7 で開始されたものだけです。 NIO.2 と呼ばれる非同期 I/O を導入します。
新しいテクノロジーの出現には常に改善と改善が伴うことはわかっていますが、Java NIO の出現にも同じことが当てはまります。
従来の I/O は I/O をブロックしており、主な問題はシステム リソースの無駄です。たとえば、TCP 接続のデータを読み取るために、InputStream の read() メソッドを呼び出します。これにより、データが到着するまで現在のスレッドが一時停止され、データが到着するまでの間、スレッドがメモリを占有します。リソース (ストレージ スレッド スタック) は何もしません。これは、よく言われるように、ピットを占有し、他の接続のデータを読み取るためには、別のスレッドを開始する必要があります。同時接続数が少ない場合は問題ないかもしれませんが、接続数がある程度の規模に達すると、大量のスレッドによってメモリリソースが消費されてしまいます。一方、スレッドの切り替えでは、プログラム カウンタやレジスタの値などのプロセッサのステータスを変更する必要があるため、多数のスレッドを頻繁に切り替えることもリソースの無駄になります。
テクノロジーの発展に伴い、最新のオペレーティング システムは、このリソースの無駄を回避するための新しい I/O メカニズムを提供します。これをもとにJava NIOが誕生しました。NIOの代表的な機能はノンブロッキングI/Oです。その後、ノンブロッキング I/O を使用するだけでは問題は解決しないことがわかりました。ノンブロッキング モードでは、データが読み込まれないとすぐに read() メソッドが返されるため、いつデータが到着するかがわかりません。再試行するには read() メソッドを呼び出し続けることしかできませんが、これは明らかに CPU リソースの無駄です。以下に示すように、Selector コンポーネントはこの問題を解決するために生まれました。
ストリーム操作が Stream オブジェクトに基づくのと同じように、Java NIO のすべての I/O 操作は Channel オブジェクトに基づいているため、最初に Channel とは何かを理解する必要があります。 。以下の内容は、JDK 1.8 のドキュメントからの抜粋です
チャネルは、1 つ以上の個別の I/ を実行できるハードウェア デバイス、ファイル、ネットワーク ソケット、またはプログラム コンポーネントなどのエンティティへのオープン接続を表します。 O 操作 (読み取りや書き込みなど)。
上記からわかるように、チャネルは特定のエンティティへの接続を表します。このエンティティはファイル、ネットワーク ソケットなどです。言い換えれば、チャネルは、プログラムがオペレーティング システムの基礎となる I/O サービスと対話するために Java NIO によって提供されるブリッジです。
チャネルは非常に基本的で抽象的な説明であり、さまざまな I/O サービスと対話し、さまざまな I/O 操作を実行し、さまざまな実装を備えているため、具体的なものには FileChannel、SocketChannel などが含まれます。
チャネルはストリームに似ており、バッファにデータを読み込んだり、バッファ内のデータをチャネルに書き込んだりできます。
もちろん、違いはありますが、主に次の 2 つの点に反映されています:
チャネルは読み取りと書き込みが可能ですが、ストリームは一方向です (つまり、InputStream と OutputStream に分割されます)
チャネル ノンブロッキング I/O モードがあります
Java NIO で最も一般的に使用されるチャネル実装は次のとおりです。これらは従来の I/O 操作クラスに 1 対で対応していることがわかります。 -1つ。
FileChannel: ファイルの読み取りと書き込み
DatagramChannel: UDP プロトコルのネットワーク通信
SocketChannel: TCP プロトコルのネットワーク通信
ServerSocketChannel: TCP 接続の監視
NIO で使用されるバッファは単純なバイト配列ではなく、カプセル化された Buffer クラスであり、以下で詳しく説明するように、提供される API を通じてデータを柔軟に操作できます。
NIOは、Javaの基本型に対応して、ByteBuffer、CharBuffer、IntBufferなどの様々なBuffer型を提供しています。違いは、バッファの読み書き時の単位長が異なることです(読み書きは変数単位で行われます)対応するタイプの)。
Buffer には 3 つの非常に重要な変数があり、これらは、
capacity (総容量)
position (ポインタの現在位置)
です。 limit (読み取り/書き込み境界位置)
Buffer は C 言語の文字配列と非常によく似ており、capacity は配列の全長、position は文字を読み取り/書き込みするための添字変数です。制限はターミネータです。 Buffer 内の 3 つの変数の初期状態は以下の通りです
Buffer の読み書きの過程で位置は後方に移動し、その限界が位置移動の境界になります。 Buffer への書き込み時には容量の大きさに制限を設定し、Buffer の読み取り時には実際のデータの終了位置に制限を設定する必要があることは想像に難くありません。 (注: バッファー データ をチャネルに書き込むことはバッファー 読み取り 操作であり、 データをチャンネル からバッファーに読み取ることはバッファー 書き込み 操作です。)
バッファーの読み取り/書き込みの前に、いくつかの呼び出しを行うことができます。位置と限界の値を正しく設定するために Buffer クラスによって提供される補助メソッドは次のとおりですFileChannel channel = new RandomAccessFile("test.txt", "rw").getChannel(); channel.position(channel.size()); // 移动文件指针到末尾(追加写入) ByteBuffer byteBuffer = ByteBuffer.allocate(20); // 数据写入Buffer byteBuffer.put("你好,世界!\n".getBytes(StandardCharsets.UTF_8)); // Buffer -> Channel byteBuffer.flip(); while (byteBuffer.hasRemaining()) { channel.write(byteBuffer); } channel.position(0); // 移动文件指针到开头(从头读取) CharBuffer charBuffer = CharBuffer.allocate(10); CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder(); // 读出所有数据 byteBuffer.clear(); while (channel.read(byteBuffer) != -1 || byteBuffer.position() > 0) { byteBuffer.flip(); // 使用UTF-8解码器解码 charBuffer.clear(); decoder.decode(byteBuffer, charBuffer, false); System.out.print(charBuffer.flip().toString()); byteBuffer.compact(); // 数据可能有剩余 } channel.close();
前文说了,如果用阻塞I/O,需要多线程(浪费内存),如果用非阻塞I/O,需要不断重试(耗费CPU)。Selector的出现解决了这尴尬的问题,非阻塞模式下,通过Selector,我们的线程只为已就绪的通道工作,不用盲目的重试了。比如,当所有通道都没有数据到达时,也就没有Read事件发生,我们的线程会在select()方法处被挂起,从而让出了CPU资源。
如下所示,创建一个Selector,并注册一个Channel。
注意:要将 Channel 注册到 Selector,首先需要将 Channel 设置为非阻塞模式,否则会抛异常。
Selector selector = Selector.open(); channel.configureBlocking(false); SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
register()方法的第二个参数名叫“interest set”,也就是你所关心的事件集合。如果你关心多个事件,用一个“按位或运算符”分隔,比如
SelectionKey.OP_READ | SelectionKey.OP_WRITE
这种写法一点都不陌生,支持位运算的编程语言里都这么玩,用一个整型变量可以标识多种状态,它是怎么做到的呢,其实很简单,举个例子,首先预定义一些常量,它们的值(二进制)如下
可以发现,它们值为1的位都是错开的,因此对它们进行按位或运算之后得出的值就没有二义性,可以反推出是由哪些变量运算而来。怎么判断呢,没错,就是“按位与”运算。比如,现在有一个状态集合变量值为 0011,我们只需要判断 “0011 & OP_READ” 的值是 1 还是 0 就能确定集合是否包含 OP_READ 状态。
然后,注意 register() 方法返回了一个SelectionKey的对象,这个对象包含了本次注册的信息,我们也可以通过它修改注册信息。从下面完整的例子中可以看到,select()之后,我们也是通过获取一个 SelectionKey 的集合来获取到那些状态就绪了的通道。
概念和理论的东西阐述完了(其实写到这里,我发现没写出多少东西,好尴尬(⊙ˍ⊙)),看一个完整的例子吧。
这个例子使用Java NIO实现了一个单线程的服务端,功能很简单,监听客户端连接,当连接建立后,读取客户端的消息,并向客户端响应一条消息。
需要注意的是,我用字符 ‘\0′(一个值为0的字节) 来标识消息结束。
public class NioServer { public static void main(String[] args) throws IOException { // 创建一个selector Selector selector = Selector.open(); // 初始化TCP连接监听通道 ServerSocketChannel listenChannel = ServerSocketChannel.open(); listenChannel.bind(new InetSocketAddress(9999)); listenChannel.configureBlocking(false); // 注册到selector(监听其ACCEPT事件) listenChannel.register(selector, SelectionKey.OP_ACCEPT); // 创建一个缓冲区 ByteBuffer buffer = ByteBuffer.allocate(100); while (true) { selector.select(); //阻塞,直到有监听的事件发生 Iterator<SelectionKey> keyIter = selector.selectedKeys().iterator(); // 通过迭代器依次访问select出来的Channel事件 while (keyIter.hasNext()) { SelectionKey key = keyIter.next(); if (key.isAcceptable()) { // 有连接可以接受 SocketChannel channel = ((ServerSocketChannel) key.channel()).accept(); channel.configureBlocking(false); channel.register(selector, SelectionKey.OP_READ); System.out.println("与【" + channel.getRemoteAddress() + "】建立了连接!"); } else if (key.isReadable()) { // 有数据可以读取 buffer.clear(); // 读取到流末尾说明TCP连接已断开, // 因此需要关闭通道或者取消监听READ事件 // 否则会无限循环 if (((SocketChannel) key.channel()).read(buffer) == -1) { key.channel().close(); continue; } // 按字节遍历数据 buffer.flip(); while (buffer.hasRemaining()) { byte b = buffer.get(); if (b == 0) { // 客户端消息末尾的\0 System.out.println(); // 响应客户端 buffer.clear(); buffer.put("Hello, Client!\0".getBytes()); buffer.flip(); while (buffer.hasRemaining()) { ((SocketChannel) key.channel()).write(buffer); } } else { System.out.print((char) b); } } } // 已经处理的事件一定要手动移除 keyIter.remove(); } } } }
这个客户端纯粹测试用,为了看起来不那么费劲,就用传统的写法了,代码很简短。
要严谨一点测试的话,应该并发运行大量Client,统计服务端的响应时间,而且连接建立后不要立刻发送数据,这样才能发挥出服务端非阻塞I/O的优势。
public class Client { public static void main(String[] args) throws Exception { Socket socket = new Socket("localhost", 9999); InputStream is = socket.getInputStream(); OutputStream os = socket.getOutputStream(); // 先向服务端发送数据 os.write("Hello, Server!\0".getBytes()); // 读取服务端发来的数据 int b; while ((b = is.read()) != 0) { System.out.print((char) b); } System.out.println(); socket.close(); } }
以上がJava の NIO コア コンポーネントの詳細な紹介の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。