Java NIO (New Input/Output) - 新しい入出力 API パッケージ - は、2002 年に J2SE 1.4 で導入されました。 Java NIO の目標は、Java プラットフォーム上の I/O 集中型タスクのパフォーマンスを向上させることです。 10 年経った今でも、多くの Java 開発者は NIO を最大限に活用する方法を知りませんし、更新された入出力 API (NIO.2) が Java SE 7 で導入されたことを知っている人はさらに少なくなります。 Java プラットフォームに対する NIO および NIO.2 の最大の貢献は、Java アプリケーション開発のコア コンポーネントである入出力処理のパフォーマンスを向上させることです。ただし、どちらのパッケージもあまり有用ではなく、すべてのシナリオに適しているわけではありません。 Java NIO および NIO.2 を正しく使用すると、一部の一般的な I/O 操作にかかる時間を大幅に短縮できます。これが NIO と NIO.2 のスーパーパワーです。この記事では、これらを使用する 5 つの簡単な方法を紹介します。
変更通知 (すべてのイベントにリスナーが必要なため)
セレクターと非同期 IO: セレクターによる多重化の改善
チャンネル - 約束と現実
メモリマッピング - 刃には良質の鋼が使用されています
文字エンコーディングと検索
NIOの背景
10 年前から存在する拡張パッケージが Java の新しい I/O パッケージになるのはなぜですか?その理由は、ほとんどの Java プログラマにとって、基本的な I/O 操作で十分だからです。日常業務では、ほとんどの Java 開発者は NIO を学ぶ必要はありません。さらに一歩進めると、NIO は単なるパフォーマンス向上パッケージではありません。代わりに、これは Java I/O に関連するさまざまな関数のコレクションです。 NIO は、Java アプリケーションのパフォーマンスを「本質に近づける」ことでパフォーマンスの向上を実現します。これは、NIO と NIO.2 の API が低レベルのシステム操作への入り口を公開することを意味します。 NIO の代償として、より強力な I/O 制御機能が提供される一方で、基本的な I/O プログラミングよりも慎重に使用および練習する必要があることが挙げられます。 NIO のもう 1 つの特徴は、アプリケーションの表現力に焦点を当てていることです。これについては、次の演習で説明します。
NIO と NIO.2 の学習を開始する
NIO には多くの参考資料があり、参考資料の中のいくつかのリンクが選択されています。 NIO と NIO.2 を学習するには、Java 2 SDK Standard Edition (SE) ドキュメントと Java SE 7 ドキュメントが不可欠です。この記事のコードを使用するには、JDK 7 以降を使用する必要があります。
多くの開発者にとって、初めて NIO に遭遇するのは、アプリケーションを保守するときかもしれません。アプリケーションの動作がどんどん遅くなっているため、応答速度を向上させるために NIO の使用を提案する人もいます。 NIO はアプリケーションのパフォーマンスを向上させる点で優れていますが、具体的な結果は基盤となるシステムによって異なります (NIO はプラットフォームに依存することに注意してください)。 NIO を初めて使用する場合は、慎重に検討する必要があります。 NIO のパフォーマンス向上能力は、OS だけでなく、使用している JVM、ホストの仮想コンテキスト、大容量ストレージの特性、さらにはデータにも依存することがわかります。したがって、パフォーマンス測定の作業は比較的困難です。特に、システムにポータブルな展開環境がある場合は、特別な注意を払う必要があります。
上記の内容を理解したら、次は NIO と NIO.2 の 5 つの重要な機能を体験してみましょう。
1. 変更通知 (各イベントにはリスナーが必要なため)
NIO および NIO.2 に興味を持つ開発者に共通する懸念は、Java アプリケーションのパフォーマンスです。私の経験では、NIO.2 のファイル変更通知機能は、新しい入出力 API の中で最も興味深い (そして過小評価されている) 機能です。
エンタープライズ レベルのアプリケーションの多くは、次の状況で特別な処理を必要とします。
FTPフォルダーにファイルをアップロードする場合
構成内の定義が変更されたとき
ドラフト文書がアップロードされるとき
他のファイルシステムイベントが発生したとき
これらはすべて、変更通知または変更応答の例です。 Java (および他の言語) の以前のバージョンでは、ポーリングはこれらの変更イベントを検出する最良の方法でした。ポーリングは特殊な種類の無限ループです。ファイル システムまたは他のオブジェクトをチェックし、以前の状態と比較し、変化がない場合は、数百ミリ秒または 10 秒程度の間隔を置いてチェックを続けます。これが無限ループで続きます。
NIO.2 は、変更検出を実行するためのより良い方法を提供します。リスト 1 は簡単な例です。
リスト 1. NIO.2 の変更通知メカニズム
次のようにコードをコピーします。
java.nio.file.attribute.* をインポートします。
importjava.io.*;
importjava.util.*;
importjava.nio.file.Path;
importjava.nio.file.Paths;
importjava.nio.file.StandardWatchEventKinds;
importjava.nio.file.WatchEvent;
importjava.nio.file.WatchKey;
importjava.nio.file.WatchService;
importjava.util.List;
publicclassWatcher{
publicstaticvoidmain(String[]args){
Paththis_dir=Paths.get(".");
System.out.println("現在のディレクトリを監視中...");
試す{
WatchServicewatcher=this_dir.getFileSystem().newWatchService();
this_dir.register(watcher,StandardWatchEventKinds.ENTRY_CREATE);
WatchKeywatckKey=watcher.take();
List<WatchEvent<<64;>>events=watckKey.pollEvents();
for(WatchEventevent:events){
System.out.println("誰かがファイルを作成したところです'"+event.context().toString()+"'.");
}
}キャッチ(例外){
System.out.println("エラー:"+e.toString());
}
}
}
このコードをコンパイルし、コマンド ラインから実行します。同じディレクトリに新しいファイルを作成します。たとえば、touchexample または copyWatcher.classexample コマンドを実行します。次の変更通知メッセージが表示されます。
誰かが「example1」というファイルを作成してください。
この簡単な例は、JavaNIO 機能の使用を開始する方法を示しています。同時に、NIO.2 Watcher クラスも導入されています。これは、元の I/O のポーリング スキームよりも直接的で使いやすいものです。
スペルミスに注意してください
この記事のコードをコピーするときは、スペル ミスに注意してください。たとえば、リスト 1 の StandardWatchEventKinds オブジェクトは複数形です。 Java.net のドキュメントでも綴りが間違っています。
ヒント
NIO の通知メカニズムは、特定の要件の詳細な分析を無視する古いポーリング方法よりも簡単に使用できます。初めてリスナーを使用するときは、使用している概念のセマンティクスを慎重に検討する必要があります。たとえば、変化がいつ終わるかを知ることは、それがいつ始まるかを知ることよりも重要です。この種の分析は、特に FTP フォルダーの移動などの一般的なシナリオでは、非常に注意する必要があります。 NIO は非常に強力なパッケージですが、慣れていない人にとっては混乱を引き起こす可能性のある微妙な「落とし穴」もいくつかあります。
2. セレクターと非同期 IO: セレクターによる多重化の改善
NIO を初めて使用する人は一般に、NIO を「ノンブロッキング入出力」と関連付けます。 NIO は単なるノンブロッキング I/O ではありませんが、この認識は完全に間違っているわけではありません。Java の基本的な I/O はブロッキング I/O、つまり操作が完了するまで待機します。ただし、非ブロッキングまたは非同期 I/Oこれは NIO のすべての機能ではなく、NIO で最も一般的に使用される機能です。
NIO のノンブロッキング I/O はイベント駆動型であり、リスト 1 のファイル システム リスニングの例で実証されています。これは、I/O チャネルのセレクター (コールバックまたはリスナー) を定義することを意味し、その後プログラムは実行を継続できます。このセレクターでイベント (入力行の受信など) が発生すると、セレクターが「ウェイクアップ」して実行されます。これらはすべて単一のスレッドを通じて実装されますが、これは Java の標準 I/O とは大きく異なります。
リスト 2 は、NIO のセレクターを使用して実装されたマルチポート ネットワーク プログラムの echo-er を示しています。これは、2003 年に Greg Travis によって作成された小さなプログラムです (リソース リストを参照)。 Unix および Unix 類似システムは、長い間、Java ネットワークにおける高性能プログラミング モデルの優れた参照モデルである効率的なセレクターを実装してきました。
リスト 2.NIO セレクター
次のようにコードをコピーします。
importjava.io.*;
importjava.net.*;
importjava.nio.*;
importjava.nio.channels.*;
importjava.util.*;
publicclassMultiPortEcho
{
プライベートポート[];
privateByteBufferechoBuffer=ByteBuffer.allocate(1024);
publicMultiPortEcho(intports[])throwsIOException{
this.ports=ポート;
configure_selector();
}
privatevoidconfigure_selector()throwsIOException{
//新しいセレクターを作成する
Selectorselector=Selector.open();
//各ポートをリスナーとして開き、それぞれを登録します
//セレクターを使用して
for(inti=0;i<ports.length;++i){
ServerSocketChannelssc=ServerSocketChannel.open();
ssc.configureBlocking(false);
ServerSocketss=ssc.socket();
InetSocketAddressaddress=newInetSocketAddress(ports[i]);
ss.bind(アドレス);
SelectionKeykey=ssc.register(selector,SelectionKey.OP_ACCEPT);
System.out.println("Goingtolistenon"+ports[i]);
}
while(true){
intnum=selector.select();
SetselectedKeys=selector.selectedKeys();
Iteratorit=selectedKeys.iterator();
while(it.hasNext()){
選択キーキー=(選択キー)it.next();
if((key.readyOps()&SelectionKey.OP_ACCEPT)
==SelectionKey.OP_ACCEPT){
//新しい接続を受け入れます
ServerSocketChannelssc=(ServerSocketChannel)key.channel();
SocketChannelsc=ssc.accept();
sc.configureBlocking(false);
//新しい接続をセレクタに追加します
SelectionKeynewKey=sc.register(selector,SelectionKey.OP_READ);
it.remove();
System.out.println("接続元からの接続を取得しました"+sc);
}elseif((key.readyOps()&SelectionKey.OP_READ)
==SelectionKey.OP_READ){
//データを読み取る
SocketChannelsc=(SocketChannel)key.channel();
//エコーデータ
intbytesEchoed=0;
while(true){
echoBuffer.clear();
intnumber_of_bytes=sc.read(echoBuffer);
if(バイト数<=0){
壊す;
}
echoBuffer.flip();
sc.write(echoBuffer);
bytesEchoed+=number_of_bytes;
}
System.out.println("Echoed"+bytesEchoed+"from"+sc);
it.remove();
}
}
}
}
staticpublicvoidmain(Stringargs[])throwsException{
if(args.length<=0){
System.err.println("使用法:javaMultiPortEchoport[ポートポート...]");
System.exit(1);
}
intports[]=newint[args.length];
for(inti=0;i<args.length;++i){
ports[i]=Integer.parseInt(args[i]);
}
newMultiPortEcho(ポート);
}
}
このコードをコンパイルし、javaMultiPortEcho80058006 のようなコマンドで開始します。このプログラムが正常に実行されたら、単純な Telnet またはその他の端末エミュレータを起動して、8005 および 8006 インターフェイスに接続します。このプログラムは受け取ったすべての文字をエコーし、Java スレッドを通じてエコーしていることがわかります。
3. パッセージ: 約束と現実
NIO では、チャネルは読み取りと書き込みが可能な任意のオブジェクトを表すことができます。その役割は、ファイルとソケットの抽象化を提供することです。 NIO チャネルは一貫したメソッドのセットをサポートしているため、標準出力、ネットワーク接続、使用中のチャネルなど、エンコード時にさまざまなオブジェクトに特別な注意を払う必要はありません。チャネルのこの機能は、Java 基本 I/O のストリームから継承されます。ストリームはブロッキング IO を提供し、チャネルは非同期 I/O をサポートします。
NIO は、そのパフォーマンスの高さから推奨されることがよくありますが、より正確には応答時間の速さによって推奨されます。シナリオによっては、NIO のパフォーマンスが基本的な Java I/O よりも劣る場合があります。たとえば、小さなファイルの単純なシーケンシャル読み取りおよび書き込みの場合、単純にストリーミングによって達成されるパフォーマンスは、対応するイベント指向のチャネルベースのエンコード実装よりも 2 ~ 3 倍高速になる可能性があります。同時に、非多重チャネル、つまりスレッドごとに個別のチャネルは、同じスレッドにセレクターを登録する複数のチャネルよりもはるかに遅くなります。
ストリームとチャネルのどちらを使用するかを検討するときは、次の質問を自問してみてください。
読み書きするために必要な I/O オブジェクトはいくつありますか?
さまざまな I/O オブジェクトは連続しているのでしょうか、それともすべて同時に発生する必要があるのでしょうか?
I/O オブジェクトは短期間存続する必要がありますか、それともプロセスの存続期間全体にわたって存続する必要がありますか?
I/O は単一スレッドでの処理に適していますか? それとも複数の異なるスレッドでの処理に適していますか?
ネットワーク通信とローカル I/O は同じように見えますか、それとも異なるパターンですか?
このような分析は、ストリームとチャネルのどちらを使用するかを決定する際のベスト プラクティスです。覚えておいてください: NIO と NIO.2 は基本的な I/O に代わるものではなく、それを補完するものです。
4. メモリマッピング - 刃には良質の鋼が使用されています。
NIO での最も重要なパフォーマンスの向上はメモリ マッピングです。メモリ マッピングは、プログラムで使用されるファイルのセクションをメモリとして扱うシステム レベルのサービスです。
メモリ マッピングには、ここでは説明しきれないほど多くの潜在的な影響があります。より高いレベルでは、ファイル アクセスの I/O パフォーマンスをメモリ アクセスの速度に到達させることができます。多くの場合、メモリ アクセスはファイル アクセスよりも桁違いに高速です。リスト 3 は、NIO メモリ マップの簡単な例です。
リスト 3. NIO のメモリ マッピング
次のようにコードをコピーします。
importjava.io.RandomAccessFile;
importjava.nio.MappedByteBuffer;
importjava.nio.channels.FileChannel;
publicclassmem_map_example{
privatestaticintmem_map_size=20*1024*1024;
privatestaticStringfn="example_memory_mapped_file.txt";
publicstaticvoidmain(String[]args)throwsException{
RandomAccessFilememoryMappedFile=newRandomAccessFile(fn,"rw");
// ファイルをメモリにマッピングする
MappedByteBufferout=memoryMappedFile.getChannel().map(FileChannel.MapMode.READ_WRITE,0,mem_map_size);
//MemoryMappedFile への書き込み
for(inti=0;i<mem_map_size;i++){
out.put((バイト)'A');
}
System.out.println("File"+fn+"'isnow"+Integer.toString(mem_map_size)+"bytesfull.");
//メモリマップされたファイルから読み取ります。
for(inti=0;i<30;i++){
System.out.print((char)out.get(i));
}
System.out.println("/nメモリマップされたファイルからの読み取り'"+fn+"'は完了しました。");
}
}
リスト 3 のこの単純な例では、20M ファイル example_memory_mapped_file.txt を作成し、それに文字 A を入力して、最初の 30 バイトを読み取ります。実際のアプリケーションでは、メモリ マッピングは I/O の生の速度を向上させるだけでなく、複数の異なるリーダーとライターが同じファイル イメージを同時に処理できるようになります。このテクノロジーは強力であると同時に危険でもありますが、正しく使用すると IO 速度が数倍向上します。周知のとおり、ウォール街の取引業務では、数秒、場合によってはミリ秒で優位性を得るためにメモリ マッピング テクノロジが使用されています。
5.文字コードと検索
この記事で説明する NIO の最後の機能は、異なる文字エンコーディングを変換するために使用されるパッケージである charset です。 NIO が登場する前は、Java は getByte メソッドを通じて組み込みの同じ機能のほとんどを実装していました。 charset が人気があるのは、getBytes よりも柔軟性が高く、より低いレベルで実装できるため、パフォーマンスが向上するためです。これは、エンコード、順序付け、その他の言語機能に影響を受ける英語以外の言語を検索する場合にさらに役立ちます。
リスト 4 は、Java の Unicode 文字を Latin-1 に変換する例を示しています。
リスト4.NIOのキャラクター
次のようにコードをコピーします。
Stringsome_string="これは Java で Unicode を格納する文字列です。";
Charsetlatin1_charset=Charset.forName("ISO-8859-1");
CharsetEncodelatin1_encoder=charset.newEncoder();
ByteBufferlatin1_bbuf=latin1_encoder.encode(CharBuffer.wrap(some_string));
Charset とチャネルは一緒に使用されるように設計されているため、メモリ マッピング、非同期 I/O、およびエンコード変換が調整されている場合にプログラムが正常に実行できることに注意してください。
要約: もちろん、もっと知るべきことはあります
この記事の目的は、Java 開発者に NIO および NIO.2 の最も重要な (そして便利な) 機能のいくつかを理解してもらうことです。これらの例によって確立された基礎を使用して、NIO の他のメソッドを理解することができます。たとえば、チャネルについて学んだ知識は、NIO のパス内のファイル システムのシンボリック リンクの処理を理解するのに役立ちます。また、後で説明したリソース リストを参照することもできます。このリストには、Java の新しい I/O API を詳しく学習するためのドキュメントが含まれています。