Wstęp
Wielowątkowość jest wbudowana w Javę od jej początków. Przyjrzyjmy się zatem szybko, na czym polega wielowątkowość.
Za punkt wyjścia przyjmijmy oficjalną lekcję firmy Oracle: „
Lekcja: Aplikacja „Hello World! ”. Zmieńmy trochę kod naszej aplikacji Hello World na następujący:
class HelloWorldApp {
public static void main(String[] args) {
System.out.println("Hello, " + args[0]);
}
}
args
jest tablicą parametrów wejściowych przekazywanych podczas uruchamiania programu. Zapiszmy ten kod do pliku o nazwie odpowiadającej nazwie klasy i rozszerzeniu
.java
. Skompilujmy przy użyciu narzędzia
javac :
javac HelloWorldApp.java
Następnie wywołaj nasz kod z jakimś parametrem, na przykład Roger:
java HelloWorldApp Roger
Nasz kod ma teraz poważną wadę. Jeśli nie przekażemy żadnego argumentu (czyli po prostu uruchomimy Java HelloWorldApp), otrzymamy błąd:
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 0
at HelloWorldApp.main(HelloWorldApp.java:3)
Wystąpił wyjątek (tzn. błąd) w wątku o nazwie
main
. Okazuje się, że w Javie są jakieś wątki? Tutaj zaczyna się nasza podróż.
Java i wątki
Aby zrozumieć, czym jest wątek, musisz zrozumieć, w jaki sposób uruchamiana jest aplikacja Java. Zmieńmy nasz kod w następujący sposób:
class HelloWorldApp {
public static void main(String[] args) {
while (true) {
}
}
}
Teraz skompilujmy go ponownie, używając javac. Następnie dla wygody uruchomimy nasz kod Java w osobnym oknie. W systemie Windows możesz to zrobić w następujący sposób:
start java HelloWorldApp
. Teraz, korzystając z narzędzia
jps , zobaczmy, jakie informacje powie nam Java:
Pierwsza liczba to PID lub Process ID, identyfikator procesu. Co to jest proces?
Процесс — это совокупность kodа и данных, разделяющих общее виртуальное adresное пространство.
Za pomocą procesów wykonywanie różnych programów jest odizolowane od siebie: każda aplikacja korzysta z własnego obszaru pamięci, nie zakłócając działania innych programów. Radzę dokładniej przeczytać artykuł: „
https://habr.com/post/164487/ ”. Proces nie może istnieć bez wątków, więc jeśli proces istnieje, istnieje w nim co najmniej jeden wątek. Jak to się dzieje w Javie? Kiedy uruchamiamy program Java, jego wykonanie rozpoczyna się od pliku
main
. Wchodzimy do programu, więc ta specjalna metoda
main
nazywa się punktem wejścia lub „punktem wejścia”. Metoda
main
musi być zawsze
public static void
taka, aby wirtualna maszyna Java (JVM) mogła rozpocząć wykonywanie naszego programu. Więcej szczegółów znajdziesz w artykule „
Dlaczego główna metoda Java jest statyczna? ”. Okazuje się, że program uruchamiający Java (java.exe lub javaw.exe) jest prostą aplikacją (prostą aplikacją w C): ładuje różne biblioteki DLL, które w rzeczywistości są maszyną JVM. Program uruchamiający Java wykonuje określony zestaw wywołań Java Native Interface (JNI). JNI to mechanizm łączący świat wirtualnej maszyny Java i świat C++. Okazuje się, że programem uruchamiającym nie jest JVM, ale jej moduł ładujący. Zna prawidłowe polecenia, które należy wykonać, aby uruchomić maszynę JVM. Wie, jak zorganizować całe niezbędne środowisko za pomocą wywołań JNI. Ta organizacja środowiska obejmuje również utworzenie głównego wątku, który zwykle nazywa się
main
. Aby lepiej zobaczyć, jakie wątki żyją w procesie Java, używamy programu
jvisualvm , który jest zawarty w JDK. Znając pid procesu, możemy od razu otworzyć o nim dane:
jvisualvm --openpid айдипроцесса
Co ciekawe, każdy wątek ma swój własny, odrębny obszar w pamięci przydzielony dla procesu. Ta struktura pamięci nazywa się stosem. Stos składa się z ramek. Ramka jest punktem wywołania metody, punktem wykonania. Ramkę można również przedstawić jako StackTraceElement (zobacz Java API dla
StackTraceElement ). Więcej o pamięci przydzielonej każdemu wątkowi możesz przeczytać
tutaj . Jeśli spojrzymy na
API Java i wyszukamy słowo Thread, zobaczymy, że istnieje klasa
java.lang.Thread . To właśnie ta klasa reprezentuje strumień w Javie i właśnie z nią musimy pracować.
java.lang.Wątek
Wątek w Javie jest reprezentowany jako instancja klasy
java.lang.Thread
. Warto od razu zrozumieć, że instancje klasy Thread w Javie same w sobie nie są wątkami. Jest to po prostu rodzaj API dla wątków niskiego poziomu zarządzanych przez maszynę JVM i system operacyjny. Kiedy uruchamiamy maszynę JVM za pomocą programu uruchamiającego Java, tworzy ona główny wątek z nazwą
main
i kilkoma innymi wątkami usług. Jak stwierdzono w dokumencie JavaDoc klasy Thread:
When a Java Virtual Machine starts up, there is usually a single non-daemon thread
Istnieją 2 typy wątków: demony i inne niż demony. Wątki demona to wątki działające w tle (wątki usługowe), które wykonują pewne prace w tle. To ciekawe określenie jest nawiązaniem do „demona Maxwella”, o którym więcej można przeczytać w artykule na Wikipedii o „
demonach ”. Jak stwierdzono w dokumentacji, JVM kontynuuje wykonywanie programu (procesu) do momentu:
- Metoda Runtime.exit nie jest wywoływana
- Wszystkie wątki inne niż demony zakończyły swoją pracę (zarówno bez błędów, jak i ze zgłoszonymi wyjątkami)
Stąd ważny szczegół: wątki demona można zakończyć po wykonaniu dowolnego polecenia. Dlatego nie gwarantuje się integralności zawartych w nich danych. Dlatego wątki demonów nadają się do niektórych zadań serwisowych. Na przykład w Javie istnieje wątek odpowiedzialny za przetwarzanie metod finalizacji lub wątków związanych z modułem Garbage Collector (GC). Każdy wątek należy do jakiejś grupy (
ThreadGroup ). Grupy mogą wchodzić w siebie, tworząc pewną hierarchię lub strukturę.
public static void main(String []args){
Thread currentThread = Thread.currentThread();
ThreadGroup threadGroup = currentThread.getThreadGroup();
System.out.println("Thread: " + currentThread.getName());
System.out.println("Thread Group: " + threadGroup.getName());
System.out.println("Parent Group: " + threadGroup.getParent().getName());
}
Grupy pozwalają usprawnić zarządzanie przepływami i śledzić je. Oprócz grup wątki mają własną procedurę obsługi wyjątków. Spójrzmy na przykład:
public static void main(String []args) {
Thread th = Thread.currentThread();
th.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println("Wystąpił błąd: " + e.getMessage());
}
});
System.out.println(2/0);
}
Dzielenie przez zero spowoduje błąd, który zostanie przechwycony przez procedurę obsługi. Jeśli sam nie określisz procedury obsługi, zadziała domyślna implementacja obsługi, która wyświetli stos błędów w StdError. Więcej możesz przeczytać w recenzji
http://pro-java.ru/java-dlya-opytnyx/obrabotchik-neperexvachennyx-isklyuchenij-java/ ”. Poza tym wątek ma priorytet. Więcej o priorytetach możesz przeczytać w dziale artykuł „
Priorytet wątku Java w wielowątkowości ”.
Tworzenie wątku
Jak podano w dokumentacji, mamy 2 sposoby na utworzenie wątku. Pierwszym z nich jest stworzenie swojego spadkobiercy. Na przykład:
public class HelloWorld{
public static class MyThread extends Thread {
@Override
public void run() {
System.out.println("Hello, World!");
}
}
public static void main(String []args){
Thread thread = new MyThread();
thread.start();
}
}
Jak widać w metodzie uruchamiane jest zadanie
run
, a w metodzie wątek
start
. Nie należy ich mylić, ponieważ... jeśli uruchomimy metodę
run
bezpośrednio, żaden nowy wątek nie zostanie uruchomiony. Jest to metoda
start
, która prosi maszynę JVM o utworzenie nowego wątku. Opcja z potomkiem Thread jest zła, ponieważ uwzględniamy Thread w hierarchii klas. Drugą wadą jest to, że zaczynamy łamać zasadę „Wyłącznej odpowiedzialności” SOLID, ponieważ nasza klasa staje się jednocześnie odpowiedzialna zarówno za zarządzanie wątkiem, jak i za pewne zadanie, które należy wykonać w tym wątku. Który jest poprawny? Odpowiedź kryje się w metodzie,
run
którą zastępujemy:
public void run() {
if (target != null) {
target.run();
}
}
Oto
target
niektóre z nich
java.lang.Runnable
, które możemy przekazać do Thread podczas tworzenia instancji klasy. Dlatego możemy to zrobić:
public class HelloWorld{
public static void main(String []args){
Runnable task = new Runnable() {
public void run() {
System.out.println("Hello, World!");
}
};
Thread thread = new Thread(task);
thread.start();
}
}
Jest to także
Runnable
interfejs funkcjonalny od wersji Java 1.8. Dzięki temu możesz jeszcze piękniej pisać kod zadań dla wątków:
public static void main(String []args){
Runnable task = () -> {
System.out.println("Hello, World!");
};
Thread thread = new Thread(task);
thread.start();
}
Całkowity
Mam więc nadzieję, że z tej historii jasno wynika, czym jest strumień, jak istnieje i jakie podstawowe operacje można na nim wykonać. W
dalszej części warto zrozumieć jak wątki oddziałują na siebie i jaki jest ich cykl życia. #Wiaczesław
GO TO FULL VERSION