CodeGym /Kurslar /JAVA 25 SELF /Spliterator və paralel stream-lər

Spliterator və paralel stream-lər

JAVA 25 SELF
Səviyyə , Dərs
Mövcuddur

1. Spliterator ilə tanışlıq

Əgər siz düşünürdünüzsə ki, Java-da kolleksiyalar yalnız Iterator vasitəsilə iterasiya olunur, onda Java 8-ə qədər tamamilə haqlı idiniz. Amma Stream API və paralelliyə meyl gələndən yeni bir qəhrəman yarandı — Spliterator.

Spliterator — bu, yalnız kolleksiyanın elementlərini iterasiya etməyə yox, həm də mənbəni paralel işləmə üçün hissələrə bölməyə imkan verən interfeysdir. Adı — splititerator sözlərinin birləşməsidir.

Böyük bir tort təsəvvür edin. Adi Iterator onu tikə-tikə kəsib ardıcıllıqla yeyir. Spliterator tortu iki yerə bölə, yarısını dosta verə bilər — və siz hər ikiniz eyni anda yeməyə başlayarsınız. Dostlar çoxdur — bölməyə davam!

Spliterator interfeysi — əsas metodlar

public interface Spliterator<T> {
    boolean tryAdvance(java.util.function.Consumer<? super T> action);
    Spliterator<T> trySplit();
    long estimateSize();
    int characteristics();
    // ... daha bir neçə metod var, amma bunlar ən vacibləridir
}
  • tryAdvance — növbəti elementlə nəsə edir (next() + əməl analoqu).
  • trySplit — mənbəni iki hissəyə bölməyə çalışır və “ayrılmış” hissə üçün yeni Spliterator qaytarır.
  • estimateSize — neçə element qaldığını təxmini qiymətləndirir.
  • characteristics — xarakteristikaların bit maskasını qaytarır (ardıcıllıq, unikallıq, dəyişməzlik və s.).

2. Spliterator istifadəsi: əl ilə iterasiya və bölmə

Kolleksiyadan Spliterator əldə etmək

Collection-ı reallaşdıran istənilən kolleksiya öz Spliterator-unu verə bilər:

import java.util.List;
import java.util.Spliterator;

List<String> names = List.of("Vasya", "Petya", "Maşa", "Lena");
Spliterator<String> spliterator = names.spliterator();

Elementləri əl ilə iterasiya etmək

Spliterator<String> spliterator = names.spliterator();
while (spliterator.tryAdvance(name -> System.out.println("Ad: " + name))) {
    // Hər şey tryAdvance daxilində edilir
}

Kolleksiyanın bölünməsi

Ən maraqlısı — trySplit() metodu:

Spliterator<String> spliterator1 = names.spliterator();
Spliterator<String> spliterator2 = spliterator1.trySplit();

System.out.println("Birinci hissə:");
spliterator1.forEachRemaining(System.out::println);

System.out.println("İkinci hissə:");
if (spliterator2 != null) {
    spliterator2.forEachRemaining(System.out::println);
}

Nə baş verəcək: Spliterator kolleksiyanı iki hissəyə bölməyə çalışacaq (həmişə bərabər olmaya bilər — reallaşdırmadan asılıdır). İndi hər iki hissəni müstəqil şəkildə emal edə bilərsiniz — hətta müxtəlif thread-lərdə!

3. Paralel stream-lər: nə üçün və bu necə işləyir

Paralel stream (parallelStream()) — elementləri ardıcıllıqla deyil, eyni vaxtda bir neçə thread-də emal edən stream-dir. Xüsusilə böyük həcmli verilənlər və çoxnüvəli prosessorlar üçün faydalıdır.

import java.util.List;

List<String> names = List.of("Vasya", "Petya", "Maşa", "Lena");
// Adi stream:
names.stream().forEach(System.out::println);
// Paralel stream:
names.parallelStream().forEach(System.out::println);

Mahiyyət nədədir?
Adi stream-də elementlər bir thread-də emal olunur. Paralel stream-də isə mənbə ( Spliterator vasitəsilə) hissələrə bölünür və hər hissə ayrı thread-də emal edilir.

Daxildə bu necə işləyir?

  1. Spliterator kolleksiyanı hissələrə bölür — adətən mövcud nüvələrin sayına görə (bəzən bir az artıq).
  2. Hər hissə öz thread-ində emal olunur — ümumi ForkJoinPool istifadə edilir.
  3. Nəticələr geri toplanır — yekun kolleksiyaya və ya dəyərə birləşdirilir.

Paralel stream-in iş sxemi

flowchart LR
    A[Kolleksiya] --> B{Spliterator}
    B --> C1[Hissə 1] --> D1[Thread 1]
    B --> C2[Hissə 2] --> D2[Thread 2]
    B --> C3[Hissə 3] --> D3[Thread 3]
    D1 & D2 & D3 --> E[Nəticənin toplanması]

4. Paralel stream-lərin üstünlükləri və məhdudiyyətləri

Üstünlüklər

  • Sürətlənmə böyük kolleksiyaların emalında: ağır hesablamalarda paralel stream icranı nəzərəçarpacaq dərəcədə sürətləndirir.
  • Sadəlik: çoxaxınlı kodu əl ilə yazmağa ehtiyac yoxdur — stream()-i parallelStream() ilə əvəz edin.

Məhdudiyyətlər və tələlər

  • Həmişə sürətli deyil: kiçik kolleksiyalar üçün üst xərclər faydanı “yeyə” bilər.
  • Sıra təmin olunmur: forEach/map/filter-də sıra fərqli ola bilər. Sıra vacibdirsə — forEachOrdered istifadə edin.
  • Thread-safety problemləri: yan təsirli əməliyyatlar (xarici kolleksiyaların/dəyişənlərin dəyişdirilməsi) data race-lərə gətirib çıxarır.
  • Bütün əməliyyatlar uyğun deyil: asılı hesablama növləri (məsələn, ardıcıl yığım) gözlənildiyi kimi işləməyə bilər.

Paralel stream-ləri nə zaman istifadə etməli?

  • Böyük kolleksiyalar (on minlərlə element və daha çox).
  • Hər elementdə ağır əməliyyatlar.
  • Sərt sıra kritik deyil.
  • Yan təsirlər yoxdur (pure funksiyalar).

Nə zaman istifadə ETMƏMƏK?

  • Elementlər azdır.
  • Kod xarici dəyişənləri və ya kolleksiyaları dəyişir.
  • Emal ardıcıllığını saxlamaq vacibdir.
  • Mənbə pis bölünür (məsələn, LinkedList).

5. Praktiki nümunələr

Nümunə 1: İcra vaxtının müqayisəsi

import java.util.*;
import java.util.stream.*;

public class ParallelStreamDemo {
    public static void main(String[] args) {
        List<Integer> numbers = IntStream.range(0, 10_000_000)
                                         .boxed()
                                         .collect(Collectors.toList());

        long start = System.currentTimeMillis();
        long count = numbers.stream()
                .filter(n -> isPrime(n))
                .count();
        long time = System.currentTimeMillis() - start;
        System.out.println("Adi stream: " + time + " ms, tapılan sadələrin sayı: " + count);

        start = System.currentTimeMillis();
        count = numbers.parallelStream()
                .filter(n -> isPrime(n))
                .count();
        time = System.currentTimeMillis() - start;
        System.out.println("Paralel stream: " + time + " ms, tapılan sadələrin sayı: " + count);
    }

    // Nümunə üçün ən sadə sadə ədəd yoxlaması
    public static boolean isPrime(int n) {
        if (n < 2) return false;
        for (int i = 2, sqrt = (int)Math.sqrt(n); i <= sqrt; i++)
            if (n % i == 0) return false;
        return true;
    }
}

Nəticə nə olacaq: böyük həcmli verilənlərdə paralel stream çox vaxt daha sürətlidir (xüsusən çoxnüvəli prosessorlarda). Kiçik həcmlərdə — fərq olmaya bilər və ya paralel variant daha yavaş ola bilər.

Nümunə 2: Sıra problemi

import java.util.List;

List<String> names = List.of("Vasya", "Petya", "Maşa", "Lena");
System.out.println("Adi stream:");
names.stream().forEach(System.out::println);

System.out.println("Paralel stream:");
names.parallelStream().forEach(System.out::println);

System.out.println("forEachOrdered ilə paralel stream:");
names.parallelStream().forEachOrdered(System.out::println);

Nəticə: adi stream-də və forEachOrdered istifadə ediləndə sıra qorunur, paralel stream-də isə onsuz — qorunmur.

Nümunə 3: Yan təsirlərin təhlükəsi

import java.util.*;
import java.util.stream.*;

List<Integer> numbers = IntStream.range(1, 1000).boxed().collect(Collectors.toList());
List<Integer> results = new ArrayList<>();

// TEHLÜKƏLİDİR! Belə etməyin!
numbers.parallelStream().forEach(n -> results.add(n * n));

System.out.println("Siyahının ölçüsü: " + results.size());

Nə baş verə bilər? Siyahının ölçüsü gözləniləndən az ola bilər və bəzən ConcurrentModificationException yarana bilər. Səbəb — ArrayList thread-safe deyil, paralel stream isə bir neçə thread-i eyni vaxtda işə salır.

6. Spliterator: xüsusiyyətlər və xarakteristikalar

Spliterator-un xarakteristikaları

Spliterator öz xüsusiyyətlərini bit maskası ilə təsvir edir:

  • ORDERED — elementlər müəyyən ardıcıllıqla gedir (məsələn, siyahıda).
  • DISTINCT — bütün elementlər unikal olur (məsələn, multiset olmayan toplusunda).
  • SORTED — elementlər çeşidlənib.
  • SIZED — ölçüsü məlumdur.
  • IMMUTABLE — kolleksiya dəyişməzdir.
  • CONCURRENT — kolleksiya thread-safe-dir.
  • SUBSIZEDtrySplit()-dən sonra alınan bütün spliterator-lar da öz ölçüsünü bilir.
Spliterator<String> spliterator = names.spliterator();
int characteristics = spliterator.characteristics();
System.out.println(Integer.toBinaryString(characteristics));

Bunu niyə bilmək lazımdır? Stream API və paralel stream-lər bu əlamətlərdən optimizasiyalar üçün istifadə edir. Məsələn, mənbə dəyişməz və çeşidlidirsə, onu daha təhlükəsiz və səmərəli bölmək və nəticəni toplamaq mümkündür.

7. Spliterator-u nə vaxt və necə birbaşa istifadə etməli?

Gündəlik həyatda öz Spliterator-unuzu yazmaq nadir hallarda lazım olur: standart kolleksiyalar bunu artıq reallaşdırır. Amma öz məlumat mənbənizi yaradırsınızsa və ya iterasiya/bölməni incə tənzimləmək istəyirsinizsə, Spliterator işinizə yarayacaq.

Nümunə: tryAdvance ilə əl ilə iterasiya

import java.util.List;
import java.util.Spliterator;

List<String> names = List.of("Vasya", "Petya", "Maşa", "Lena");
Spliterator<String> spliterator = names.spliterator();
spliterator.tryAdvance(name -> System.out.println("Birinci element: " + name));
spliterator.forEachRemaining(name -> System.out.println("Qalanlar: " + name));

Nümunə: kolleksiyanın bölünməsi

Spliterator<String> spliterator1 = names.spliterator();
Spliterator<String> spliterator2 = spliterator1.trySplit();

if (spliterator2 != null) {
    spliterator2.forEachRemaining(name -> System.out.println("Hissə 2: " + name));
}
spliterator1.forEachRemaining(name -> System.out.println("Hissə 1: " + name));

8. Spliterator və paralel stream-lərlə işləyərkən tipik səhvlər

Səhv №1: Kiçik kolleksiyalar üçün paralel stream-lərdən istifadə. Sürətlənmə əvəzinə yavaşıma alacaqsınız — bölmə və tapşırıqların planlaşdırılması üzrə üst xərclər faydanı üstələyəcək.

Səhv №2: Elementlərin sırasının qorunacağını gözləmək. Paralel stream-lər sıranı təmin etmir. Sıra vacibdirsə — forEachOrdered istifadə edin, amma paralel effektivliyin bir hissəsi itəcək.

Səhv №3: Lambda ifadələrində yan təsirlər. Paralel stream daxilində xarici dəyişənləri/kolleksiyaları təhlükəsiz dəyişmək olmaz — data race-lər və çətin aşkarlanan bug-lar əldə edəcəksiniz.

Səhv №4: Paralel stream daxilində təhlükəsiz olmayan kolleksiyalardan istifadə. Bir neçə thread-dən adi ArrayList-ə əlavə etmək ConcurrentModificationException kimi səhvlərə birbaşa yoldur.

Səhv №5: Ani sürətlənmə gözləntisi. Paralel stream-lər sehrli çubuq deyil. Profilinq edin: verilənlər azdırsa və ya əməliyyat yüngüldürsə — adi stream daha sürətli olacaq.

Səhv №6: Bölünməsi zəif olan mənbələrlə paralel stream-lər. Məsələn, LinkedList çox vaxt səmərəli bölünmür — paralellik icranı yalnız yavaşıda bilər.

Şərhlər
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION