0Pricing
Java Academy · Lekcja

Bezpieczeństwo wątkowe i efekty uboczne

Unikanie współdzielonego, zmiennego stanu

Bezpieczeństwo wątkowe i efekty uboczne to bezpłatna lekcja Java Academy na CoddyKit. To lekcja 3 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej Java Academy, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs Java Academy zawiera 4 lekcji w sumie.

Równoległość oznacza współbieżny dostęp

W strumieniu równoległym wiele wątków wykonuje lambdy jednocześnie. Każdy współdzielony stan zmienny, którego dotykają, staje się źródłem wyścigu.

import java.util.stream.IntStream;

public class Main {
    public static void main(String[] args) {
        int safe = IntStream.rangeClosed(1, 1000).parallel().sum();
        System.out.println(safe);
    }
}

Zagrożenie: modyfikowanie współdzielonej zmiennej

Zwykły licznik typu int używany przez wiele wątków gubi aktualizacje. Nigdy nie należy gromadzić wyników we współdzielonym stanie zmiennym wewnątrz forEach.

import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.IntStream;

public class Main {
    public static void main(String[] args) {
        AtomicInteger counter = new AtomicInteger();
        IntStream.rangeClosed(1, 1000).parallel()
            .forEach(n -> counter.incrementAndGet());
        System.out.println(counter.get());
    }
}

Dlaczego zwykłe kolekcje zawodzą

Dodawanie elementów do niebezpiecznej wątkowo klasy ArrayList z równoległego forEach może ją uszkodzić lub spowodować utratę elementów. Rozwiązaniem nie jest współdzielenie i modyfikowanie stanu, lecz całkowite unikanie takiego podejścia.

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class Main {
    public static void main(String[] args) {
        List<Integer> result = IntStream.rangeClosed(1, 10).parallel()
            .boxed()
            .collect(Collectors.toList());
        System.out.println(result.size());
    }
}

Właściwe narzędzie: collect

Należy używać collect zamiast powodującego skutki uboczne forEach. Mechanizm kolektorów bezpiecznie gromadzi częściowe kontenery w poszczególnych wątkach i łączy je.

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class Main {
    public static void main(String[] args) {
        List<Integer> squares = IntStream.rangeClosed(1, 6).parallel()
            .map(n -> n * n)
            .boxed()
            .collect(Collectors.toList());
        System.out.println(squares);
    }
}

Właściwe narzędzie: reduce

reduce jest z natury bezpieczny wątkowo, gdy operacja jest asocjacyjna i bezstanowa, ponieważ łączy częściowe wyniki bez współdzielonego modyfikowania stanu.

import java.util.stream.IntStream;

public class Main {
    public static void main(String[] args) {
        int sum = IntStream.rangeClosed(1, 1000).parallel()
            .reduce(0, Integer::sum);
        System.out.println(sum);
    }
}

Bezstanowe lambdy

Lambdy w strumieniach powinny być bezstanowe: ich wynik zależy wyłącznie od danych wejściowych, a nie od zewnętrznych zmiennych zmiennych ani wcześniejszych elementów.

import java.util.stream.IntStream;

public class Main {
    public static void main(String[] args) {
        long count = IntStream.rangeClosed(1, 1000).parallel()
            .filter(n -> n % 2 == 0)
            .count();
        System.out.println(count);
    }
}

Typy atomowe jako rozwiązanie awaryjne

Jeśli współdzielenie licznika jest naprawdę konieczne, należy użyć klas atomowych, takich jak AtomicLong. Poprawnie serializują one aktualizacje, choć powodują rywalizację między wątkami.

import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.IntStream;

public class Main {
    public static void main(String[] args) {
        AtomicLong sum = new AtomicLong();
        IntStream.rangeClosed(1, 1000).parallel()
            .forEach(sum::addAndGet);
        System.out.println(sum.get());
    }
}

Kolekcje współbieżne

Gdy potrzebna jest współdzielona mapa używana przez wiele wątków, bezpiecznym rozwiązaniem jest ConcurrentHashMap. Mimo to, jeśli to możliwe, należy preferować Collectors.groupingByConcurrent.

import java.util.Map;
import java.util.List;
import java.util.stream.Collectors;

public class Main {
    public static void main(String[] args) {
        List<String> words = List.of("a", "bb", "cc", "ddd");
        Map<Integer, List<String>> byLen = words.parallelStream()
            .collect(Collectors.groupingByConcurrent(String::length));
        System.out.println(byLen);
    }
}

Należy unikać logiki zależnej od kolejności

Nie należy polegać na kolejności napotkania elementów wewnątrz lambd równoległych. Operacje zakładające przetwarzanie sekwencyjne dają niepoprawne wyniki w trybie równoległym.

import java.util.stream.IntStream;

public class Main {
    public static void main(String[] args) {
        int sum = IntStream.rangeClosed(1, 100).parallel()
            .map(n -> n * 2)
            .sum();
        System.out.println(sum);
    }
}

Funkcje czyste można bezpiecznie wykonywać równolegle

Najważniejsza zasada: należy pisać operacje czyste, bezstanowe i nieingerujące. Wtedy strumień może działać sekwencyjnie lub równolegle, zawsze zwracając identyczne i poprawne wyniki.

import java.util.List;
import java.util.stream.Collectors;

public class Main {
    public static void main(String[] args) {
        List<String> in = List.of("red", "green", "blue");
        String out = in.parallelStream()
            .map(String::toUpperCase)
            .sorted()
            .collect(Collectors.joining(","));
        System.out.println(out);
    }
}

Brak ingerencji

Nie należy modyfikować kolekcji źródłowej podczas działania strumienia przechodzącego po jej elementach. Powoduje to ConcurrentModificationException lub niezdefiniowane działanie, zarówno sekwencyjne, jak i równoległe.

import java.util.List;
import java.util.stream.Collectors;

public class Main {
    public static void main(String[] args) {
        List<Integer> src = List.of(1, 2, 3, 4);
        List<Integer> doubled = src.parallelStream()
            .map(n -> n * 2)
            .collect(Collectors.toList());
        System.out.println(doubled);
    }
}

Szybkie sprawdzenie

Jaki jest zalecany sposób gromadzenia wyników ze strumienia równoległego?

Podsumowanie

Zapewniono poprawność strumieni równoległych:

  • Współdzielony stan zmienny w równoległych lambdach powoduje wyścigi.
  • Należy preferować collect i reduce zamiast powodującego skutki uboczne forEach.
  • Lambdy powinny być bezstanowe, czyste i nieingerujące.
  • Jeśli współdzielenie jest konieczne, należy użyć typów atomowych lub kolekcji współbieżnych, ale powinno to być rozwiązanie ostateczne.

Często zadawane pytania

Czy lekcja „Bezpieczeństwo wątkowe i efekty uboczne” jest bezpłatna?

Tak — pełny tekst „Bezpieczeństwo wątkowe i efekty uboczne” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu Java Academy, przejdź na CoddyKit PRO. Kurs Java Academy zawiera 4 lekcji w sumie.

Co nauczysz się w „Bezpieczeństwo wątkowe i efekty uboczne”?

Unikanie współdzielonego, zmiennego stanu Ćwiczysz Java Academy z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.

Czy potrzebuję doświadczenia, aby zacząć Java Academy?

Nie wymagamy żadnego doświadczenia. Java Academy w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 3 z 4.

Ile czasu zajmuje lekcja „Bezpieczeństwo wątkowe i efekty uboczne”?

Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.

Czy mogę pisać i uruchamiać kod w tej lekcji Java Academy?

Tak. Każda lekcja Java Academy zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.

Wszystkie lekcje w tym kursie

  1. Tworzenie strumieni równoległych
  2. Kiedy równoległość pomaga
  3. Bezpieczeństwo wątkowe i efekty uboczne
  4. Typowe pułapki
← Powrót do Java Academy