1. Paralelləşdirmək mümkün olan hər şeyi paralelləşdirmək lazım deyil
Java-da tapşırıqları paralelləşdirməyin bir çox yolu var. Lakin “paralellik = həmişə daha sürətlidir” — bu, şorbaya bir az da duz əlavə etsən, dadının yaxşılaşacağını düşünməyə bənzəyir: müəyyən yerə qədər — bəli, ondan sonra isə — sınamamaq daha yaxşıdır.
ExecutorService açıq şəkildə müəyyən edilmiş tapşırıqları işə salmaq və idarə etmək lazım olanda əladır: sorğuların işlənməsi, asinxron məlumat yüklənməsi, müstəqil hesablamalar. Siz özünüz hovuzda neçə thread olacağına qərar verirsiniz və tapşırıqların həyat dövrünü idarə edirsiniz.
parallelStream — əməliyyatlar müstəqil və yan təsirsiz olduqda kolleksiyaların emalını tez paralelləşdirməyin yoludur. “Ağır” kolleksiyalar üçün sərfəlidir (on minlərlə element və daha çox).
ForkJoinPool — alt tapşırıqlara yaxşı bölünən (divide & conquer) tapşırıqlar üçün seçimdir: çeşidləmə, axtarış, böyük massivlərin aqreqasiyası. parallelStream daxilində istifadə olunur, lakin onu birbaşa da idarə etmək olar.
Paralelliyi “hər ehtimala qarşı” istifadə etməyin. Tapşırıq kiçikdirsə, planlaşdırma, kontekst dəyişimi və sinxronizasiya xərcləri bütün qazancı yeyə bilər.
Nümunə: paralellik lazım olmadıqda
List<Integer> smallList = List.of(1, 2, 3, 4, 5);
int sum = smallList.parallelStream()
.mapToInt(x -> x)
.sum(); // 5 ədəd üçün paralelləşdirmək — artıqlıqdır!
2. Thread-safety: ümumi dəyişən məlumatlardan qaçın
Parallel dünyada əsas təhdid — məlumat yarışları (race conditions). Bir neçə thread eyni dəyişəni dəyişirsə, nəticə gözlənilməz ola bilər.
- Ümumi mutable dəyişənlərdən qaçının. Hətta counter++ ifadəsi belə atomik deyil.
- Thread-safe kolleksiyalardan və atomik əməliyyatlardan istifadə edin. Məsələn, ConcurrentHashMap, CopyOnWriteArrayList, AtomicInteger, AtomicLong.
- Paralel stream-lərdə yan təsirlərsiz. parallelStream-dən xarici strukturları dəyişməyin.
Pis kod nümunəsi
List<Integer> numbers = Arrays.asList(1,2,3,4,5);
List<Integer> result = new ArrayList<>();
numbers.parallelStream().forEach(n -> result.add(n * 2)); // TEHLÜKƏLİ!
Burada result.add() — thread-safe deyil. Nəticə — itirilmiş elementlər və ya istisnalar.
Necə düzgün?
List<Integer> result = numbers.parallelStream()
.map(n -> n * 2)
.collect(Collectors.toList());
3. Performans: daha çox thread həmişə daha yaxşı deyil
Xırda tapşırıqları paralelləşdirmək sərfəli deyil. İş millisanilərlə ölçülürsə, paralel işə salma tez-tez əlavə xərclərə görə icranı yalnız ləngidir.
Performansı ölçün. Sürətli ölçmələr üçün System.nanoTime() uyğundur:
long start = System.nanoTime();
// ... sizin kodunuz ...
long end = System.nanoTime();
System.out.println("İcra vaxtı: " + (end - start) + " ns");
Ciddi mikrobençmarklar üçün JMH (Java Microbenchmark Harness) istifadə edin.
Nümunə: ardıcıl və paralel stream-in müqayisəsi
List<Integer> bigList = IntStream.range(0, 1_000_000)
.boxed().collect(Collectors.toList());
long t1 = System.nanoTime();
long sum1 = bigList.stream().mapToLong(x -> x).sum();
long t2 = System.nanoTime();
long sum2 = bigList.parallelStream().mapToLong(x -> x).sum();
long t3 = System.nanoTime();
System.out.println("Ardıcıl: " + (t2 - t1) / 1_000_000 + " ms");
System.out.println("Paralel: " + (t3 - t2) / 1_000_000 + " ms");
Öz kompüterinizdə sınayın — fayda həqiqətən böyük kolleksiyalarda və ağır əməliyyatlarda nəzərə çarpır.
4. Xətaların işlənməsi: thread-lərdə istisnaları görməməzlikdən gəlməyin
Future və istisnaların işlənməsi
Əgər tapşırığı ExecutorService.submit() ilə işə salmısınızsa, istisnalar avtomatik “ötürülməyəcək” — onları Future.get() vasitəsilə emal etmək lazımdır:
Future<Integer> future = executor.submit(() -> {
if (Math.random() > 0.5) throw new RuntimeException("Ups!");
return 42;
});
try {
Integer result = future.get(); // ExecutionException ata bilər
} catch (ExecutionException e) {
System.err.println("Tapşırıqda xəta: " + e.getCause());
}
ForkJoin və xətaların işlənməsi
ForkJoinPool-da istisnalar tapşırıq daxilində “qablaşdırılır”. join()/get() çağırışında onlar üzə çıxacaq:
ForkJoinPool pool = new ForkJoinPool();
RecursiveTask<Integer> task = new MyTask();
try {
int result = pool.invoke(task);
} catch (Exception e) {
System.err.println("ForkJoin-də xəta: " + e);
}
InterruptedException-in işlənməsini unutmayın
Bir çox metodlar (məsələn, Future.get(), Thread.sleep()) InterruptedException ata bilər. Onu “udmayın” — düzgün reaksiya verin: kəsilmə bayrağını bərpa edin və ya tapşırığı tamamlayın.
5. Paralel kodun sazlanması və test edilməsi
Loglama və debug
Paralel səhvlər kələkbazdır və çox vaxt stabil təkrarlanmır. Thread göstərilərək log yazın: Thread.currentThread().getName(). Bu, kodu kimin və nə zaman icra etdiyini anlamağa kömək edir.
Çətin hallarda çoxthread-li debug dəstəyi olan debugger istifadə edin (məsələn, IntelliJ IDEA). Müvəqqəti Thread.sleep() bəzən nadir data race-i “tutmağa” kömək edir.
Çoxthread-li ssenarilərin test edilməsi
Paralel əməliyyatlar üçün ayrıca testlər ayırın və şərtlərin gözlənməsi üçün utilitilərdən istifadə edin, məsələn Awaitility. Bu cür testləri dəfələrlə işə salın: bəzi problemlər yalnız 100‑cü və ya 1000‑ci işə salmada üzə çıxır.
6. Faydalı nüanslar və məsləhətlər
Oxunaqlılıq və dəstək: anlaşılan paralel kod yazın
- Sənədləşdirin. Çətin hissələri və alət seçimini şərh edin.
- Yüksək səviyyəli abstraksiyalardan istifadə edin. Əl ilə thread idarəçiliyindənsə, ExecutorService, parallelStream, ForkJoinPool üstünlük verin.
- “Sehr”dən qaçın. Mümkündürsə, sinxronizasiyanı çətinləşdirməyin.
Cədvəl: hansı aləti nə vaxt istifadə etmək
| Ssenari | Tövsiyə olunan alət |
|---|---|
| Çox sayda müstəqil tapşırıq | |
| Böyük kolleksiyanın işlənməsi | parallelStream və ya ForkJoin |
| “Böl və hökm sür” tapşırığı | |
| Sadə asinxron tapşırıq | |
| Çoxlu kiçik tapşırıqlar | Ardıcıl stream |
| Yan təsirli tapşırıqlar | Yalnız thread-safe kolleksiyalar! |
Paralel proqramçının “qaydaları”
- Ümumi mutable dəyişənlərdən istifadə etməyin — əgər onların thread-safe olduğuna əmin deyilsinizsə.
- Təkcə paralelləşdirmək xətrinə paralelləşdirməyin: potensial qazancı qiymətləndirin.
- Pool-ları bağlamağı unutmayın: shutdown()/shutdownNow().
- Yan təsirli əməliyyatlar üçün parallelStream istifadə etməyin.
- Future və ForkJoinTask istisnalarını işləməyi unutmayın.
- InterruptedException-i “udmayın” — tapşırığı düzgün tamamlayın.
7. Parallel proqramlaşdırmada tipik səhvlər
Səhv №1: Kiçik tapşırıqların paralelləşdirilməsi. Tez-tez yeni başlayanlar hər şeyi paralelləşdirirlər, hətta iş mikrosaniyələr çəkəndə belə. Nəticə — əlavə xərclərə görə daha yavaşdır.
Səhv №2: Stream-lərdə yan təsirlər. parallelStream-də xarici dəyişənləri və ya kolleksiyaları dəyişmək olmaz — data race və proqnozlaşdırılmayan səhvlər alacaqsınız.
Səhv №3: İstisnaların görməməzlikdən gəlinməsi. Əgər Future.get() və ya ForkJoinTask-dan gələn xətaları emal etməsəniz, tapşırığın niyə yerinə yetirilmədiyini bilməyəcəksiniz.
Səhv №4: ExecutorService üçün unudulmuş shutdown(). Açıq tamamlanma olmadan tətbiq çıxışda “asılı” qala bilər.
Səhv №5: Thread-safe olmayan kolleksiyaların istifadəsi. Adi ArrayList-ə bir neçə thread-dən yazmaq — səhvlərə birbaşa yoldur.
Səhv №6: InterruptedException-i “udmaq”. Əgər thread kəsilibsə — bunu nəzərə alın və işi düzgün tamamlayın.
Səhv №7: Həddindən artıq mürəkkəb sinxronizasiya məntiqi. Artıq synchronized blokları deadlock/livelock-a aparır. Yüksək səviyyəli abstraksiyalara üstünlük verin.
GO TO FULL VERSION