Comportamento del thread nella JVM

Il threading si riferisce alla pratica di eseguire contemporaneamente processi di programmazione per migliorare le prestazioni dell'applicazione. Sebbene non sia così comune lavorare con i thread direttamente nelle applicazioni aziendali, vengono utilizzati sempre nei framework Java.

Ad esempio, i framework che elaborano un grande volume di informazioni, come Spring Batch, utilizzano i thread per gestire i dati. La manipolazione simultanea di thread o processi della CPU migliora le prestazioni, producendo programmi più veloci ed efficienti.

Ottieni il codice sorgente

Ottieni il codice per questo Java Challenger. Puoi eseguire i tuoi test mentre segui gli esempi.

Trova il tuo primo thread: metodo main () di Java

Anche se non hai mai lavorato direttamente con i thread Java, hai lavorato indirettamente con loro perché il metodo main () di Java contiene un thread principale. Ogni volta che hai eseguito il main()metodo, hai anche eseguito il main Thread.

Lo studio della Threadclasse è molto utile per capire come funziona il threading nei programmi Java. Possiamo accedere al thread in esecuzione invocando il currentThread().getName()metodo, come mostrato qui:

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

Questo codice stamperà "main", identificando il thread attualmente in esecuzione. Sapere come identificare il thread in esecuzione è il primo passo per assorbire i concetti del thread.

Il ciclo di vita del thread Java

Quando si lavora con i thread, è fondamentale essere consapevoli dello stato del thread. Il ciclo di vita del thread Java è costituito da sei stati del thread:

  • Nuovo : Thread()è stato istanziato un nuovo .
  • Runnable : Threadil start()metodo s è stato invocato.
  • In esecuzione : il start()metodo è stato richiamato e il thread è in esecuzione.
  • Sospeso : il thread è temporaneamente sospeso e può essere ripreso da un altro thread.
  • Bloccato : il thread attende un'opportunità per essere eseguito. Ciò accade quando un thread ha già richiamato il synchronized()metodo e il thread successivo deve attendere fino al termine.
  • Terminato : l'esecuzione del thread è completa.
Rafael Chinelato Del Nero

C'è altro da esplorare e comprendere sugli stati dei thread, ma le informazioni nella Figura 1 sono sufficienti per risolvere questa sfida Java.

Elaborazione simultanea: estensione di una classe Thread

Nella sua forma più semplice, l'elaborazione simultanea viene eseguita estendendo una Threadclasse, come mostrato di seguito.

 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"); } } 

Qui stiamo eseguendo due thread: il MainThreade il InheritingThread. Quando invochiamo il start()metodo con il nuovo inheritingThread(), run()viene eseguita la logica nel metodo.

Passiamo anche il nome del secondo thread nel Threadcostruttore della classe, quindi l'output sarà:

 main is running. inheritingThread is running. 

L'interfaccia eseguibile

Invece di usare l'ereditarietà, potresti implementare l'interfaccia Runnable. Passare Runnableall'interno di un Threadcostruttore si traduce in meno accoppiamenti e più flessibilità. Dopo il passaggio Runnable, possiamo invocare il start()metodo esattamente come abbiamo fatto nell'esempio precedente:

 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()); } } 

Thread non daemon vs daemon

In termini di esecuzione, ci sono due tipi di thread:

  • I thread non daemon vengono eseguiti fino alla fine. Il thread principale è un buon esempio di thread non daemon. Il codice in main()verrà sempre eseguito fino alla fine, a meno che un non System.exit()costringa il programma a completare.
  • Un thread daemon è l'opposto, fondamentalmente un processo che non deve essere eseguito fino alla fine.

Ricorda la regola : se un thread non daemon che racchiude termina prima di un thread daemon, il thread daemon non verrà eseguito fino alla fine.

Per comprendere meglio la relazione tra thread daemon e non daemon, studia questo esempio:

 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()); } } 

In questo esempio ho usato un thread daemon per dichiarare un intervallo da 1 a 100.000, iterarli tutti e quindi stampare. Ma ricorda, un thread daemon non completerà l'esecuzione se il thread principale del non daemon termina per primo.

L'output procederà come segue:

  1. Inizio dell'esecuzione nel thread principale.
  2. Stampa numeri da 1 a possibilmente 100.000.
  3. Fine dell'esecuzione nel thread principale, molto probabilmente prima del completamento dell'iterazione a 100.000.

L'output finale dipenderà dall'implementazione della JVM.

E questo mi porta al punto successivo: i thread sono imprevedibili.

Priorità thread e JVM

È possibile dare la priorità all'esecuzione del thread con il setPrioritymetodo, ma il modo in cui viene gestito dipende dall'implementazione della JVM. Linux, MacOS e Windows hanno tutti implementazioni JVM diverse e ognuno gestirà la priorità del thread in base alle proprie impostazioni predefinite.

Tuttavia, la priorità del thread impostata influenza l'ordine di chiamata del thread. Le tre costanti dichiarate nella Threadclasse sono:

 /** * 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.

Questa storia, "Il comportamento dei thread nella JVM" è stata originariamente pubblicata da JavaWorld.