Wstęp
Wielowątkowość była wbudowana w Javę od samego początku. Przyjrzyjmy się pokrótce temu zjawisku, które nazywa się wielowątkowością.
Jako punkt odniesienia przyjmujemy oficjalną lekcję firmy Oracle: „
Lekcja: Aplikacja „Hello World! ”. Nieznacznie zmienimy kod naszego programu Hello World w następujący sposób:
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. Zapisz ten kod w pliku o nazwie zgodnej z nazwą klasy i z rozszerzeniem
.java
. Skompiluj go za pomocą narzędzia
javac :
javac HelloWorldApp.java
. Następnie uruchamiamy nasz kod z pewnym parametrem, na przykład „Roger”:
java HelloWorldApp Roger
Nasz kod ma obecnie poważną wadę. Jeśli nie przekażesz żadnego argumentu (tj. wykonasz tylko „java HelloWorldApp”), otrzymamy błąd:
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 0
at HelloWorldApp.main(HelloWorldApp.java:3)
Wystąpił wyjątek (tj. błąd) w wątku o nazwie „main”. Więc Java ma wątki? Tu zaczyna się nasza podróż.
Java i wątki
Aby zrozumieć, czym jest wątek, musisz zrozumieć, jak uruchamia się program Java. Zmieńmy nasz kod w następujący sposób:
class HelloWorldApp {
public static void main(String[] args) {
while (true) {
}
}
}
Teraz skompilujmy to ponownie za pomocą
javac
. Dla wygody uruchomimy nasz kod Java w osobnym oknie. W systemie Windows można to zrobić w następujący sposób:
start java HelloWorldApp
. Teraz użyjemy narzędzia
jps , aby zobaczyć, jakie informacje może nam przekazać Java:
Pierwsza liczba to PID lub identyfikator procesu. Co to jest proces?
A process is a combination of code and data sharing a common virtual address space.
W przypadku procesów różne programy są od siebie odizolowane podczas działania: każda aplikacja korzysta z własnego obszaru pamięci bez ingerencji w inne programy. Aby dowiedzieć się więcej, polecam przeczytanie tego samouczka:
Procesy i wątki . Proces nie może istnieć bez wątku, więc jeśli proces istnieje, to ma co najmniej jeden wątek. Ale jak to się dzieje w Javie? Kiedy uruchamiamy program Java, wykonanie rozpoczyna się od
main
metody. To tak, jakbyśmy wchodzili do programu, więc ta specjalna
main
metoda nazywana jest punktem wejścia. Metodą
main
musi być zawsze „public static void”, aby wirtualna maszyna Java (JVM) mogła rozpocząć wykonywanie naszego programu. Aby uzyskać więcej informacji,
Dlaczego główna metoda Java jest statyczna?. Okazuje się, że program uruchamiający Java (java.exe lub javaw.exe) jest prostą aplikacją C: ładuje różne biblioteki DLL, które faktycznie składają się na JVM. Program uruchamiający Java wykonuje określony zestaw wywołań Java Native Interface (JNI). JNI to mechanizm łączący świat wirtualnej maszyny Javy ze światem C++. Tak więc program uruchamiający nie jest samą maszyną JVM, ale raczej mechanizmem do jej ładowania. Zna poprawne polecenia do wykonania w celu uruchomienia maszyny JVM. Wie, jak używać wywołań JNI do konfigurowania niezbędnego środowiska. Konfiguracja tego środowiska obejmuje utworzenie głównego wątku, który oczywiście nazywa się „głównym”. Aby lepiej zilustrować, które wątki istnieją w procesie Java, używamy metody
jvisualvmnarzędzie, które jest dołączone do JDK. Znając pid procesu, możemy od razu zobaczyć informacje o tym procesie:
jvisualvm --openpid <process id>
Co ciekawe, każdy wątek ma swój odrębny obszar w pamięci przydzielony procesowi. Ta struktura pamięci nazywana jest stosem. Stos składa się z ramek. Ramka reprezentuje aktywację metody (niedokończone wywołanie metody). Ramkę można również przedstawić jako element StackTraceElement (zobacz API języka Java dla
StackTraceElement ). Więcej informacji na temat pamięci przydzielanej każdemu wątkowi można znaleźć w dyskusji tutaj: „
W jaki sposób Java (JVM) przydziela stos dla każdego wątku ”. Jeśli spojrzysz na
Java API i wyszukasz słowo „Thread”, znajdziesz
java.lang.Threadklasa. Jest to klasa reprezentująca wątek w Javie i będziemy musieli z nią pracować.
java.lang.Wątek
W Javie wątek jest reprezentowany przez instancję klasy
java.lang.Thread
. Powinieneś od razu zrozumieć, że instancje klasy Thread same w sobie nie są wątkami wykonania. To tylko rodzaj API dla wątków niskiego poziomu zarządzanych przez JVM i system operacyjny. Kiedy uruchamiamy JVM za pomocą programu uruchamiającego Java, tworzy ona wątek
main
o nazwie „główny” i kilka innych wątków porządkowych. Jak stwierdzono w JavaDoc dla klasy Thread:
When a Java Virtual Machine starts up, there is usually a single non-daemon thread
. Istnieją 2 rodzaje wątków: demony i nie-demony. Wątki demona to wątki tła (porządkujące), które wykonują pewne prace w tle. Słowo „demon” odnosi się do demona Maxwella. Możesz dowiedzieć się więcej z tego
artykułu w Wikipedii . Jak podano w dokumentacji, JVM kontynuuje wykonywanie programu (procesu) do momentu:
- Wywoływana jest metoda Runtime.exit ().
- Wszystkie wątki NON-daemon kończą swoją pracę (bez błędów lub z rzuconymi wyjątkami)
Wynika z tego ważny szczegół: wątki demona można zakończyć w dowolnym momencie. W rezultacie nie ma gwarancji co do integralności ich danych. W związku z tym wątki demonów są odpowiednie do niektórych zadań porządkowych. Na przykład Java ma wątek odpowiedzialny za przetwarzanie
finalize()
wywołań metod, czyli wątki zaangażowane w Garbage Collector (gc). Każdy wątek jest częścią grupy (
ThreadGroup ). A grupy mogą być częścią innych grup, 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 porządkują zarządzanie wątkami. Oprócz grup wątki mają własne procedury obsługi wyjątków. Spójrz 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("An error occurred: " + e.getMessage());
}
});
System.out.println(2/0);
}
Dzielenie przez zero spowoduje błąd, który zostanie przechwycony przez moduł obsługi. Jeśli nie określisz własnego modułu obsługi, JVM wywoła domyślny moduł obsługi, który wyśle ślad stosu wyjątku do StdError. Każdy wątek ma również priorytet. Możesz przeczytać więcej o priorytetach w tym artykule:
Java Thread Priority in Multithreading .
Tworzenie wątku
Jak podano w dokumentacji, mamy 2 sposoby na utworzenie wątku. Pierwszym sposobem jest stworzenie własnej podklasy. 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ć, praca nad zadaniem odbywa się w
run()
metodzie, ale sam wątek jest uruchamiany w
start()
metodzie. Nie należy mylić tych metod: jeśli bezpośrednio wywołamy
un()
metodę r, to żaden nowy wątek nie zostanie uruchomiony. Jest to
start()
metoda, która prosi maszynę JVM o utworzenie nowego wątku. Ta opcja, w której dziedziczymy Thread, jest już zła, ponieważ dołączamy Thread do naszej hierarchii klas. Drugą wadą jest to, że zaczynamy naruszać zasadę „pojedynczej odpowiedzialności”. Oznacza to, że nasza klasa jest jednocześnie odpowiedzialna za sterowanie wątkiem i za wykonanie jakiegoś zadania w tym wątku. Jaka jest właściwa droga? Odpowiedź znajduje się w tej samej
run()
metodzie, którą nadpisujemy:
public void run() {
if (target != null) {
target.run();
}
}
Oto
target
some
java.lang.Runnable
, które możemy przekazać podczas tworzenia instancji klasy Thread. Oznacza to, że 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();
}
}
Runnable
był również funkcjonalnym interfejsem od wersji Java 1.8. Dzięki temu możliwe jest napisanie jeszcze piękniejszego kodu dla zadania wątku:
public static void main(String[] args) {
Runnable task = () -> {
System.out.println("Hello, World!");
};
Thread thread = new Thread(task);
thread.start();
}
Wniosek
Mam nadzieję, że ta dyskusja wyjaśni, czym jest wątek, jak powstają wątki i jakie podstawowe operacje można wykonywać na wątkach. W
następnej części postaramy się zrozumieć, w jaki sposób wątki wchodzą ze sobą w interakcję i zbadać cykl życia wątków.
Razem lepiej: Java i klasa Thread. Część II — Synchronizacja Lepiej razem: Java i klasa Thread. Część III — Interakcja Lepiej razem: Java i klasa Thread. Część IV — Callable, Future i friends Razem lepiej: Java i klasa Thread. Część V — Executor, ThreadPool, Fork/Join Better together: Java i klasa Thread. Część VI — Odpalaj!