1. Çoxaxınlılıq nə vaxt kömək edir
Çoxaxınlılıq o zaman lazımdır ki, eyni vaxtda görülə bilən çoxlu iş olsun. Məsələn, onlarla faylı emal etmək — onları kopyalamaq, yenidən hesablamaq və ya analiz etmək — lazım olduqda, hər şeyi ardıcıl etməkdənsə, müxtəlif hissələri müxtəlif axınlara həvalə etmək daha asandır. Bu, bir dostunuz əvəzinə foto arxivinizə baxmaq üçün birdən beş dostu çağırmağa bənzəyir: iş daha sürətli və daha xoş gedəcək.
Bu, xüsusilə faylların paket halında emalında, böyük faylların hissə-hissə yüklənməsi və ya kopyalanmasında, yaxud məlumat oxunduqdan sonra müxtəlif hissələr üçün nəyinsə paralel hesablanmasının lazım olduğu hallarda faydalıdır.
Amma çoxaxınlılıq həmişə kömək etmir. Bir kiçik faylınız varsa, onun üçün onlarla axın işə salmağın mənası yoxdur. Disk və ya şəbəkə onsuz da son həddə yüklənibsə, yeni axınlar prosesi yalnız ləngidəcək. Və əgər bir neçə axın eyni fayla eyni vaxtda sinxronizasiya olmadan yazarsa — korlanmış məlumatlarla əsl xaos əldə edə bilərsiniz.
Sadə dillə desək, çoxaxınlılıq bir alətdir. Mismar üçün çəkic kimidir: onunla həm mismarı vura bilərsiniz, həm də barmağınıza. Əsas odur ki, onu nə zaman və necə istifadə etməyi biləsiniz.
2. Java-da çoxaxınlı IO üçün alətlər
Artıq bilirsiniz ki, Java-da tapşırıqları paralel işə salmağın bir neçə yolu var:
- Klassik Thread — axınların əl ilə yaradılması.
- ExecutorService vasitəsilə axın hovuzu — müasir, çevik və rahat üsul.
- CompletableFuture və paralel stream-lər (Stream API) — qabaqcıl tapşırıqlar üçün (onları növbəti mühazirələrdə daha ətraflı nəzərdən keçirəcəyik).
Ən sadəsindən başlayaq: bir neçə faylın müxtəlif axınlarda emalı.
Nümunə 1: Klassik Thread
public class FileCopyTask extends Thread {
private final Path source;
private final Path target;
public FileCopyTask(Path source, Path target) {
this.source = source;
this.target = target;
}
@Override
public void run() {
try {
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
System.out.println("Fayl kopyalandı: " + source);
} catch (IOException e) {
System.err.println("Kopyalama xətası " + source + ": " + e.getMessage());
}
}
}
// Bir neçə kopyanın ayrı axınlarda işə salınması
List<Path> filesToCopy = List.of(
Path.of("log1.txt"), Path.of("log2.txt"), Path.of("log3.txt")
);
for (Path file : filesToCopy) {
new FileCopyTask(file, Path.of("backup_" + file.getFileName())).start();
}
Üstünlüklər: Sadə və anlaşılandır.
Çatışmazlıqlar: Çoxlu axınları əl ilə idarə etmək əlverişsizdir, eyni vaxtda işləyən axınların sayına nəzarət yoxdur.
Nümunə 2: ExecutorService — axın hovuzu
ExecutorService tapşırıqları axın hovuzuna həvalə etməyə imkan verir; hovuz özü eyni vaxtda nə qədər axın istifadə olunacağını müəyyənləşdirir.
import java.util.concurrent.*;
public class MultiFileCopier {
public static void main(String[] args) throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(4); // maksimum 4 axın
List<Path> filesToCopy = List.of(
Path.of("log1.txt"), Path.of("log2.txt"), Path.of("log3.txt")
);
for (Path file : filesToCopy) {
executor.submit(() -> {
try {
Files.copy(file, Path.of("backup_" + file.getFileName()), StandardCopyOption.REPLACE_EXISTING);
System.out.println("Kopyalandı: " + file);
} catch (IOException e) {
System.err.println("Xəta: " + file + " " + e.getMessage());
}
});
}
executor.shutdown(); // daha tapşırıqlar qəbulu yoxdur
executor.awaitTermination(1, TimeUnit.MINUTES); // bütün tapşırıqların bitməsini gözləyirik
}
}
Üstünlüklər:
- Asan miqyaslana bilir (lazımi axın sayını təyin etmək mümkündür).
- Tapşırıqların tamamlanmasına nəzarət rahatdır (shutdown(), awaitTermination(...) metodları).
- Yüzlərlə və minlərlə faylın emalı üçün uyğundur.
3. Çoxaxınlı IO-nun problemləri və məhdudiyyətləri
Resurslar uğrunda rəqabət
Əgər eyni faylı eyni vaxtda bir neçə axından sinxronizasiya olmadan oxumağa və ya yazmağa cəhd etsəniz — nəticə qeyri-müəyyən olacaq. Bu, iki adamın eyni kitab səhifəsinə eyni vaxtda yazmasına bənzəyir: qarışıqlıq alınacaq. Koordinasiya üçün, məsələn, synchronized, aşkar bloklamalar və ya ayrıca yazı axınından istifadə edin.
Fayl sistemi və OS məhdudiyyətləri
- Bütün fayl sistemləri eyni fayla eyni vaxtda yazını yaxşı dəstəkləmir.
- Əməliyyat sistemi eyni vaxtda açıq faylların sayını məhdudlaşdıra bilər.
- Sərt disk (xüsusilə HDD) çoxsaylı təsadüfi müraciətlər zamanı pis işləyir.
Ümumi resursa yazarkən sinxronizasiya
Bir neçə axın eyni fayla (məsələn, log) yazırsa, mütləq sinxronizasiya istifadə edin (məsələn, synchronized, bloklamalar və ya xüsusi yazı axınları vasitəsilə).
Kiçik fayllarda qeyri-effektivlik
Kiçik fayllar üçün axınların yaradılması və onlar arasında keçid üzrə əlavə xərclər paralelləşmədən əldə edilən qazancı üstələyə bilər.
4. Praktik nümunələr
Faylların paralel kopyalanması
Tutaq ki, arxiv qovluğuna kopyalanmalı çoxlu log fayllarımız var.
import java.nio.file.*;
import java.util.List;
import java.util.concurrent.*;
public class ParallelFileCopier {
public static void main(String[] args) throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(4);
List<Path> filesToCopy = List.of(
Path.of("log1.txt"), Path.of("log2.txt"), Path.of("log3.txt")
// ... istədiyiniz qədər fayl əlavə edin
);
for (Path file : filesToCopy) {
executor.submit(() -> {
try {
Path target = Path.of("archive", file.getFileName().toString());
Files.copy(file, target, StandardCopyOption.REPLACE_EXISTING);
System.out.println("Kopyalandı: " + file);
} catch (IOException e) {
System.err.println("Xəta: " + file + " " + e.getMessage());
}
});
}
executor.shutdown();
executor.awaitTermination(10, TimeUnit.MINUTES);
}
}
Şərh:
- 4 axından ibarət hovuzdan istifadə edirik — bu, adətən diski yükləmək üçün yetərlidir, sistemi isə həddən artıq yükləmir.
- 1000 fayl üçün hovuzu 8-ə qədər artırmaq olar, amma onu həddən artıq böyütməyə dəyməz.
Stream API ilə fayl sətirlərinin paralel emalı
Java 8-dən etibarən fayl məzmununun emalı üçün paralel stream-lərdən istifadə etmək mümkündür:
import java.nio.file.*;
import java.io.IOException;
public class ParallelLineProcessing {
public static void main(String[] args) throws IOException {
Path path = Path.of("biglog.txt");
// Files.lines Stream<String> qaytarır — fayl sətirlərinin axını
Files.lines(path)
.parallel() // paralel axına çeviririk
.filter(line -> line.contains("ERROR"))
.forEach(line -> System.out.println("Xəta: " + line));
}
}
Vacibdir:
- Paralel stream-lər emalı sürətləndirir, əgər o, çox hesablamalar tələb edirsə (CPU-bound), oxuma prosesinin özünü (IO-bound) isə yox.
- Sətir emalı sadədirsə (məsələn, sadəcə System.out.println ilə çap), artım ola bilməz.
Böyük faylın müxtəlif hissələrinin oxunması/yazılması
Java müxtəlif mövqelər üzrə FileChannel və mövqe metodları vasitəsilə eyni faylın müxtəlif hissələrini eyni vaxtda oxumağa və ya yazmağa imkan verir. Bu artıq qabaqcıl səviyyədir, amma prinsip sadədir: hər axın faylın öz hissəsi ilə işləyir.
import java.nio.channels.FileChannel;
import java.nio.file.*;
import java.io.*;
import java.nio.ByteBuffer;
public class FileChunkReader implements Runnable {
private final Path path;
private final long position;
private final long size;
public FileChunkReader(Path path, long position, long size) {
this.path = path;
this.position = position;
this.size = size;
}
@Override
public void run() {
try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {
ByteBuffer buffer = ByteBuffer.allocate((int) size);
channel.read(buffer, position);
System.out.println("Mövqeyi " + position + " olan hissə, ölçüsü " + size + " oxundu");
// Burada buffer emal oluna bilər
} catch (IOException e) {
System.err.println("Hissəni oxuma xətası: " + e.getMessage());
}
}
}
// İşə salma nümunəsi: faylı 4 axınla 1 MB-lıq hissələrlə oxuyuruq
Path file = Path.of("bigdata.bin");
long fileSize = Files.size(file);
long chunkSize = 1024 * 1024; // 1 MB
int chunks = (int) Math.ceil((double) fileSize / chunkSize);
ExecutorService executor = Executors.newFixedThreadPool(4);
for (int i = 0; i < chunks; i++) {
long position = i * chunkSize;
long size = Math.min(chunkSize, fileSize - position);
executor.submit(new FileChunkReader(file, position, size));
}
executor.shutdown();
executor.awaitTermination(10, TimeUnit.MINUTES);
Şərh:
- Hər axın faylın öz hissəsini oxuyur və başqalarına mane olmur.
- Bu yanaşma, məsələn, torrentlərdə və yükləyicilərdə istifadə olunur.
Ümumi fayla yazarkən sinxronizasiya
Bir neçə axın eyni fayla (məsələn, log) yazırsa, sətrlərin «vinaqret»inə çevrilməməsi üçün girişi sinxronizasiya etmək lazımdır:
import java.io.*;
public class SafeLogger {
private final Writer writer;
public SafeLogger(String filename) throws IOException {
this.writer = new BufferedWriter(new FileWriter(filename, true));
}
public synchronized void log(String message) throws IOException {
writer.write(message);
writer.write(System.lineSeparator());
writer.flush();
}
public void close() throws IOException {
writer.close();
}
}
Şərh:
- log metodu synchronized kimi işarələnib ki, həmin anda yalnız bir axın fayla yaza bilsin.
- Bu işləyir, lakin axınların sayı çox olduqda darboğaza çevrilə bilər — daha yaxşısı, ayrı-ayrı fayllara yazıb sonra birləşdirməkdir.
5. Çoxaxınlılığı nə zaman istifadə etməmək lazımdır
Çoxaxınlılıq cəlbedici görünə bilər: axınlar nə qədər çox olsa, hər şey bir o qədər uçmalıdı! Amma təcrübədə bu həmişə belə deyil. Əgər bir-iki kiçik fayl emal edirsinizsə, bunu ardıcıl etmək daha sadə və etibarlıdır. Axınların işə salınmasına və onların koordinasiyasına sərf etdiyiniz vaxt, çox vaxt özünü doğrultmur.
Bəzən problem ümumiyyətlə diskdə deyil, şəbəkədə olur — onda axınların artırılması heç nəyi sürətləndirməyəcək, çünki darboğaz tamam başqa yerdədir. Daha bir tələ — eyni fayla paralel yazıdır. Əgər sinxronizasiya ilə az təcrübəniz varsa, buna cəhd etməmək daha yaxşıdır: korlanmış məlumatlar alma ehtimalı böyükdür.
Və nəhayət, əgər diskiniz və ya fayl sisteminiz onlarla axının onu eyni vaxtda «dartmağını» sevmirsə, çoxaxınlılıq xilas etməyəcək, əksinə vəziyyəti pisləşdirəcək.
Sadə desək, əgər elə gəlirsə ki, «axın nə qədər çox olsa, bir o qədər yaxşıdır», — çox vaxt belə deyil. Bəzən bir sakit axın işi on tələsik axından daha səliqəli, daha sürətli və daha etibarlı görür.
6. İrəliləmiş tapşırıqlar üçün FileChannel ilə qısa tanışlıq
FileChannel (java.nio.channels paketindən) — istənilən mövqelər üzrə məlumatı oxumağa və yazmağa imkan verən aşağı səviyyəli fayl işi alətidir. Bu, məsələn, paralel yükləməni və ya böyük faylların hissə-hissə emalını reallaşdırmağa imkan verir.
Nümunə:
try (FileChannel channel = FileChannel.open(Path.of("bigfile.bin"), StandardOpenOption.READ)) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
long position = 0;
int bytesRead = channel.read(buffer, position); // 0 mövqeyindən 1024 bayt oxuyuruq
// buffer-in emalı
}
Vacibdir:
- FileChannel sinxronizə olunmayıb — əgər bir neçə axın eyni kanal ilə işləyirsə, sinxronizasiyanı özünüz təmin etməlisiniz.
- Paralel iş üçün hər axın üçün ayrı kanal açmaq daha sadədir.
7. Çoxaxınlı IO-da tipik səhvlər
Səhv №1: Eyni fayla yazarkən sinxronizasiyasız giriş.
Nəticədə — korlanmış məlumatlar, qəribə simvollar, bəzən fayl ümumiyyətlə oxunmur. Həmişə girişi sinxronizasiya edin və ya ayrı fayllara yazın.
Səhv №2: Həddindən artıq çox axın.
Əgər 1000 faylı kopyalamaq üçün 1000 axın açırsınızsa, kompüteriniz inciyə bilər (OutOfMemoryError, donmalar, çökmələr). Axın hovuzundan (ExecutorService) istifadə edin və sayını məhdudlaşdırın.
Səhv №3: Axınlar/fayllar bağlanmır.
Hər açıq axın — OS resursudur. Onları bağlamasanız, «Too many open files» xətası ala bilərsiniz. try-with-resources istifadə edin və ya close() çağırmağı unutmayın.
Səhv №4: Proqramın vaxtından əvvəl tamamlanması.
Əgər bütün axınların bitməsini gözləməsəniz (məsələn, executor.awaitTermination(...) çağırılmasa), proqram bütün fayllar kopyalanmadan əvvəl tamamlana bilər.
Səhv №5: Mövqelər nəzərə alınmadan faylın eyni sahəsinə paralel yazma.
Əgər bir neçə axın faylın eyni sahəsinə yazırsa — məlumatlar bir-birinə qarışacaq. Mövqelərə görə yazı üçün kanallardan və diapazonların dəqiq bölünməsindən istifadə edin.
GO TO FULL VERSION