U čemu je Java NIO bolja od tradicionalnog IO?
Proveo sam nekoliko dana učeći osnove NIO-a. Tokom tog vremena pregledao sam NIO module u knjigama Thinking in Java i Crazy Java Lectures. Ali, i nakon čitanja sam i dalje bio prilično zbunjen — nisam tačno znao čemu NIO služi; gradivo na internetu i ono u knjigama se nisu dobro poklapali.
Prvo ću vam pokazati jednu sliku koja upoređuje tradicionalni IO i NIO, da steknete utisak.

Tradicionalni IO se zasniva na bajt ili znakovnim tokovima (poput FileInputStream, BufferedReader itd.) za čitanje i pisanje fajlova, i koristi Socket i ServerSocket za mrežni prenos.
NIO koristi kanale (Channel) i bafore (Buffer) za rad sa fajlovima, i SocketChannel i ServerSocketChannel za mrežni prenos.
Tradicionalni IO koristi blokirajući model: za svaku vezu potrebno je kreirati poseban nit koji obrađuje operacije čitanja i pisanja. Dok nit čeka na I/O operaciju, ne može izvršavati druge zadatke. To dovodi do masovnog kreiranja i uništavanja niti i čestih promena konteksta, što smanjuje performanse sistema.
NIO koristi neblokirajući model koji dozvoljava niti da izvršava druge zadatke dok čeka na I/O. Ovaj režim, pomoću selektora (Selector), nadgleda I/O događaje na više kanala (Channel) i na taj način postiže više performanse i veću skalabilnost.
01. Razlika između NIO i tradicionalnog IO pri radu sa fajlovima
U JDK-u 1.4, paket java.nio.* uveo je novu Java I/O biblioteku sa ciljem da poveća brzinu. U stvari, „stari" I/O paket je već ponovo implementiran korišćenjem NIO-a, tako da imamo koristi od njega i kada eksplicitno ne programiramo sa NIO-om.
- nio se može prevesti kao no-blocking io ili new io — svejedno je, oba značenja važe~
Kada sam u knjizi Thinking in Java pročitao da „čak i ako eksplicitno ne programiramo sa NIO-om, ipak imamo koristi od njega", to mi je prilično privuklo pažnju, pa evo: testiraćemo performanse kopiranja fajla pomoću NIO-a u poređenju sa tradicionalnim IO kopiranjem fajla.
public class SimpleFileTransferTest {
// Prenos fajla tradicionalnim I/O pristupom
private long transferFile(File source, File des) throws IOException {
long startTime = System.currentTimeMillis();
if (!des.exists())
des.createNewFile();
// Kreiranje ulaznog i izlaznog toka
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(source));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(des));
// Prenos podataka pomoću niza bajtova
byte[] bytes = new byte[1024 * 1024];
int len;
while ((len = bis.read(bytes)) != -1) {
bos.write(bytes, 0, len);
}
long endTime = System.currentTimeMillis();
return endTime - startTime;
}
// Prenos fajla pomoću NIO pristupa
private long transferFileWithNIO(File source, File des) throws IOException {
long startTime = System.currentTimeMillis();
if (!des.exists())
des.createNewFile();
// Kreiranje RandomAccessFile objekata za nasumični pristup
RandomAccessFile read = new RandomAccessFile(source, "rw");
RandomAccessFile write = new RandomAccessFile(des, "rw");
// Dobijanje kanala fajla (FileChannel)
FileChannel readChannel = read.getChannel();
FileChannel writeChannel = write.getChannel();
// Kreiranje i korišćenje ByteBuffer-a za prenos podataka
ByteBuffer byteBuffer = ByteBuffer.allocate(1024 * 1024);
while (readChannel.read(byteBuffer) > 0) {
byteBuffer.flip();
writeChannel.write(byteBuffer);
byteBuffer.clear();
}
// Zatvaranje kanala fajla
writeChannel.close();
readChannel.close();
long endTime = System.currentTimeMillis();
return endTime - startTime;
}
public static void main(String[] args) throws IOException {
SimpleFileTransferTest simpleFileTransferTest = new SimpleFileTransferTest();
File sourse = new File("[Filmovi-www.dygod.cn]Trainspotting-cd1.rmvb");
File des = new File("io.avi");
File nio = new File("nio.avi");
// Upoređivanje vremena prenosa fajla između tradicionalnog I/O i NIO-a
long time = simpleFileTransferTest.transferFile(sourse, des);
System.out.println(time + ": vreme običnog bajt toka");
long timeNio = simpleFileTransferTest.transferFileWithNIO(sourse, nio);
System.out.println(timeNio + ": NIO vreme");
}
}Da prvo objasnim ovaj kod: RandomAccessFile koji se javlja u njemu već smo ranije obradili. FileChannel je klasa iz Java NIO (New I/O) biblioteke koja pruža efikasan I/O rad sa fajlovima, podržava nasumični pristup fajlu i omogućava čitanje i pisanje na proizvoljnoj poziciji u fajlu.
Za razliku od RandomAccessFile, FileChannel koristi bafer (ByteBuffer) za prenos podataka.
Dobro, pre nego što iznesem stvarni zaključak, da li bi i ti doneo ovakav zaključak:
- Za manje fajlove, razlika u performansama između NIO-a i običnog IO-a možda neće biti previše izražena, jer je fajl sam po sebi mali pa je kopiranje brzo.
- Za veće fajlove, performanse NIO-a bi mogle biti primetno bolje od običnog IO-a, jer NIO koristi efikasniji mehanizam bafera i kanala, što omogućava brži prenos podataka u memoriji.
Međutim, stvarni rezultat će te iznenaditi:

Što je fajl veći, ispostavlja se da je običan bajt tok (tradicionalni IO) brži (naravno, ovo je lični test, možda nije sasvim precizan) — pa čemu onda uopšte NIO? Tim pre što je i cena učenja NIO-a nešto viša u poređenju sa tradicionalnim IO.
Znači li to da možemo da prestanemo da koristimo ili da učimo NIO?
Odgovor je negativan: IO operacije se najčešće koriste u dva scenarija:
- Fajl IO
- Mrežni IO
A čar NIO-a se pre svega ispoljava na mreži!
Cilj dizajna NIO-a (New I/O) jeste da reši usko grlo u performansama koje tradicionalni I/O (BIO, Blocking I/O) ima pri obradi velikog broja konkurentnih veza. Tradicionalni I/O u mrežnoj komunikaciji uglavnom koristi blokirajući I/O, dodeljujući po jedan nit svakoj vezi. Kada broj veza poraste, performanse sistema se ozbiljno narušavaju i resursi niti postaju ključno usko grlo. NIO, s druge strane, pruža neblokirajući I/O i I/O multipleksiranje, pa u okviru jednog nita može obraditi više konkurentnih veza, čime se u mrežnom prenosu značajno poboljšavaju performanse.
Evo razloga zbog kojih je NIO u mrežnom prenosu bolji od tradicionalnog I/O-a:
①. NIO podržava neblokirajući I/O, što znači da se pri izvršavanju I/O operacije nit ne blokira. To omogućava da se u mrežnom prenosu efikasno upravlja velikim brojem konkurentnih veza (na hiljade, čak i na milione). Pri radu sa fajlovima ova prednost nije toliko izražena, jer čitanje i pisanje fajlova obično ne uključuje mnogo konkurentnih operacija.
②. NIO podržava I/O multipleksiranje, što znači da jedan nit istovremeno može nadgledati više kanala (poput soketa) i obrađivati ih kada I/O događaj (npr. spremnost za čitanje ili pisanje) postane spreman. To znatno poboljšava performanse u mrežnom prenosu, jer jedan nit efikasno upravlja brojnim konkurentnim vezama. Pri radu sa fajlovima se ova prednost takođe ne može ispoljiti.
③. NIO pruža klasu ByteBuffer kojom se baferom upravlja na efikasan način. To je u mrežnom prenosu važno, jer se podaci najčešće prenose u obliku bajt toka. Pri radu sa fajlovima, iako takođe postoji bafer, prednost i dalje nije naročito izražena.
02. Razlika između NIO i tradicionalnog IO u mrežnom prenosu
Pogledajmo razliku u kodu na strani servera.
IO varijanta koristi sokete; kod je prilično jednostavan, pa neću dodavati komentare — već smo o tome učili, trebalo bi da je razumljivo. Sa while petljom osluškuje klijentske Socket-e:
public class IOServer {
public static void main(String[] args) {
try {
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
Socket client = serverSocket.accept();
InputStream in = client.getInputStream();
OutputStream out = client.getOutputStream();
byte[] buffer = new byte[1024];
int bytesRead = in.read(buffer);
out.write(buffer, 0, bytesRead);
in.close();
out.close();
client.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}NIO varijantu ću ovde dodati komentare; uglavnom se koriste ServerSocketChannel i Selector:
public class NIOServer {
public static void main(String[] args) {
try {
// Kreiranje ServerSocketChannel-a
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// Povezivanje na port
serverSocketChannel.bind(new InetSocketAddress(8081));
// Postavljanje u neblokirajući režim
serverSocketChannel.configureBlocking(false);
// Kreiranje Selector-a
Selector selector = Selector.open();
// Registrovanje ServerSocketChannel-a kod Selector-a, praćenje OP_ACCEPT događaja
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// Beskonačna petlja za obradu događaja
while (true) {
// Blokira dok se neki događaj ne desi
selector.select();
// Dohvatanje SelectionKey-eva za događaje koji su se desili
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
// Nakon obrade, uklanja se iz skupa selectedKeys
iterator.remove();
// Određivanje tipa događaja
if (key.isAcceptable()) {
// Stigao je novi zahtev za vezu
ServerSocketChannel server = (ServerSocketChannel) key.channel();
// Prihvatanje veze
SocketChannel client = server.accept();
// Postavljanje u neblokirajući režim
client.configureBlocking(false);
// Registrovanje novog SocketChannel-a kod Selector-a, praćenje OP_READ događaja
client.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
// Podaci su spremni za čitanje
SocketChannel client = (SocketChannel) key.channel();
// Kreiranje ByteBuffer bafera
ByteBuffer buffer = ByteBuffer.allocate(1024);
// Čitanje podataka iz SocketChannel-a i upis u ByteBuffer
client.read(buffer);
// Okretanje (flip) ByteBuffer-a, priprema za čitanje
buffer.flip();
// Upisivanje podataka iz ByteBuffer-a nazad u SocketChannel
client.write(buffer);
// Zatvaranje veze
client.close();
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}Gornji kod kreira jednostavan TCP server zasnovan na Java NIO-u. On pomoću ServerSocketChannel-a i Selector-a (biće reči o njima kasnije) implementira neblokirajući I/O i I/O multipleksiranje. Server u petlji osluškuje događaje: kada stigne novi zahtev za vezu, prihvata je i registruje novi SocketChannel kod Selector-a, prateći OP_READ događaj. Kada podaci postanu dostupni za čitanje, čita ih iz SocketChannel-a u ByteBuffer, a zatim ih iz ByteBuffer-a upisuje nazad u SocketChannel.
Ovde ukratko o Socket-u i ServerSocket-u, kao i o ServerSocketChannel-u i SocketChannel-u.
Socket i ServerSocket su tradicionalni, blokirajući I/O način programiranja, namenjen uspostavljanju i upravljanju TCP vezama.
- Socket: predstavlja klijentski soket; odgovoran je za uspostavljanje veze sa serverom i čitanje odnosno pisanje podataka.
- ServerSocket: predstavlja serverski soket; odgovoran je za osluškivanje klijentskih zahteva za vezu. Kada stigne novi zahtev, ServerSocket kreira novu instancu Socket-a koja komunicira sa klijentom.
U tradicionalnom blokirajućem I/O programiranju, svaka veza zahteva poseban nit za obradu, što dovodi do problema sa performansama u scenarijima visoke konkurentnosti — videćemo to u narednom testu klijenta.
Da bi rešio problem performansi blokirajućeg I/O-a, Java NIO uvodi ServerSocketChannel i SocketChannel. Oni su neblokirajući I/O i omogućavaju obradu više veza u okviru jednog nita.
- ServerSocketChannel: sličan ServerSocket-u, predstavlja serverski soket kanal. Osluškuje klijentske zahteve za vezu i može se prebaciti u neblokirajući režim, što znači da nit neće biti blokiran dok čeka na zahtev klijenta.
- SocketChannel: sličan Socket-u, predstavlja klijentski soket kanal; uspostavlja vezu sa serverom i čita odnosno piše podatke. I SocketChannel se može prebaciti u neblokirajući režim, tako da nit nije blokiran pri čitanju i pisanju podataka.
Još ukratko o Selector-u; detaljnije će biti reči kasnije.
Selector je ključna komponenta Java NIO-a, namenjena realizaciji I/O multipleksiranja. On omogućava da jedan nit istovremeno nadgleda više ServerSocketChannel-a i SocketChannel-a, a SelectionKey-em označava događaje od interesa. Kada se neki događaj desi, Selector dodaje odgovarajući SelectionKey u skup izabranih ključeva. Korišćenjem Selector-a, u okviru jednog nita se može istovremeno obraditi više veza, čime se efikasno poboljšavaju performanse I/O operacija — posebno u uslovima visoke konkurentnosti.
Test primer klijenta:
public class TestClient {
public static void main(String[] args) throws InterruptedException {
int clientCount = 10000;
ExecutorService executorServiceIO = Executors.newFixedThreadPool(10);
ExecutorService executorServiceNIO = Executors.newFixedThreadPool(10);
// Klijent koji koristi tradicionalni IO
Runnable ioClient = () -> {
try {
Socket socket = new Socket("localhost", 8080);
OutputStream out = socket.getOutputStream();
InputStream in = socket.getInputStream();
out.write("Hello, Chenmo Wang Er IO!".getBytes());
byte[] buffer = new byte[1024];
in.read(buffer);
in.close();
out.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
};
// Klijent koji koristi NIO
Runnable nioClient = () -> {
try {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost", 8081));
ByteBuffer buffer = ByteBuffer.wrap("Hello, Chenmo Wang Er NIO!".getBytes());
socketChannel.write(buffer);
buffer.clear();
socketChannel.read(buffer);
socketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
};
// Posebno testiranje performansi NIO i tradicionalnog IO servera
long startTime, endTime;
startTime = System.currentTimeMillis();
for (int i = 0; i < clientCount; i++) {
executorServiceIO.execute(ioClient);
}
executorServiceIO.shutdown();
executorServiceIO.awaitTermination(1, TimeUnit.MINUTES);
endTime = System.currentTimeMillis();
System.out.println("Tradicionalni IO server obradio " + clientCount + " klijenata za: " + (endTime - startTime) + "ms");
startTime = System.currentTimeMillis();
for (int i = 0; i < clientCount; i++) {
executorServiceNIO.execute(nioClient);
}
executorServiceNIO.shutdown();
executorServiceNIO.awaitTermination(1, TimeUnit.MINUTES);
endTime = System.currentTimeMillis();
System.out.println("NIO server obradio " + clientCount + " klijenata za: " + (endTime - startTime) + "ms");
}
}U ovom jednostavnom testu performansi koristimo fiksni bazen niti (10 niti) da simuliramo konkurentne zahteve klijenata. Posebno merimo vreme potrebno da NIO odnosno tradicionalni IO server obrade 10000 klijentskih zahteva. Pogledajmo rezultat.

Vidi se da je NIO otprilike dvostruko brži od tradicionalnog IO-a. Naravno, ovo je testirano na mojoj lokalnoj mašini; u produkcionom okruženju rezultat bi se donekle razlikovao, ali sam nekoliko puta ponovio test lokalno i ishod je uvek bio otprilike ovakav.
To pokazuje da su performanse NIO-a u mrežnom prenosu zaista bolje od tradicionalnog IO-a.
03. Kratak rezime
Ovaj članak govori o razlikama između NIO-a (New IO) i tradicionalnog IO-a, obuhvativši IO modele, rad sa fajlovima i mrežni prenos.
- Tradicionalni I/O koristi blokirajući model: nit za vreme I/O operacije ne može izvršavati druge zadatke. NIO koristi neblokirajući model koji dozvoljava niti da radi druge zadatke dok čeka na I/O; pomoću selektora (Selector) nadgleda I/O događaje na više kanala (Channel) i na taj način povećava performanse i skalabilnost.
- Tradicionalni I/O za čitanje i pisanje fajlova koristi klase zasnovane na bajt ili znakovnim tokovima (poput FileInputStream, BufferedReader itd.). NIO za rad sa fajlovima koristi kanale (Channel) i bafore (Buffer); prednost NIO-a u performansama ovde nije velika.
- Tradicionalni I/O za mrežni prenos koristi Socket i ServerSocket, što nosi problem blokiranja. NIO pruža SocketChannel i ServerSocketChannel koji podržavaju neblokirajući mrežni prenos i poboljšavaju sposobnost obrade konkurentnih zahteva.
