Zachowanie wątku w JVM

Wątkowanie odnosi się do praktyki wykonywania procesów programowania jednocześnie w celu poprawy wydajności aplikacji. Chociaż nie jest tak powszechne, aby pracować z wątkami bezpośrednio w aplikacjach biznesowych, są one używane przez cały czas w frameworkach Java.

Na przykład struktury przetwarzające duże ilości informacji, takie jak Spring Batch, używają wątków do zarządzania danymi. Jednoczesne manipulowanie wątkami lub procesami procesora poprawia wydajność, co skutkuje szybszymi i bardziej wydajnymi programami.

Pobierz kod źródłowy

Uzyskaj kod dla tego programu Java Challenger. Możesz przeprowadzić własne testy, postępując zgodnie z przykładami.

Znajdź swój pierwszy wątek: metoda main () języka Java

Nawet jeśli nigdy nie pracowałeś bezpośrednio z wątkami Java, pracowałeś z nimi pośrednio, ponieważ metoda main () języka Java zawiera główny wątek. Za każdym razem, gdy wykonałeś tę main()metodę, wykonałeś także main Thread.

Zapoznanie się z tą Threadklasą jest bardzo pomocne w zrozumieniu, jak działa wątkowanie w programach Java. Możemy uzyskać dostęp do wątku, który jest wykonywany, wywołując currentThread().getName()metodę, jak pokazano poniżej:

 public class MainThread { public static void main(String... mainThread) { System.out.println(Thread.currentThread().getName()); } } 

Ten kod wypisze „main”, identyfikując aktualnie wykonywany wątek. Wiedza o tym, jak zidentyfikować wykonywany wątek, jest pierwszym krokiem do przyswojenia pojęć dotyczących wątków.

Cykl życia wątku Java

Podczas pracy z wątkami należy pamiętać o stanie wątku. Cykl życia wątku Java składa się z sześciu stanów wątku:

  • Nowość : utworzono nową Thread()instancję.
  • Runnable : the Thread„s start()metoda została wywołana.
  • Uruchomiona : start()metoda została wywołana i wątek jest uruchomiony.
  • Zawieszony : wątek jest tymczasowo zawieszony i może zostać wznowiony przez inny wątek.
  • Zablokowany : wątek czeka na możliwość uruchomienia. Dzieje się tak, gdy jeden wątek już wywołał synchronized()metodę, a następny wątek musi poczekać, aż zostanie zakończony.
  • Zakończony : WYKONANIE wątku jest kompletna.
Rafael Chinelato Del Nero

Jest więcej do zbadania i zrozumienia stanów wątków, ale informacje na rysunku 1 wystarczą, aby rozwiązać to wyzwanie Java.

Przetwarzanie współbieżne: rozszerzanie klasy Thread

Mówiąc najprościej, współbieżne przetwarzanie odbywa się poprzez rozszerzenie Threadklasy, jak pokazano poniżej.

 public class InheritingThread extends Thread { InheritingThread(String threadName) { super(threadName); } public static void main(String... inheriting) { System.out.println(Thread.currentThread().getName() + " is running"); new InheritingThread("inheritingThread").start(); } @Override public void run() { System.out.println(Thread.currentThread().getName() + " is running"); } } 

Tutaj uruchamiamy dwa wątki: the MainThreadi the InheritingThread. Kiedy wywołujemy start()metodę z new inheritingThread(), wykonywana jest logika run()metody.

Przekazujemy również nazwę drugiego wątku w Threadkonstruktorze klasy, więc wynik będzie:

 main is running. inheritingThread is running. 

Interfejs Runnable

Zamiast używać dziedziczenia, możesz zaimplementować interfejs Runnable. Przechodzenie do Runnablewnętrza Threadkonstruktora skutkuje mniejszym sprzężeniem i większą elastycznością. Po przejściu Runnablemożemy wywołać start()metodę dokładnie tak, jak zrobiliśmy to w poprzednim przykładzie:

 public class RunnableThread implements Runnable { public static void main(String... runnableThread) { System.out.println(Thread.currentThread().getName()); new Thread(new RunnableThread()).start(); } @Override public void run() { System.out.println(Thread.currentThread().getName()); } } 

Wątki inne niż demon a wątki demonów

Pod względem wykonania istnieją dwa rodzaje wątków:

  • Wątki niebędące demonami są wykonywane do końca. Wątek główny jest dobrym przykładem wątku niebędącego demonem. Kod w main()będzie zawsze wykonywany do końca, chyba że a System.exit()wymusi zakończenie programu.
  • Gwint demon to przeciwnie, w zasadzie procesem, który nie musi być wykonana do końca.

Pamiętaj o zasadzie : jeśli zamykający wątek niebędący demonem kończy się przed wątkiem demona, wątek demona nie będzie wykonywany aż do końca.

Aby lepiej zrozumieć związek wątków demonów i wątków niebędących demonami, przeanalizuj następujący przykład:

 import java.util.stream.IntStream; public class NonDaemonAndDaemonThread { public static void main(String... nonDaemonAndDaemon) throws InterruptedException { System.out.println("Starting the execution in the Thread " + Thread.currentThread().getName()); Thread daemonThread = new Thread(() -> IntStream.rangeClosed(1, 100000) .forEach(System.out::println)); daemonThread.setDaemon(true); daemonThread.start(); Thread.sleep(10); System.out.println("End of the execution in the Thread " + Thread.currentThread().getName()); } } 

W tym przykładzie użyłem wątku demona do zadeklarowania zakresu od 1 do 100 000, iteracji wszystkich z nich, a następnie wydrukowania. Pamiętaj jednak, że wątek demona nie zakończy wykonywania, jeśli główny wątek niebędący demonem zakończy się jako pierwszy.

Wynik będzie wyglądał następująco:

  1. Rozpoczęcie wykonywania w głównym wątku.
  2. Wydrukuj liczby od 1 do 100 000.
  3. Koniec wykonywania w głównym wątku, najprawdopodobniej przed zakończeniem iteracji do 100 000.

Ostateczny wynik będzie zależał od implementacji maszyny JVM.

I to prowadzi mnie do następnego punktu: wątki są nieprzewidywalne.

Priorytet wątków i JVM

Istnieje możliwość ustalenia priorytetów wykonywania wątku za pomocą setPrioritymetody, ale sposób jej obsługi zależy od implementacji maszyny JVM. Linux, MacOS i Windows mają różne implementacje JVM i każdy obsługuje priorytet wątków zgodnie z własnymi domyślnymi ustawieniami.

Ustawiony priorytet wątku ma jednak wpływ na kolejność wywoływania wątku. Trzy stałe zadeklarowane w Threadklasie to:

 /** * The minimum priority that a thread can have. */ public static final int MIN_PRIORITY = 1; /** * The default priority that is assigned to a thread. */ public static final int NORM_PRIORITY = 5; /** * The maximum priority that a thread can have. */ public static final int MAX_PRIORITY = 10; 

Try running some tests on the following code to see what execution priority you end up with:

 public class ThreadPriority { public static void main(String... threadPriority) { Thread moeThread = new Thread(() -> System.out.println("Moe")); Thread barneyThread = new Thread(() -> System.out.println("Barney")); Thread homerThread = new Thread(() -> System.out.println("Homer")); moeThread.setPriority(Thread.MAX_PRIORITY); barneyThread.setPriority(Thread.NORM_PRIORITY); homerThread.setPriority(Thread.MIN_PRIORITY); homerThread.start(); barneyThread.start(); moeThread.start(); } } 

Even if we set moeThread as MAX_PRIORITY, we cannot count on this thread being executed first. Instead, the order of execution will be random.

Constants vs enums

The Thread class was introduced with Java 1.0. At that time, priorities were set using constants, not enums. There's a problem with using constants, however: if we pass a priority number that is not in the range of 1 to 10, the setPriority() method will throw an IllegalArgumentException. Today, we can use enums to get around this issue. Using enums makes it impossible to pass an illegal argument, which both simplifies the code and gives us more control over its execution.

Take the Java threads challenge!

You've learned just a little bit about threads, but it's enough for this post's Java challenge.

To start, study the following code:

 public class ThreadChallenge { private static int wolverineAdrenaline = 10; public static void main(String... doYourBest) { new Motorcycle("Harley Davidson").start(); Motorcycle fastBike = new Motorcycle("Dodge Tomahawk"); fastBike.setPriority(Thread.MAX_PRIORITY); fastBike.setDaemon(false); fastBike.start(); Motorcycle yamaha = new Motorcycle("Yamaha YZF"); yamaha.setPriority(Thread.MIN_PRIORITY); yamaha.start(); } static class Motorcycle extends Thread { Motorcycle(String bikeName) { super(bikeName); } @Override public void run() { wolverineAdrenaline++; if (wolverineAdrenaline == 13) { System.out.println(this.getName()); } } } } 

What will be the output of this code? Analyze the code and try to determine the answer for yourself, based on what you've learned.

A. Harley Davidson

B. Dodge Tomahawk

C. Yamaha YZF

D. Indeterminate

What just happened? Understanding threads behavior

In the above code, we created three threads. The first thread is Harley Davidson, and we assigned this thread the default priority. The second thread is Dodge Tomahawk, assigned MAX_PRIORITY. The third is Yamaha YZF, with MIN_PRIORITY. Then we started the threads.

In order to determine the order the threads will run in, you might first note that the Motorcycle class extends the Thread class, and that we've passed the thread name in the constructor. We've also overridden the run() method with a condition: if wolverineAdrenaline is equals to 13.

Even though Yamaha YZF is the third thread in our order of execution, and has MIN_PRIORITY, there's no guarantee that it will be executed last for all JVM implementations.

You might also note that in this example we set the Dodge Tomahawk thread as daemon. Because it's a daemon thread, Dodge Tomahawk may never complete execution. But the other two threads are non-daemon by default, so the Harley Davidson and Yamaha YZF threads will definitely complete their execution.

To conclude, the result will be D: Indeterminate, because there is no guarantee that the thread scheduler will follow our order of execution or thread priority.

Remember, we can't rely on program logic (order of threads or thread priority) to predict the JVM's order of execution.

Video challenge! Debugging variable arguments

Debugging is one of the easiest ways to fully absorb programming concepts while also improving your code. In this video you can follow along while I debug and explain the thread behavior challenge:

Common mistakes with Java threads

  • Invoking the run() method to try to start a new thread.
  • Trying to start a thread twice (this will cause an IllegalThreadStateException).
  • Allowing multiple processes to change the state of an object when it shouldn't change.
  • Writing program logic that relies on thread priority (you can't predict it).
  • Relying on the order of thread execution--even if we start a thread first, there is no guarantee it will be executed first.

What to remember about Java threads

  • Invoke the start() method to start a Thread.
  • It's possible to extend the Thread class directly in order to use threads.
  • It's possible to implement a thread action inside a Runnable interface.
  • Thread priority depends on the JVM implementation.
  • Thread behavior will always depend on the JVM implementation.
  • A daemon thread won't complete if an enclosing non-daemon thread ends first.

Learn more about Java threads on JavaWorld

  • Read the Java 101 threads series to learn more about threads and runnables, thread synchronization, thread scheduling with wait/notify, and thread death.
  • Modern threading: A Java concurrency primer introduces java.util.concurrent and answers common questions for developers new to Java concurrency.
  • Modern threading for not-quite-beginners offers more advanced tips and best practices for working with java.util.concurrent.

More from Rafael

  • Get more quick code tips: Read all the posts in the Java Challengers series.
  • Build your Java skills: Visit the Java Dev Gym for a code workout.
  • Want to work on stress free projects and write bug-free code? Visit the NoBugsProject for your copy of No Bugs, No Stress - Create a Life-Changing Software Without Destroying Your Life.

Artykuł „Zachowanie wątków w JVM” został pierwotnie opublikowany przez JavaWorld.