1. Asinxron IO ilə tanışlıq
Gəlin dərhal terminlərlə aydınlaşaq. Klassik (sinxron) IO-da siz oxu və ya yazma metodunu çağıranda icra axınınız (məsələn, proqramın əsas axını) dayanır və əməliyyat bitənədək gözləyir. Bu, sanki dostunuza zəng etmisiniz və o, dəstəyi qaldırmayana qədər sadəcə dayanıb telefona baxıb gözləməyiniz kimidir.
Asinxron IO (AIO) — oxu/yazma əməliyyatını sistemə həvalə etdiyiniz və öz işinizə davam etdiyiniz vaxtdır. Əməliyyat tamamlandıqda — sizə geri «zəng edəcəklər» (məsələn, callback-metodunuz çağırılacaq və ya nəticə Future vasitəsilə qaytarılacaq).
Harada lazımdır?
- Server tətbiqləri: disk «düşünərkən» axınları boş yerə sərf etməmək üçün.
- Böyük faylların kütləvi emalı: əsas axını bloklamamaq üçün.
- UI-li tətbiqlər: oxu/yazma zamanı interfeys «donmasın» deyə.
Təsəvvür edin ki, pitsa sifariş etmisiniz. Sinxron dünyada qapıda dayanıb kuryeri gözləyərdiniz. Asinxron dünyada isə öz işinizi görürsünüz, pitsa gələndə sizə zəng edirlər və deyirlər: «Pitsa buradadır!»
2. AsynchronousFileChannel icmalı
Java-da asinxron giriş/çıxış 7-ci versiyadan etibarən java.nio.channels paketində reallaşdırılıb. Əsas qəhrəman — AsynchronousFileChannel sinfidir.
Nələri bacarır?
- Fayla məlumatları asinxron şəkildə oxumaq və yazmaq.
- Bufferlərlə işləmək (ByteBuffer).
- Nəticəni əldə etməyin müxtəlif yanaşmaları: Future və ya CompletionHandler vasitəsilə.
- Hadisələrin işlənməsi üçün axın hovuzunu (ExecutorService) açıq şəkildə göstərməyə imkan verir.
Əsas metodlar
- read(ByteBuffer dst, long position): Future<Integer> qaytarır.
- read(ByteBuffer dst, long position, A attachment, CompletionHandler<Integer, ? super A> handler).
- write(ByteBuffer src, long position): Future<Integer> qaytarır.
- write(ByteBuffer src, long position, A attachment, CompletionHandler<Integer, ? super A> handler).
- static open(Path file, Set<OpenOption> options, ExecutorService executor, FileAttribute<?>... attrs) — kanalı açır.
İş variantları:
- Future vasitəsilə: əməliyyatı işə salırsınız və sonra onun bitməsini gözləyə bilərsiniz.
- CompletionHandler vasitəsilə: əməliyyat bitəndə (və ya xətaya düşəndə) çağırılacaq «handler» ötürürsünüz.
Faylın asinxron oxu/yazması üçün açılmasına nümunə
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
AsynchronousFileChannel channel = AsynchronousFileChannel.open(
Path.of("data.txt"),
StandardOpenOption.READ, StandardOpenOption.WRITE
);
Hadisələrin işlənməsi üçün axın hovuzunu açıq şəkildə göstərmək də olar:
import java.util.EnumSet;
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
ExecutorService executor = Executors.newFixedThreadPool(4);
AsynchronousFileChannel channel = AsynchronousFileChannel.open(
Path.of("data.txt"),
EnumSet.of(StandardOpenOption.READ, StandardOpenOption.WRITE),
executor
);
Maraqlı fakt:
Əgər ExecutorService göstərməsəniz, Java IO hadisələrini xidmət edən daxili axın hovuzu yaradacaq. Sadə tapşırıqlar üçün bu kifayətdir, amma server tətbiqlərində hovuzu özünüz idarə etmək daha yaxşıdır.
3. İcra axınları (ExecutorService) və onların rolu
Asinxron kanalla işləyəndə, səhnə arxasında Java sizin callback-lərinizi icra etməli və ya Future-u tamamlamalıdır. O bunu sehrlə deyil, xüsusi işçi axınları — executor servisi vasitəsilə edir.
Əgər öz axın hovuzunuzu ötürməsəniz, Java sadəcə daxili birini yaradır — adətən hər prosessora bir axın. Rahatdır, amma həmişə təhlükəsiz deyil. Axınların sayına, tapşırıqların prioritetinə və yükün paylanmasına özünüz nəzarət etmək istəyəndə, öz ExecutorService-inizi yaradıb onu open-ə ötürmək daha yaxşıdır.
Server tətbiqlərində bu xüsusilə vacibdir. Öz hovuzunuz olmadan gözlənilməz yük sıçrayışları almaq asandır — hamar iş əvəzinə server «boğula» bilər.
Nümunə:
ExecutorService pool = Executors.newFixedThreadPool(8);
AsynchronousFileChannel channel = AsynchronousFileChannel.open(
Path.of("huge.log"),
EnumSet.of(StandardOpenOption.READ),
pool
);
Hovuz seçiminin təsiri:
- Çox axın — daha çox paralellik, amma sistemə daha çox yük.
- Az axın — daha az eyni vaxtlı əməliyyat, amma daha az overhead.
- Minlərlə asinxron əməliyyat işə salırsınızsa, balans barədə düşünün!
4. Təcrübə: Asinxron fayl oxuma
Sinxron oxu (müqayisə üçün)
import java.nio.file.Files;
import java.nio.file.Path;
byte[] data = Files.readAllBytes(Path.of("input.txt"));
System.out.println("Oxunan bayt sayı: " + data.length);
Problem ondadır ki, axın fayl tam oxunana qədər gözləyir. Fayl böyükdürsə və ya disk ləng işləyirsə, proqram da «ləngiyir» — bu an digər hər şey pauzada qalır.
AsynchronousFileChannel və Future ilə asinxron oxu
import java.nio.channels.AsynchronousFileChannel;
import java.nio.ByteBuffer;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.concurrent.Future;
public class AsyncReadExample {
public static void main(String[] args) throws Exception {
Path path = Path.of("input.txt");
try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
ByteBuffer buffer = ByteBuffer.allocate(1024); // 1 KB oxuyuruq
Future<Integer> result = channel.read(buffer, 0);
// Paralel olaraq başqa işlər görmək olar!
System.out.println("Oxu işə salındı...");
// ... sonra nəticəni gözləyirik
int bytesRead = result.get(); // əməliyyat bitənədək axını bloklayır
System.out.println("Oxunan bayt sayı: " + bytesRead);
buffer.flip();
// Baytları sətirə çeviririk (əgər mətnsə)
byte[] data = new byte[bytesRead];
buffer.get(data, 0, bytesRead);
String text = new String(data);
System.out.println("Məzmun: " + text);
}
}
}
- channel.read(buffer, 0) — 0 mövqeyindən asinxron oxunu işə salır.
- Future<Integer> qaytarır, onu nəticəni gözləmək üçün istifadə etmək olar.
- Əməliyyat bitənədək digər işləri icra edə bilərsiniz.
- result.get() axını bloklayır, amma yalnız nəticə hələ hazır deyilsə.
CompletionHandler ilə asinxron oxu
(Ətraflısına növbəti mühazirədə baxacağıq, amma «dadına baxmaq» üçün...)
import java.nio.channels.AsynchronousFileChannel;
import java.nio.ByteBuffer;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.channels.CompletionHandler;
public class AsyncReadWithHandler {
public static void main(String[] args) throws Exception {
Path path = Path.of("input.txt");
try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer bytesRead, ByteBuffer buf) {
buf.flip();
byte[] data = new byte[bytesRead];
buf.get(data, 0, bytesRead);
String text = new String(data);
System.out.println("Asinxron oxundu: " + text);
}
@Override
public void failed(Throwable exc, ByteBuffer buf) {
System.err.println("Oxu xətası: " + exc.getMessage());
}
});
// Proqramın dərhal bitməsinə imkan vermirik (əks halda callback icra olunmaya bilər)
Thread.sleep(100); // Real tətbiqlərdə — daha yaxşısı latch, future və s. ilə sinxronizasiya etməkdir.
}
}
}
5. Faydalı nüanslar
Müqayisə: Asinxron vs Sinxron oxu
| Xüsusiyyət | Sinxron IO ( Files.readAllBytes ) | Asinxron IO ( AsynchronousFileChannel ) |
|---|---|---|
| Axını bloklayır | Bəli | Xeyr (əgər get() çağırmasanız) |
| Miqyaslanma | Aşağı | Yüksək |
| UI/serverlər üçün uyğundur | Xeyr | Bəli |
| Kodun mürəkkəbliyi | Sadə | Bir az daha mürəkkəb |
| Resursların idarə edilməsi | Sadə | Kanalı bağlamağı unutmaq olmaz! |
Asinxron IO-nun iş sxemi
sequenceDiagram
participant Main as Sizin axınınız
participant OS as Əməliyyat sistemi
participant Disk as Disk
Main->>OS: Asinxron oxunu işə salır (read)
OS->>Disk: Məlumatları oxuyur
Main->>Main: Başqa tapşırıqları yerinə yetirir
OS-->>Main: Bitməyi bildirir (Future/CompletionHandler)
Main->>Main: Nəticəni emal edir
6. AsynchronousFileChannel ilə işləyərkən tipik xətalar
Xəta №1: kanalı bağlamağı unutmaq.
AsynchronousFileChannel — bağlanmalı olan bir resursdur. Kanalı (channel.close() və ya try-with-resources) bağlamağı unutsaq, deskriptor sızıntıları və fayllara çıxış problemləri yarana bilər. Mümkün olan hər yerdə try-with-resources istifadə edin.
Xəta №2: əsas axında bloklayan get().
Əgər Future istifadə edir və əsas axında (məsələn, UI-tətbiqində) get() çağırırsınızsa, asinxron IO-nun mənası itir — axın yenə də gözləyəcək. Nəticəni gözləmək üçün CompletionHandler və ya ayrıca axından istifadə edin.
Xəta №3: ByteBuffer ilə səhv iş.
Buffera yazdıqdan sonra onu oxuya hazırlamaq üçün flip() etməyi unutmayın. Oxuduqdan sonra — təkrar istifadə edəcəksinizsə clear() və ya compact().
Xəta №4: xətaların emalını unutmaq.
Asinxron əməliyyatlar xətayla bitə bilər (məsələn, fayl tapılmadı, icazə yoxdur). Əgər istisnaları CompletionHandler-də emal etməsəniz və ya Future-də xətanı yoxlamasanız, proqram əməliyyatı «səssizcə» yerinə yetirməyəcək.
Xəta №5: nəzərə alınmayan paralellik.
Eyni kanalda bir neçə əməliyyatı eyni vaxtda işə salırsınızsa, kodunuzun axın təhlükəsiz olduğuna və bufferlər və fayl mövqeləri üzərində yarış problemlərinin yaranmadığına əmin olun.
GO TO FULL VERSION