9 błędów, których należy unikać podczas programowania w Javie
Jakich błędów należy unikać podczas programowania w Javie? W poniższym artykule odpowiemy na to pytanie.
Przeczytaj pierwszą część naszej serii blogów poświęconych współbieżności w Javie. W kolejnym artykule przyjrzymy się bliżej różnicom między wątkami i procesami, pulom wątków, executorom i wielu innym!
Ogólnie rzecz biorąc, konwencjonalne podejście do programowania jest sekwencyjne. Wszystko w programie dzieje się krok po kroku.
Ale w rzeczywistości równoległość jest sposobem, w jaki działa cały świat - jest to zdolność do wykonywania więcej niż jednego zadania jednocześnie.
Omówienie takich zaawansowanych tematów jak współbieżność w Java lub wielowątkowość, musimy ustalić pewne wspólne definicje, aby mieć pewność, że jesteśmy po tej samej stronie.
Zacznijmy od podstaw. W niesekwencyjnym świecie, mamy dwa rodzaje reprezentantów współbieżności: procesy oraz
wątki. Proces to instancja uruchomionego programu. Zazwyczaj jest on odizolowany od innych procesów.
System operacyjny jest odpowiedzialny za przypisywanie zasobów do każdego procesu. Ponadto działa jako przewodnik, który
harmonogramy i kontroluje je.
Wątek jest rodzajem procesu, ale na niższym poziomie, dlatego jest również znany jako lekki wątek. Wiele wątków może działać w jednym
proces. W tym przypadku program działa jako harmonogram i kontroler wątków. W ten sposób poszczególne programy wydają się wykonywać
wiele zadań w tym samym czasie.
Podstawową różnicą między wątkami a procesami jest poziom izolacji. Proces ma swój własny zestaw
podczas gdy wątek współdzieli dane z innymi wątkami. Może się to wydawać podejściem podatnym na błędy i rzeczywiście tak jest. Dla
Nie skupiajmy się teraz na tym, ponieważ wykracza to poza zakres tego artykułu.
Procesy, wątki - ok... Ale czym dokładnie jest współbieżność? Współbieżność oznacza możliwość wykonywania wielu zadań jednocześnie.
czasu. Nie oznacza to, że zadania te muszą być wykonywane jednocześnie - na tym polega równoległość. Concurrenc w Javay również nie
wymaga posiadania wielu procesorów lub nawet wielu rdzeni. Można to osiągnąć w środowisku jednordzeniowym, wykorzystując
przełączanie kontekstu.
Termin związany ze współbieżnością to wielowątkowość. Jest to cecha programów, która pozwala im wykonywać kilka zadań jednocześnie. Nie każdy program korzysta z tego podejścia, ale te, które to robią, można nazwać wielowątkowymi.
Jesteśmy prawie gotowi, jeszcze tylko jedna definicja. Asynchronia oznacza, że program wykonuje operacje bez blokowania.
Inicjuje zadanie, a następnie wykonuje inne czynności w oczekiwaniu na odpowiedź. Gdy otrzyma odpowiedź, może na nią zareagować.
Domyślnie każdy Aplikacja Java działa w jednym procesie. W procesie tym istnieje jeden wątek związany z aplikacją main()
metoda
aplikacji. Jak jednak wspomniano, możliwe jest wykorzystanie mechanizmów wielowątkowości w ramach jednej aplikacji.
program.
Wątek
jest Java w której dzieje się magia. Jest to reprezentacja obiektowa wcześniej wspomnianego wątku. Do
utworzyć własny wątek, można rozszerzyć Wątek
klasa. Nie jest to jednak zalecane podejście. Nici
powinien być używany jako mechanizm uruchamiający zadanie. Zadania są elementami kod które chcemy uruchomić w trybie współbieżnym. Możemy je zdefiniować za pomocą funkcji Wykonywalny
interfejs.
Ale dość teorii, włóżmy nasz kod tam, gdzie są nasze usta.
Załóżmy, że mamy kilka tablic liczb. Dla każdej tablicy chcemy poznać sumę liczb w tablicy. Załóżmy
Załóżmy, że takich tablic jest wiele i każda z nich jest stosunkowo duża. W takich warunkach chcemy wykorzystać współbieżność i zsumować każdą tablicę jako osobne zadanie.
int[] a1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int[] a2 = {10, 10, 10, 10, 10, 10, 10, 10};;
int[] a3 = {3, 4, 3, 4, 3, 4, 2, 1, 3, 7};
Runnable task1 = () -> {
int sum = Arrays.stream(a1).sum();
System.out.println("1. Suma wynosi: " + sum);
};
Runnable task2 = () -> {
int sum = Arrays.stream(a2).sum();
System.out.println("2. Suma wynosi: " + sum);
};
Runnable task3 = () -> {
int sum = Arrays.stream(a3).sum();
System.out.println("3. Suma wynosi: " + sum);
};
new Thread(task1).start();
new Thread(task2).start();
new Thread(task3).start();
Jak widać z powyższego kodu Wykonywalny
jest interfejsem funkcjonalnym. Zawiera jedną abstrakcyjną metodę run()
bez argumentów. The Wykonywalny
Interfejs powinien być implementowany przez każdą klasę, której instancje mają być
wykonywane przez wątek.
Po zdefiniowaniu zadania można utworzyć wątek do jego uruchomienia. Można to osiągnąć poprzez new Thread()
konstruktor, który
bierze Wykonywalny
jako argument.
Ostatnim krokiem jest start()
nowo utworzonego wątku. W API dostępne są również run()
metody w Wykonywalny
oraz wWątek
. Nie jest to jednak sposób na wykorzystanie współbieżności w Javie. Bezpośrednie wywołanie każdej z tych metod skutkuje
wykonanie zadania w tym samym wątku main()
metoda działa.
W przypadku dużej liczby zadań tworzenie osobnego wątku dla każdego z nich nie jest dobrym pomysłem. Tworzenie wątku Wątek
jest
i znacznie lepiej jest ponownie wykorzystać istniejące wątki niż tworzyć nowe.
Gdy program tworzy wiele krótkotrwałych wątków, lepiej jest użyć puli wątków. Pula wątków zawiera pewną liczbę
gotowych do uruchomienia, ale obecnie nieaktywnych wątków. Dając Wykonywalny
do puli powoduje, że jeden z wątków wywołuje funkcjęrun()
metoda danego Wykonywalny
. Po zakończeniu zadania wątek nadal istnieje i znajduje się w stanie bezczynności.
Ok, jasne - wolisz pulę wątków zamiast ręcznego tworzenia. Ale jak można wykorzystać pule wątków? The Wykonawcy
posiada szereg statycznych metod fabrycznych do konstruowania pul wątków. Na przykład newCachedThredPool()
tworzy
pula, w której nowe wątki są tworzone w razie potrzeby, a bezczynne wątki są przechowywane przez 60 sekund. W przeciwieństwie do tego,newFixedThreadPool()
zawiera stały zestaw wątków, w którym bezczynne wątki są przechowywane w nieskończoność.
Zobaczmy, jak to może działać w naszym przykładzie. Teraz nie musimy tworzyć wątków ręcznie. Zamiast tego musimy utworzyćExecutorService
która zapewnia pulę wątków. Następnie możemy przypisać do niego zadania. Ostatnim krokiem jest zamknięcie wątku
aby uniknąć wycieków pamięci. Reszta poprzedniego kodu pozostaje bez zmian.
ExecutorService executor = Executors.newCachedThreadPool();
executor.submit(task1);
executor.submit(task2);
executor.submit(task3);
executor.shutdown();
Wykonywalny
wydaje się fajnym sposobem na tworzenie współbieżnych zadań, ale ma jedną poważną wadę. Nie może zwrócić żadnego
wartość. Co więcej, nie możemy określić, czy zadanie zostało ukończone, czy nie. Nie wiemy też, czy zostało ono ukończone
normalnie lub wyjątkowo. Rozwiązaniem tych bolączek jest Na żądanie
.
Na żądanie
jest podobny do Wykonywalny
w pewnym sensie również opakowuje zadania asynchroniczne. Główną różnicą jest to, że jest w stanie
zwraca wartość. Zwracana wartość może być dowolnego (nieprymitywnego) typu, tak jak funkcja Na żądanie
jest typem sparametryzowanym.Na żądanie
jest funkcjonalnym interfejsem, który posiada call()
która może rzucić metodę Wyjątek
.
Zobaczmy teraz, jak możemy wykorzystać Na żądanie
w naszym problemie z tablicą.
int[] a1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int[] a2 = {10, 10, 10, 10, 10, 10, 10, 10};;
int[] a3 = {3, 4, 3, 4, 3, 4, 2, 1, 3, 7};
Callable task1 = () -> Arrays.stream(a1).sum();
Callable task2 = () -> Arrays.stream(a2).sum();
Callable task3 = () -> Arrays.stream(a3).sum();
ExecutorService executor = Executors.newCachedThreadPool();
Future future1 = executor.submit(task1);
Future future2 = executor.submit(task2);
Future future3 = executor.submit(task3);
System.out.println("1. Suma wynosi: " + future1.get());
System.out.println("2. Suma wynosi: " + future2.get());
System.out.println("3. Suma wynosi: " + future3.get());
executor.shutdown();
Ok, możemy zobaczyć jak Na żądanie
jest tworzony, a następnie przesyłany do ExecutorService
. Ale czym do cholery jest Przyszłość
?Przyszłość
działa jako pomost między wątkami. Suma każdej tablicy jest tworzona w oddzielnym wątku i potrzebujemy sposobu na
otrzymanie tych wyników z powrotem do main()
.
Aby pobrać wynik z Przyszłość
musimy zadzwonić get()
metoda. Tutaj może wydarzyć się jedna z dwóch rzeczy. Po pierwsze
wynik obliczeń wykonanych przez Na żądanie
jest dostępny. Wtedy otrzymujemy go natychmiast. Po drugie, wynik nie jest
jeszcze gotowy. W takim przypadku get()
będzie blokowana do momentu uzyskania wyniku.
Problem z Przyszłość
jest to, że działa w "paradygmacie push". Podczas korzystania z Przyszłość
musisz być jak szef, który
nieustannie pyta: "Czy zadanie zostało wykonane? Czy jest gotowe?", dopóki nie przyniesie rezultatu. Działanie pod ciągłą presją to
drogie. O wiele lepszym podejściem byłoby zamówienie Przyszłość
co zrobić, gdy będzie gotowy do wykonania swojego zadania. Niestety,Przyszłość
nie może tego zrobić, ale ComputableFuture
może.
ComputableFuture
działa w paradygmacie "pull". Możemy mu powiedzieć, co ma zrobić z wynikiem, gdy wykona swoje zadania. To
jest przykładem podejścia asynchronicznego.
ComputableFuture
działa idealnie z Wykonywalny
ale nie z Na żądanie
. Zamiast tego możliwe jest dostarczenie zadania doComputableFuture
w postaci Dostawca
.
Zobaczmy, jak powyższe odnosi się do naszego problemu.
int[] a1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int[] a2 = {10, 10, 10, 10, 10, 10, 10, 10};;
int[] a3 = {3, 4, 3, 4, 3, 4, 2, 1, 3, 7};
CompletableFuture.supplyAsync(() -> Arrays.stream(a1).sum())
.thenAccept(System.out::println);
CompletableFuture.supplyAsync(() -> Arrays.stream(a2).sum())
.thenAccept(System.out::println);
CompletableFuture.supplyAsync(() -> Arrays.stream(a3).sum())
.thenAccept(System.out::println);
Pierwszą rzeczą, która rzuca się w oczy, jest to, o ile krótsze jest to rozwiązanie. Poza tym wygląda schludnie i estetycznie.
Zadanie do CompletableFuture
mogą być dostarczane przez supplyAsync()
metoda, która przyjmuje Dostawca
lub przez runAsync()
że
bierze Wykonywalny
. Wywołanie zwrotne - fragment kodu, który powinien zostać uruchomiony po zakończeniu zadania - jest definiowane przez thenAccept()
metoda.
Java zapewnia wiele różnych podejść do współbieżności. W tym artykule ledwie dotknęliśmy tego tematu.
Niemniej jednak, omówiliśmy podstawy Wątek
, Wykonywalny
, Na żądanie
oraz CallableFuture
co stanowi dobry punkt
do dalszego badania tematu.