Jedan članak koji temeljno i jasno objašnjava NIO, BIO i AIO u Javi
U prethodnom članku upoznali smo se sa razlikom između NIO-a i tradicionalnog IO-a; u ovom članku upoznaćemo se sa razlikom između BIO, NIO i AIO. Početnici se u to lako zapetljaju, pa ćemo pristupiti na drugačiji način — izložićemo to tako kao da to objašnjavamo devojci 👂.
Tokom jednog vikenda popodneva, kod kuće, vodio sam telefonski intervju. Postavio sam kandidatu nekoliko pitanja o IO-u, između ostalog šta su BIO, NIO i AIO, koja je razlika između njih i kako se konkretno koriste. Odgovori kandidata, međutim, nisu bili naročito zadovoljavajući. Tako sam u oceni intervjua zapisao: „dubljem razumevanju Java IO-a nije dorastao". To je, slučajno, videla i moja devojka.



Java IO naspram BIO i NIO
IO, često pisano i kao I/O, skraćenica je od Input/Output, odnosno ulaz/izlaz. Najčešće se odnosi na unos i izlaz podataka između unutrašnje memorije (RAM) i spoljne memorije (čvrsti disk, USB itd.) ili drugih perifernih uređaja.
Ulaz/izlaz je komunikacija između sistema za obradu informacija (npr. računara) i spoljnog sveta (to može biti čovek ili drugi sistem za obradu informacija).
Ulaz su signali ili podaci koje sistem prima, a izlaz su signali ili podaci koje sistem šalje.
U Javi je na raspolaganju čitav niz API-ja pomoću kojih programeri mogu čitati i pisati spoljne podatke ili fajlove. Te API-je nazivamo Java IO.
IO je u Javi prilično važna, ali i prilično teška tema, pre svega zato što se, sa razvojem Jave, danas istovremeno koriste tri vrste IO-a. To su BIO, NIO i AIO.
BIO, čije je puno ime Block-IO, predstavlja sinhroni i blokirajući model komunikacije. To je donekle tradicionalan način komunikacije — model je jednostavan i lagan za upotrebu, ali je sposobnost obrade konkurentnosti niska, komunikacija traje i zavisi od brzine mreže.
Java NIO, čije je puno ime Non-Block IO, nova je funkcija za optimizaciju mrežnog prenosa uvedena u Java SE 1.4. Predstavlja neblokirajuću i sinhronu komunikaciju.
NIO i originalni I/O imaju istu ulogu i svrhu; najvažnija razlika među njima jeste način na koji se podaci pakuju i prenose. Originalni I/O obrađuje podatke u obliku toka (stream), dok NIO obrađuje podatke u obliku bloka.
I/O sistem orijentisan na tok obrađuje podatke po jedan bajt istovremeno. Jedan ulazni tok proizvodi jedan bajt podataka, a jedan izlazni tok troši jedan bajt podataka.
I/O sistem orijentisan na blok obrađuje podatke u obliku blokova. Svaka operacija u jednom koraku proizvodi ili troši po jedan blok podataka. Obrada podataka po blokovima je znatno brža nego obrada po (tokovnim) bajtovima. Međutim, I/O orijentisan na blokove ima manje one elegantnosti i jednostavnosti koje ima I/O orijentisan na tokove.
Java AIO, čije je puno ime Asynchronous IO, jeste asinhroni i neblokirajući IO. Predstavlja neblokirajući, asinhroni model komunikacije.
Nadogradnjom na NIO uvodi se koncept asinhronih kanala i pružaju se implementacije asinhronog kanala fajlova i asinhronog kanala soketa.



Razlika između tri vrste IO
Da bismo stekli makro pregled, najpre još jednom istaknimo ključno:
BIO (Blocking I/O): sinhroni, blokirajući I/O model.
NIO (New I/O): sinhroni, neblokirajući model.
AIO (Asynchronous I/O): asinhroni, neblokirajući I/O model.
Sinhroni blokirajući model: u ovom modelu naš radni tok je — odlazimo u kuhinju, počinjemo da grejemo vodu i sedamo ispred rerne/najlona, tj. čekamo ispred čajnika dok voda ne prokuva.
Sinhroni neblokirajući model: u ovom modelu naš radni tok je — odlazimo u kuhinju, počinjemo da grejemo vodu, ali ne sedamo pred čajnikom i čekamo; umesto toga se vraćamo u dnevnu sobu da gledamo televiziju, a onda svakih nekoliko minuta odemo u kuhinju da proverimo da li je voda prokuvala.
Asinhroni neblokirajući I/O model: u ovom modelu naš radni tok je — odlazimo u kuhinju, počinjemo da grejemo vodu, ne čekamo ispred čajnika, ali ga ne idemo ni da povremeno proveravamo; gledamo televiziju u dnevnoj sobi, a čajnik ima prekidač koji nas obavesti čim voda prokuva.
Blokirajuće naspram neblokirajućeg: da li čovek sedi ispred čajnika i čeka čitavo vreme.
Sinhrono naspram asinhronog: da li čajnik sam, pošto voda prokuva, aktivno obaveštava čoveka.
Primenljivi scenariji
BIO pristup je pogodan za arhitekture sa relativno malim i fiksnim brojem veza; ovaj pristup postavlja veće zahteve prema resursima servera, a konkurentnost je ograničena na onu unutar same aplikacije. Bio je to jedini izbor pre JDK-a 1.4, ali je program intuitivan, jednostavan i lako razumljiv.
NIO pristup je pogodan za arhitekture sa velikim brojem veza koje su kraćeg trajanja (lake operacije), na primer za servere za ćaskanje (chat); konkurentnost je ograničena na onu unutar aplikacije, programiranje je složenije, a podrška postoji od JDK-a 1.4.
AIO pristup je pogodan za arhitekture sa velikim brojem veza koje su dužeg trajanja (teške operacije), na primer za servere foto-albuma; u potpunosti angažuje operativni sistem u konkurentnim operacijama, programiranje je složenije, a podrška postoji od JDK-a 7.
Način upotrebe
Korišćenje BIO-a za čitanje i pisanje fajla.
public class BioFileDemo {
public static void main(String[] args) {
BioFileDemo demo = new BioFileDemo();
demo.writeFile();
demo.readFile();
}
// Pisanje fajla pomoću BIO-a
public void writeFile() {
String filename = "logs/itwanger/paicoding.txt";
try {
FileWriter fileWriter = new FileWriter(filename);
BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);
bufferedWriter.write("Uči programiranje na Jishupaiju");
bufferedWriter.newLine();
System.out.println("Upisivanje završeno");
bufferedWriter.close();
fileWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
// Čitanje fajla pomoću BIO-a
public void readFile() {
String filename = "logs/itwanger/paicoding.txt";
try {
FileReader fileReader = new FileReader(filename);
BufferedReader bufferedReader = new BufferedReader(fileReader);
String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println("Pročitani sadržaj: " + line);
}
bufferedReader.close();
fileReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}Ovaj primer pokazuje kako se pomoću tradicionalnog, blokirajućeg I/O-a (BIO) u Javi vrše čitanje i pisanje nad fajlom. U metodi writeFile() najpre kreiramo objekat FileWriter i koristimo BufferedWriter za baferirano pisanje. Zatim metodom bufferedWriter.write() upisujemo string u fajl, nakon čega metodom bufferedWriter.newLine() dodajemo znak za novi red. Na kraju zatvaramo BufferedWriter i FileWriter. U metodi readFile() kreiramo objekat FileReader i koristimo BufferedReader za baferirano čitanje. Zatim u petlji čitamo sadržaj fajla pozivom bufferedReader.readLine(), sve dok ne dobijemo null, što znači da je čitanje završeno. Na kraju zatvaramo BufferedReader i FileReader.
Sledi NIO varijanta.
public class NioFileDemo {
public static void main(String[] args) {
NioFileDemo demo = new NioFileDemo();
demo.writeFile();
demo.readFile();
}
// Pisanje fajla pomoću NIO-a
public void writeFile() {
Path path = Paths.get("logs/itwanger/paicoding.txt");
try {
FileChannel fileChannel = FileChannel.open(path, EnumSet.of(StandardOpenOption.CREATE, StandardOpenOption.WRITE));
ByteBuffer buffer = StandardCharsets.UTF_8.encode("Uči programiranje na Jishupaiju");
fileChannel.write(buffer);
System.out.println("Upisivanje završeno");
fileChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
// Čitanje fajla pomoću NIO-a
public void readFile() {
Path path = Paths.get("logs/itwanger/paicoding.txt");
try {
FileChannel fileChannel = FileChannel.open(path, StandardOpenOption.READ);
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = fileChannel.read(buffer);
while (bytesRead != -1) {
buffer.flip();
System.out.println("Pročitani sadržaj: " + StandardCharsets.UTF_8.decode(buffer));
buffer.clear();
bytesRead = fileChannel.read(buffer);
}
fileChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}Ovaj primer demonstrira kako se pomoću NIO FileChannel-a vrše čitanje i pisanje nad fajlom. U metodi writeFile() najpre otvaramo kanal fajla i navodimo opcije za kreiranje i upisivanje. Zatim string koji želimo da upišemo pretvaramo u ByteBuffer, a potom metodom fileChannel.write() upisujemo ga u fajl. U metodi readFile() otvaramo kanal fajla i navodimo opciju čitanja, a zatim kreiramo ByteBuffer za smeštanje pročitanih podataka. Metodom fileChannel.read() u petlji čitamo sadržaj fajla, sve dok ne dobijemo -1, što znači da je čitanje završeno. U petlji okrenemo bafer (flip), dekodiramo ga u string i odštampamo, a zatim ga očistimo kako bismo bili spremni za sledeće čitanje. Na kraju zatvaramo kanal fajla.
Korišćenje AIO-a za čitanje i pisanje fajla.
public class AioDemo {
public static void main(String[] args) {
AioDemo demo = new AioDemo();
demo.writeFile();
demo.readFile();
}
// Pisanje fajla pomoću AsynchronousFileChannel-a
public void writeFile() {
// Dohvatanje putanje fajla pomoću Paths.get()
Path path = Paths.get("logs/itwanger/paicoding.txt");
try {
// Otvaranje kanala fajla pomoću AsynchronousFileChannel.open(), uz navođenje opcija za upisivanje i kreiranje fajla.
AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.WRITE, StandardOpenOption.CREATE);
// Pretvaranje stringa koji treba upisati ("Uči programiranje na Jishupaiju") u ByteBuffer.
ByteBuffer buffer = StandardCharsets.UTF_8.encode("Uči programiranje na Jishupaiju");
// Pozivom fileChannel.write() upisujemo sadržaj ByteBuffer-a u fajl. Pošto je ovo asinhrona operacija, koristimo Future objekat da sačekamo njen završetak.
Future<Integer> result = fileChannel.write(buffer, 0);
// Čekanje da se upisivanje završi
result.get();
System.out.println("Upisivanje završeno");
fileChannel.close();
} catch (IOException | InterruptedException | java.util.concurrent.ExecutionException e) {
e.printStackTrace();
}
}
// Čitanje fajla pomoću AsynchronousFileChannel-a
public void readFile() {
Path path = Paths.get("logs/itwanger/paicoding.txt");
try {
// Navođenje opcije za čitanje fajla.
AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.READ);
// Kreiranje ByteBuffer-a za smeštanje podataka pročitanih iz fajla.
ByteBuffer buffer = ByteBuffer.allocate(1024);
// Asinhrono čitanje podataka iz fajla pomoću fileChannel.read(). Metoda prihvata CompletionHandler objekat koji obrađuje povratni poziv nakon završetka asinhrone operacije.
fileChannel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
// U metodi completed() CompletionHandler-a prevrćemo ByteBuffer (attachment.flip()), a zatim ga pomoću Charset.forName("UTF-8").decode() dekodiramo u string i štampamo. Na kraju čistimo bafer i zatvaramo kanal fajla.
attachment.flip();
System.out.println("Pročitani sadržaj: " + StandardCharsets.UTF_8.decode(attachment));
attachment.clear();
try {
fileChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
// Ako asinhrono čitanje ne uspe, biće pozvana metoda failed() CompletionHandler-a, koja štampa poruku o grešci.
System.out.println("Čitanje nije uspelo");
exc.printStackTrace();
}
});
// Čekanje da se asinhrona operacija završi
Thread.sleep(1000);
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}Ovaj kod prikazuje klasu AioDemo sa dve metode: writeFile() i readFile(). Ove dve metode pomoću AsynchronousFileChannel-a asinhrono upisuju odnosno čitaju fajl. Specificno značenje koda sam dodao u komentare, pa obratite pažnju na njih.
Di-di-di, voda je prokuvala.


Kratak rezime
BIO (Blocking I/O): koristi blokirajući I/O model — nit se pri izvršavanju I/O operacije blokira i ne može obrađivati druge zadatke; pogodan je za scenarije sa manjim i stabilnim brojem veza.
NIO (New I/O ili Non-blocking I/O): koristi neblokirajući I/O model — nit dok čeka na I/O može raditi druge zadatke; pomoću Selector-a nadgleda događaje na više Channel-a, što poboljšava performanse i skalabilnost i pogodno je za scenarije visoke konkurentnosti.
AIO (Asynchronous I/O): koristi asinhroni I/O model — nit nakon slanja I/O zahteva odmah nastavlja dalje, a kada se operacija završi, dobija obaveštenje putem funkcije povratnog poziva, čime se dodatno povećava sposobnost obrade konkurentnosti i pogodno je za scenarije sa velikim protokom.
Referentni link: https://mp.weixin.qq.com/s/QQxrr5yP8X9YdFqIwXDoQQ
