Java 101: Zrozumienie wątków Java, część 1: Wprowadzenie do wątków i elementów wykonawczych

Ten artykuł jest pierwszym z czteroczęściowej serii Java 101 poświęconej wątkom Java. Chociaż możesz pomyśleć, że wątki w Javie byłyby trudne do zrozumienia, zamierzam pokazać, że wątki są łatwe do zrozumienia. W tym artykule przedstawię wątki Java i elementy wykonawcze. W kolejnych artykułach omówimy synchronizację (za pomocą blokad), problemy z synchronizacją (takie jak zakleszczenie), mechanizm oczekiwania / powiadamiania, planowanie (z priorytetem i bez), przerwanie wątku, liczniki czasu, zmienność, grupy wątków i zmienne lokalne wątku .

Zwróć uwagę, że ten artykuł (część archiwów JavaWorld) został zaktualizowany o nowe listy kodów i kod źródłowy do pobrania w maju 2013.

Zrozumieć wątki Java - przeczytaj całą serię

  • Część 1: Wprowadzenie wątków i elementów wykonawczych
  • Część 2: Synchronizacja
  • Część 3: Planowanie wątków i czekanie / powiadamianie
  • Część 4: Grupy wątków i zmienność

Co to jest nić?

Koncepcyjnie pojęcie wątku nie jest trudne do uchwycenia: jest to niezależna ścieżka wykonania poprzez kod programu. Gdy wykonywanych jest wiele wątków, ścieżka jednego wątku w tym samym kodzie zwykle różni się od pozostałych. Na przykład załóżmy, że jeden wątek wykonuje odpowiednik kodu bajtowego części instrukcji if-else if, podczas gdy inny wątek wykonuje równoważny kod bajtowy elseczęści. W jaki sposób JVM śledzi wykonanie każdego wątku? JVM nadaje każdemu wątkowi własny stos wywołań metod. Oprócz śledzenia bieżącej instrukcji kodu bajtowego stos wywołań metod śledzi zmienne lokalne, parametry przekazywane przez maszynę JVM do metody oraz wartość zwracaną przez metodę.

Gdy wiele wątków wykonuje sekwencje instrukcji kodu bajtowego w tym samym programie, akcja ta jest nazywana wielowątkowością . Wielowątkowość przynosi programowi różne korzyści:

  • Programy oparte na wielowątkowym graficznym interfejsie użytkownika (GUI) pozostają responsywne dla użytkowników podczas wykonywania innych zadań, takich jak zmiana strony lub drukowanie dokumentu.
  • Programy wielowątkowe zazwyczaj kończą się szybciej niż ich niegwintowane odpowiedniki. Jest to szczególnie prawdziwe w przypadku wątków działających na maszynie wieloprocesorowej, gdzie każdy wątek ma własny procesor.

Java realizuje wielowątkowość poprzez swoją java.lang.Threadklasę. Każdy Threadobiekt opisuje pojedynczy wątek wykonania. Że wykonanie nastąpi w Thread„s run()metody. Ponieważ run()metoda domyślna nic nie robi, musisz wykonać podklasę Threadi nadpisać, run()aby wykonać użyteczną pracę. Aby poznać przedsmak wątków i wielowątkowości w kontekście Thread, zapoznaj się z listą 1:

Listing 1. ThreadDemo.java

// ThreadDemo.java class ThreadDemo { public static void main (String [] args) { MyThread mt = new MyThread (); mt.start (); for (int i = 0; i < 50; i++) System.out.println ("i = " + i + ", i * i = " + i * i); } } class MyThread extends Thread { public void run () { for (int count = 1, row = 1; row < 20; row++, count++) { for (int i = 0; i < count; i++) System.out.print ('*'); System.out.print ('\n'); } } }

Listing 1 przedstawia kod źródłowy aplikacji składającej się z klas ThreadDemoi MyThread. Klasa ThreadDemosteruje aplikacją, tworząc MyThreadobiekt, uruchamiając wątek, który kojarzy się z tym obiektem i wykonując kod w celu wydrukowania tabeli kwadratów. W przeciwieństwie do MyThreadnadpisania Thread„s run()metoda drukowania (na standardowym strumieniu wyjściowym) do trójkąta prostokątnego składający się ze znaków gwiazdką.

Planowanie wątków i JVM

Większość (jeśli nie wszystkie) implementacji JVM wykorzystuje możliwości obsługi wątków platformy. Ponieważ te możliwości są specyficzne dla platformy, kolejność danych wyjściowych programów wielowątkowych może różnić się od kolejności danych wyjściowych innych osób. Ta różnica wynika z planowania, który omówię w dalszej części tej serii.

Po wpisaniu w java ThreadDemocelu uruchomienia aplikacji maszyna JVM tworzy początkowy wątek wykonywania, który wykonuje main()metodę. Wykonując mt.start ();, wątek początkowy instruuje maszynę JVM, aby utworzyła drugi wątek wykonania, który wykonuje instrukcje w kodzie bajtowym zawierające metodę MyThreadobiektu run(). Gdy start()metoda zwraca, wątek początkowy wykonuje forpętlę, aby wydrukować tabelę kwadratów, podczas gdy nowy wątek wykonuje run()metodę drukowania trójkąta prostokątnego.

Jak wygląda wynik? Biegnij, ThreadDemoaby się dowiedzieć. Zauważysz, że wyjście każdego wątku ma tendencję do przeplatania się z wyjściem drugiego. Dzieje się tak, ponieważ oba wątki wysyłają swoje dane wyjściowe do tego samego standardowego strumienia wyjściowego.

Klasa Thread

Aby stać się biegłym w pisaniu kodu wielowątkowego, musisz najpierw zrozumieć różne metody tworzące Threadklasę. W tej sekcji omówiono wiele z tych metod. W szczególności dowiesz się o metodach uruchamiania wątków, nazywania wątków, przełączania wątków w stan uśpienia, określania, czy wątek żyje, łączenia jednego wątku z innym wątkiem i wyliczania wszystkich aktywnych wątków w grupie wątków i podgrupach bieżącego wątku. Omawiam również Threadpomoce debugowania i wątki użytkownika w porównaniu z wątkami demonów.

Pozostałe Threadmetody przedstawię w kolejnych artykułach, z wyjątkiem przestarzałych metod firmy Sun.

Przestarzałe metody

Firma Sun wycofała różne Threadmetody, takie jak suspend()i resume(), ponieważ mogą one blokować programy lub uszkadzać obiekty. W rezultacie nie powinieneś wywoływać ich w swoim kodzie. Zapoznaj się z dokumentacją zestawu SDK, aby poznać obejścia tych metod. Nie omawiam przestarzałych metod z tej serii.

Konstruowanie wątków

Threadma ośmiu konstruktorów. Najprostsze to:

  • Thread(), który tworzy Threadobiekt o domyślnej nazwie
  • Thread(String name), co tworzy Threadobiekt o nazwie nameokreślonej przez argument

Kolejnymi najprostszymi konstruktorami są Thread(Runnable target)i Thread(Runnable target, String name). Poza Runnableparametrami te konstruktory są identyczne jak wyżej wymienione konstruktory. Różnica: Runnableparametry identyfikują obiekty na zewnątrz, Threadktóre zapewniają run()metody. (Się dowiedzieć o Runnabledalszej części tego artykułu). Ostatnie cztery konstruktorów przypominają Thread(String name), Thread(Runnable target)oraz Thread(Runnable target, String name); jednak ostateczni konstruktorzy zawierają również ThreadGroupargument ze względów organizacyjnych.

Jeden z czterech ostatnich konstruktorów Thread(ThreadGroup group, Runnable target, String name, long stackSize)jest interesujący, ponieważ pozwala określić żądany rozmiar stosu wywołań metod wątku. Możliwość określenia tego rozmiaru okazuje się pomocna w programach z metodami, które wykorzystują rekursję - technikę wykonywania, w której metoda wielokrotnie wywołuje samą siebie - w celu eleganckiego rozwiązywania pewnych problemów. Poprzez jawne ustawienie rozmiaru stosu można czasami zapobiec StackOverflowErrors. Jednak zbyt duży rozmiar może spowodować OutOfMemoryErrors. Ponadto Sun uważa rozmiar stosu wywołań metody za zależny od platformy. W zależności od platformy rozmiar stosu wywołań metody może się zmienić. Dlatego zastanów się dokładnie nad konsekwencjami swojego programu przed napisaniem kodu, który go wywołuje Thread(ThreadGroup group, Runnable target, String name, long stackSize).

Uruchom swoje pojazdy

Wątki przypominają pojazdy: przenoszą programy od początku do końca. Threada Threadobiekty podklasy nie są wątkami. Zamiast tego opisują atrybuty wątku, takie jak jego nazwa, i zawierają kod (za pomocą run()metody), który wątek wykonuje. Kiedy nadchodzi czas wykonania nowego wątku run(), inny wątek wywołuje metodę Threadobiektu 's lub jego podklasy start(). Na przykład, aby rozpocząć drugi wątek, main()wywołuje wątek początkowy aplikacji, który jest wykonywany start(). W odpowiedzi kod obsługi wątków maszyny JVM współpracuje z platformą, aby zapewnić prawidłową inicjalizację wątku i wywołanie metody Threadobiektu klasy A lub jej podklasy run().

Po start()zakończeniu wykonywania wielu wątków. Ponieważ mamy tendencję do myślenia w sposób liniowy, często trudno jest nam zrozumieć równoczesną (jednoczesną) aktywność, która ma miejsce, gdy działają dwa lub więcej wątków. Dlatego powinieneś przyjrzeć się wykresowi, który pokazuje, gdzie wykonuje wątek (jego położenie) w funkcji czasu. Poniższy rysunek przedstawia taki wykres.

Wykres przedstawia kilka znaczących okresów czasu:

  • Inicjalizacja wątku początkowego
  • Moment, w którym wątek zaczyna się wykonywać main()
  • Moment, w którym wątek zaczyna się wykonywać start()
  • Chwila start()tworzy nowy wątek i wraca domain()
  • Inicjalizacja nowego wątku
  • W momencie, gdy nowy wątek zaczyna się wykonywać run()
  • W różnych momentach każdy wątek się kończy

Zauważ, że inicjalizacja nowego wątku, jego wykonanie run()i zakończenie następuje jednocześnie z wykonaniem wątku początkowego. Należy również zauważyć, że po wywołaniu wątku start()kolejne wywołania tej metody przed zakończeniem run()metody powodują start()zgłoszenie java.lang.IllegalThreadStateExceptionobiektu.

Co jest w imieniu?

Podczas sesji debugowania pomocne jest rozróżnianie jednego wątku od drugiego w sposób przyjazny dla użytkownika. Aby rozróżnić wątki, Java kojarzy nazwę z wątkiem. Domyślnie ta nazwa Threadto znak łącznika i liczba całkowita liczona od zera. Możesz zaakceptować domyślne nazwy wątków Java lub możesz wybrać własne. Aby dostosować nazwy niestandardowe, Threadzapewnia konstruktory, które pobierają nameargumenty i setName(String name)metodę. Threadudostępnia również getName()metodę, która zwraca bieżącą nazwę. Listing 2 pokazuje, jak ustalić niestandardową nazwę za pomocą Thread(String name)konstruktora i pobrać bieżącą nazwę w run()metodzie, wywołując getName():

Listing 2. NameThatThread.java

// NameThatThread.java class NameThatThread { public static void main (String [] args) { MyThread mt; if (args.length == 0) mt = new MyThread (); else mt = new MyThread (args [0]); mt.start (); } } class MyThread extends Thread { MyThread () { // The compiler creates the byte code equivalent of super (); } MyThread (String name) { super (name); // Pass name to Thread superclass } public void run () { System.out.println ("My name is: " + getName ()); } }

Możesz przekazać opcjonalny argument nazwa MyThreadw wierszu poleceń. Na przykład java NameThatThread Xustanawia Xjako nazwę wątku. Jeśli nie określisz nazwy, zobaczysz następujące dane wyjściowe:

My name is: Thread-1

Jeśli wolisz, możesz zmienić super (name);wywołanie w MyThread (String name)konstruktorze na wywołanie setName (String name)—jak w setName (name);. To drugie wywołanie metody osiąga ten sam cel - ustalenie nazwy wątku - co super (name);. Zostawiam to jako ćwiczenie dla ciebie.

Nazewnictwo main

Java przypisuje nazwę maindo wątku, który uruchamia main()metodę, wątku początkowego. Nazwa ta jest zwykle widoczna w Exception in thread "main"komunikacie, który wyświetla domyślny program obsługi wyjątków maszyny JVM, gdy wątek początkowy zgłasza obiekt wyjątku.

Spać czy nie spać

W dalszej części tego artykułu wprowadzę Cię w animację - wielokrotne rysowanie na jednej powierzchni obrazów, które nieco się od siebie różnią, aby uzyskać iluzję ruchu. Aby wykonać animację, wątek musi zatrzymać się podczas wyświetlania dwóch kolejnych obrazów. Metoda Threadstatyczna wywołania sleep(long millis)wymusza na wątku wstrzymanie na millismilisekundy. Inny wątek mógłby prawdopodobnie przerwać śpiącą nić. Jeśli tak się stanie, śpiąca nić budzi się i wyrzuca InterruptedExceptionobiekt z sleep(long millis)metody. W rezultacie, kod, który wywołuje sleep(long millis)musi pojawić się w tryblokowo lub metody kodu musi zawierać InterruptedExceptionw swojej throwsklauzuli.

Aby zademonstrować sleep(long millis), napisałem CalcPI1aplikację. Ta aplikacja uruchamia nowy wątek, który używa algorytmu matematycznego do obliczenia wartości stałej matematycznej pi. Podczas obliczania nowego wątku wątek początkowy zatrzymuje się na 10 milisekund przez wywołanie sleep(long millis). Po przebudzeniu wątku początkowego wypisuje wartość pi, którą nowy wątek przechowuje w zmiennej pi. Listing 3 przedstawia CalcPI1kod źródłowy:

Listing 3. CalcPI1.java

// CalcPI1.java class CalcPI1 { public static void main (String [] args) { MyThread mt = new MyThread (); mt.start (); try { Thread.sleep (10); // Sleep for 10 milliseconds } catch (InterruptedException e) { } System.out.println ("pi = " + mt.pi); } } class MyThread extends Thread { boolean negative = true; double pi; // Initializes to 0.0, by default public void run () { for (int i = 3; i < 100000; i += 2) { if (negative) pi -= (1.0 / i); else pi += (1.0 / i); negative = !negative; } pi += 1.0; pi *= 4.0; System.out.println ("Finished calculating PI"); } }

Jeśli uruchomisz ten program, zobaczysz dane wyjściowe podobne (ale prawdopodobnie nie identyczne) do następujących:

pi = -0.2146197014017295 Finished calculating PI