새로운 입력/출력 API 패키지인 Java NIO(새 입력/출력)는 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의 또 다른 특징은 애플리케이션 표현성에 중점을 두는 것입니다. 이는 다음 연습에서 볼 수 있습니다.
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의 변경 알림 메커니즘
다음과 같이 코드 코드를 복사합니다.
import 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(WatchEvent이벤트:이벤트){
System.out.println("누군가 방금 파일을 생성했습니다.'"+event.context().toString()+"'.");
}
}catch(예외){
System.out.println("오류:"+e.toString());
}
}
}
이 코드를 컴파일하고 명령줄에서 실행하세요. 동일한 디렉터리에서 새 파일을 만듭니다. 예를 들어 touchexample 또는 copyWatcher.classexample 명령을 실행합니다. 다음 변경 알림 메시지가 표시됩니다.
누군가 방금 필드 'example1'을 생성했습니다.
이 간단한 예는 JavaNIO 기능 사용을 시작하는 방법을 보여줍니다. 동시에 원래 I/O의 폴링 방식보다 더 직접적이고 사용하기 쉬운 NIO.2 Watcher 클래스도 도입되었습니다.
철자 오류에 주의하세요
이 문서의 코드를 복사할 때 철자 오류에 주의하세요. 예를 들어 목록 1의 StandardWatchEventKinds 개체는 복수형입니다. Java.net 문서에서도 철자가 잘못되었습니다.
팁
NIO의 알림 메커니즘은 이전 폴링 방법보다 사용하기가 더 간단하므로 특정 요구 사항에 대한 자세한 분석을 무시할 수 있습니다. 리스너를 처음 사용할 때는 사용하는 개념의 의미를 신중하게 고려해야 합니다. 예를 들어, 변경이 언제 끝날지 아는 것이 언제 시작하는지 아는 것보다 더 중요합니다. 이러한 종류의 분석은 특히 FTP 폴더 이동과 같은 일반적인 시나리오의 경우 매우 주의해야 합니다. NIO는 매우 강력한 패키지이지만 익숙하지 않은 사람들에게 혼란을 야기할 수 있는 몇 가지 미묘한 "문제점"도 있습니다.
2. 선택기 및 비동기식 IO: 선택기를 통한 다중화 개선
NIO를 처음 접하는 사람들은 일반적으로 이를 "비차단 입력/출력"과 연관시킵니다. NIO는 단순한 비차단 I/O 이상이지만 이러한 인식이 완전히 잘못된 것은 아닙니다. Java의 기본 I/O는 I/O를 차단합니다. 즉, 작업이 완료될 때까지 기다립니다. 그러나 비차단 또는 비동기 I/O는 NIO 전체가 아닌 NIO의 가장 일반적으로 사용되는 기능입니다.
NIO의 비차단 I/O는 이벤트 중심적이며 목록 1의 파일 시스템 수신 예제에 설명되어 있습니다. 이는 I/O 채널에 대한 선택기(콜백 또는 리스너)를 정의하면 프로그램이 계속 실행될 수 있음을 의미합니다. 이 선택기에서 이벤트가 발생하면(예: 입력 라인 수신) 선택기가 "깨어나서" 실행됩니다. 이 모든 것은 단일 스레드를 통해 구현되는데, 이는 Java의 표준 I/O와 크게 다릅니다.
목록 2는 NIO의 선택기를 사용하여 구현된 다중 포트 네트워크 프로그램 에코어를 보여줍니다. 이는 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=포트;
구성_선택기();
}
privatevoidconfigure_selector()throwsIOException{
//새 선택기 생성
선택기selector=Selector.open();
//각 포트에서 청취자를 열고 각각을 등록합니다.
//선택기로
for(inti=0;i<ports.length;++i){
ServerSocketChannelssc=ServerSocketChannel.open();
ssc.configureBlocking(false);
ServerSocketss=ssc.socket();
InetSocketAddressaddress=newInetSocketAddress(포트[i]);
ss.bind(주소);
SelectionKeykey=ssc.register(selector,SelectionKey.OP_ACCEPT);
System.out.println("Goingtolistenon"+ports[i]);
}
동안(참){
intnum=selector.select();
SetselectedKeys=selector.selectedKeys();
Iteratorit=selectedKeys.iterator();
동안(it.hasNext()){
SelectionKeykey=(SelectionKey)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("Gotconnectionfrom"+sc);
}elseif((key.readyOps()&SelectionKey.OP_READ)
==SelectionKey.OP_READ){
//데이터 읽기
SocketChannelsc=(SocketChannel)key.channel();
//에코데이터
intbytesEchoed=0;
동안(참){
echoBuffer.clear();
intnumber_of_bytes=sc.read(echoBuffer);
if(number_of_bytes<=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[portport...]");
시스템.종료(1);
}
intports[]=newint[args.length];
for(inti=0;i<args.length;++i){
ports[i]=Integer.parseInt(args[i]);
}
newMultiPortEcho(포트);
}
}
이 코드를 컴파일하고 javaMultiPortEcho80058006과 같은 명령으로 시작합니다. 이 프로그램이 성공적으로 실행되면 간단한 텔넷이나 기타 터미널 에뮬레이터를 시작하여 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("파일'"+fn+"'isnow"+Integer.toString(mem_map_size)+"bytesfull.");
//메모리 매핑 파일에서 읽습니다.
for(inti=0;i<30;i++){
System.out.print((char)out.get(i));
}
System.out.println("/nReadingfrommemory-mappedfile'"+fn+"'완료되었습니다.");
}
}
목록 3에서 이 간단한 예제는 20M 파일 example_memory_mapped_file.txt를 생성하고 이를 문자 A로 채운 다음 처음 30바이트를 읽습니다. 실제 응용 프로그램에서 메모리 매핑은 I/O의 원시 속도를 향상시키는 데 유용할 뿐만 아니라 여러 다른 판독기와 기록기가 동시에 동일한 파일 이미지를 처리할 수 있게 해줍니다. 이 기술은 강력하지만 위험하기도 합니다. 그러나 올바르게 사용하면 IO 속도가 몇 배로 향상됩니다. 우리 모두 알고 있듯이 월스트리트 거래 작업은 몇 초 또는 밀리초 내에 이점을 얻기 위해 메모리 매핑 기술을 사용합니다.
5.문자 인코딩 및 검색
이 글에서 설명하고 싶은 NIO의 마지막 기능은 다양한 문자 인코딩을 변환하는 데 사용되는 패키지인 charset입니다. NIO 이전에 Java는 getByte 메소드를 통해 내장된 동일한 기능의 대부분을 구현했습니다. charset은 getBytes보다 더 유연하고 더 낮은 수준에서 구현될 수 있어 더 나은 성능을 제공하므로 널리 사용됩니다. 이는 인코딩, 순서 및 기타 언어 기능에 민감한 비영어권 언어를 검색할 때 더욱 유용합니다.
목록 4는 Java의 유니코드 문자를 Latin-1로 변환하는 예를 보여줍니다.
목록 4. NIO의 문자
다음과 같이 코드 코드를 복사합니다.
Stringsome_string="이것은 자바적으로 유니코드를 저장하는 문자열입니다.";
Charsetlatin1_charset=Charset.forName("ISO-8859-1");
CharsetEncodelatin1_encoder=charset.newEncoder();
ByteBufferlatin1_bbuf=latin1_encoder.encode(CharBuffer.wrap(some_string));
메모리 매핑, 비동기 I/O, 인코딩 변환이 조화를 이룰 때 프로그램이 정상적으로 실행될 수 있도록 Charset과 채널은 함께 사용되도록 설계되었습니다.
요약: 물론 알아야 할 것이 더 있습니다.
이 기사의 목적은 Java 개발자에게 NIO 및 NIO.2의 가장 중요하고 유용한 기능 중 일부를 익히는 것입니다. 이러한 예제를 통해 확립된 기초를 사용하여 NIO의 다른 방법을 이해할 수 있습니다. 예를 들어 채널에 대해 배운 지식은 NIO 경로의 파일 시스템에서 기호 링크 처리를 이해하는 데 도움이 될 수 있습니다. Java의 새로운 I/O API에 대한 심층적인 연구를 위한 일부 문서를 제공하는 나중에 제공한 리소스 목록을 참조할 수도 있습니다.