Java 101: comprensione dei thread Java, parte 1: presentazione di thread e eseguibili

Questo articolo è il primo di una serie Java 101 in quattro parti che esplora i thread Java. Anche se potresti pensare che il threading in Java sarebbe difficile da comprendere, intendo mostrarti che i thread sono facili da capire. In questo articolo, ti presento ai thread Java e ai runnable. Negli articoli successivi, esploreremo la sincronizzazione (tramite blocchi), i problemi di sincronizzazione (come deadlock), il meccanismo di attesa / notifica, la pianificazione (con e senza priorità), l'interruzione del thread, i timer, la volatilità, i gruppi di thread e le variabili locali del thread .

Si noti che questo articolo (parte degli archivi JavaWorld) è stato aggiornato con nuovi elenchi di codici e codice sorgente scaricabile nel maggio 2013.

Capire i thread Java: leggi l'intera serie

  • Parte 1: presentazione di thread ed eseguibili
  • Parte 2: sincronizzazione
  • Parte 3: pianificazione dei thread e attesa / notifica
  • Parte 4: gruppi di thread e volatilità

Cos'è un thread?

Concettualmente, la nozione di thread non è difficile da comprendere: è un percorso di esecuzione indipendente attraverso il codice del programma. Quando vengono eseguiti più thread, il percorso di un thread attraverso lo stesso codice di solito è diverso dagli altri. Ad esempio, si supponga che un thread esegua l'equivalente in byte code della parte di un'istruzione if-else if, mentre un altro thread esegua l'equivalente in byte code della elseparte. In che modo la JVM tiene traccia dell'esecuzione di ogni thread? La JVM fornisce a ogni thread il proprio stack di chiamate al metodo. Oltre a tenere traccia dell'istruzione del codice byte corrente, lo stack di chiamate al metodo tiene traccia delle variabili locali, dei parametri che la JVM passa a un metodo e del valore restituito dal metodo.

Quando più thread eseguono sequenze di istruzioni byte-code nello stesso programma, quell'azione è nota come multithreading . Il multithreading avvantaggia un programma in vari modi:

  • I programmi basati su GUI (interfaccia utente grafica) multithread rimangono reattivi per gli utenti durante l'esecuzione di altre attività, come la ripaginazione o la stampa di un documento.
  • I programmi con thread terminano in genere più velocemente delle loro controparti non thread. Ciò è particolarmente vero per i thread in esecuzione su una macchina multiprocessore, in cui ogni thread ha il proprio processore.

Java realizza il multithreading attraverso la sua java.lang.Threadclasse. Ogni Threadoggetto descrive un singolo thread di esecuzione. Che l'esecuzione avviene in Thread's run()metodo. Poiché il run()metodo predefinito non fa nulla, è necessario creare sottoclassi Threade sovrascrivere run()per eseguire un lavoro utile. Per un assaggio dei thread e del multithreading nel contesto di Thread, esaminare il Listato 1:

Listato 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'); } } }

Il listato 1 presenta il codice sorgente di un'applicazione composta da classi ThreadDemoe MyThread. La classe ThreadDemoguida l'applicazione creando un MyThreadoggetto, avviando un thread che si associa a quell'oggetto ed eseguendo del codice per stampare una tabella di quadrati. Al contrario, MyThreadsovrascrive Threadil run()metodo di stampare (sul flusso di output standard) un triangolo ad angolo retto composto da caratteri asterisco.

Pianificazione dei thread e JVM

La maggior parte (se non tutte) le implementazioni JVM utilizzano le funzionalità di threading della piattaforma sottostante. Poiché queste funzionalità sono specifiche della piattaforma, l'ordine dell'output dei programmi multithreading potrebbe differire dall'ordine dell'output di qualcun altro. Questa differenza deriva dalla pianificazione, un argomento che esplorerò più avanti in questa serie.

Quando si digita java ThreadDemoper eseguire l'applicazione, la JVM crea un thread di esecuzione iniziale, che esegue il main()metodo. In esecuzione mt.start ();, il thread iniziale dice alla JVM di creare un secondo thread di esecuzione che esegue le istruzioni del codice byte che comprendono il metodo MyThreaddell'oggetto run(). Quando il start()metodo ritorna, il thread iniziale esegue il suo forciclo per stampare una tabella di quadrati, mentre il nuovo thread esegue il run()metodo per stampare il triangolo ad angolo retto.

Come appare l'output? Corri ThreadDemoa scoprirlo. Noterai che l'output di ogni thread tende a intervallarsi con l'output dell'altro. Ciò risulta perché entrambi i thread inviano il loro output allo stesso flusso di output standard.

La classe Thread

Per diventare esperto nella scrittura di codice multithread, devi prima comprendere i vari metodi che compongono la Threadclasse. Questa sezione esplora molti di questi metodi. In particolare, si apprendono i metodi per avviare i thread, denominare i thread, mettere i thread in sospensione, determinare se un thread è attivo, unire un thread a un altro thread ed enumerare tutti i thread attivi nel gruppo di thread e nei sottogruppi del thread corrente. Discuto anche Threaddegli aiuti per il debug e dei thread utente rispetto ai thread daemon.

Presenterò il resto dei Threadmetodi di negli articoli successivi, ad eccezione dei metodi deprecati di Sun.

Metodi deprecati

Sun ha deprecato una varietà di Threadmetodi, come suspend()e resume(), perché possono bloccare i programmi o danneggiare oggetti. Di conseguenza, non dovresti chiamarli nel codice. Consultare la documentazione dell'SDK per soluzioni alternative a questi metodi. Non tratterò i metodi deprecati in questa serie.

Costruire fili

Threadha otto costruttori. I più semplici sono:

  • Thread(), che crea un Threadoggetto con un nome predefinito
  • Thread(String name), che crea un Threadoggetto con un nome namespecificato dall'argomento

I successivi costruttori più semplici sono Thread(Runnable target)e Thread(Runnable target, String name). A parte i Runnableparametri, questi costruttori sono identici ai suddetti costruttori. La differenza: i Runnableparametri identificano gli oggetti esterni Threadche forniscono i run()metodi. (Si impara a conoscere Runnablepiù avanti in questo articolo.) Gli ultimi quattro costruttori assomigliano Thread(String name), Thread(Runnable target)e Thread(Runnable target, String name); tuttavia, i costruttori finali includono anche un ThreadGroupargomento per scopi organizzativi.

Uno degli ultimi quattro costruttori,, Thread(ThreadGroup group, Runnable target, String name, long stackSize)è interessante in quanto consente di specificare la dimensione desiderata dello stack di chiamate al metodo del thread. Essere in grado di specificare tale dimensione si rivela utile nei programmi con metodi che utilizzano la ricorsione, una tecnica di esecuzione in base alla quale un metodo chiama ripetutamente se stesso, per risolvere con eleganza determinati problemi. Impostando esplicitamente la dimensione dello stack, a volte è possibile impedire StackOverflowErrors. Tuttavia, una dimensione troppo grande può causare OutOfMemoryErrors. Inoltre, Sun considera la dimensione dello stack di chiamate al metodo come dipendente dalla piattaforma. A seconda della piattaforma, la dimensione dello stack di chiamate al metodo potrebbe cambiare. Pertanto, pensa attentamente alle ramificazioni del tuo programma prima di scrivere codice che chiama Thread(ThreadGroup group, Runnable target, String name, long stackSize).

Avvia i tuoi veicoli

I thread assomigliano a veicoli: spostano i programmi dall'inizio alla fine. Threade gli Threadoggetti di sottoclasse non sono thread. Invece, descrivono gli attributi di un thread, come il suo nome, e contengono il codice (tramite un run()metodo) che il thread esegue. Quando arriva il momento di eseguire un nuovo thread run(), un altro thread chiama il metodo Threaddel suo oggetto o della sua sottoclasse start(). Ad esempio, per avviare un secondo thread, il thread iniziale dell'applicazione, che viene eseguito, main()chiama start(). In risposta, il codice di gestione dei thread della JVM funziona con la piattaforma per garantire che il thread venga inizializzato correttamente e chiami Threadil run()metodo di un oggetto o della sua sottoclasse .

Al start()termine, vengono eseguiti più thread. Poiché tendiamo a pensare in modo lineare, spesso troviamo difficile comprendere l'attività concorrente (simultanea) che si verifica quando due o più thread sono in esecuzione. Pertanto, dovresti esaminare un grafico che mostra dove viene eseguito un thread (la sua posizione) rispetto al tempo. La figura seguente presenta un tale grafico.

Il grafico mostra diversi periodi di tempo significativi:

  • Inizializzazione del thread iniziale
  • Il momento in cui il thread inizia a essere eseguito main()
  • Il momento in cui il thread inizia a essere eseguito start()
  • Il momento start()crea un nuovo filo e torna amain()
  • Inizializzazione del nuovo thread
  • Nel momento in cui inizia l'esecuzione del nuovo thread run()
  • I diversi momenti in cui ogni thread termina

Si noti che l'inizializzazione del nuovo thread, la sua esecuzione run()e la sua terminazione avvengono simultaneamente con l'esecuzione del thread iniziale. Si noti inoltre che dopo la chiamata di un thread start(), le chiamate successive a quel metodo prima che il run()metodo esca causano il start()lancio di un java.lang.IllegalThreadStateExceptionoggetto.

Cosa c'è in un nome?

During a debugging session, distinguishing one thread from another in a user-friendly fashion proves helpful. To differentiate among threads, Java associates a name with a thread. That name defaults to Thread, a hyphen character, and a zero-based integer number. You can accept Java's default thread names or you can choose your own. To accommodate custom names, Thread provides constructors that take name arguments and a setName(String name) method. Thread also provides a getName() method that returns the current name. Listing 2 demonstrates how to establish a custom name via the Thread(String name) constructor and retrieve the current name in the run() method by calling 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 ()); } }

You can pass an optional name argument to MyThread on the command line. For example, java NameThatThread X establishes X as the thread's name. If you fail to specify a name, you'll see the following output:

My name is: Thread-1

If you prefer, you can change the super (name); call in the MyThread (String name) constructor to a call to setName (String name)—as in setName (name);. That latter method call achieves the same objective—establishing the thread's name—as super (name);. I leave that as an exercise for you.

Naming main

Java assigns the name main to the thread that runs the main() method, the starting thread. You typically see that name in the Exception in thread "main" message that the JVM's default exception handler prints when the starting thread throws an exception object.

To sleep or not to sleep

Later in this column, I will introduce you to animation— repeatedly drawing on one surface images that slightly differ from each other to achieve a movement illusion. To accomplish animation, a thread must pause during its display of two consecutive images. Calling Thread's static sleep(long millis) method forces a thread to pause for millis milliseconds. Another thread could possibly interrupt the sleeping thread. If that happens, the sleeping thread awakes and throws an InterruptedException object from the sleep(long millis) method. As a result, code that calls sleep(long millis) must appear within a try block—or the code's method must include InterruptedException in its throws clause.

Per dimostrare sleep(long millis), ho scritto una CalcPI1domanda. Quell'applicazione avvia un nuovo thread che utilizza un algoritmo matematico per calcolare il valore della costante matematica pi. Mentre il nuovo thread esegue il calcolo, il thread iniziale si ferma per 10 millisecondi chiamando sleep(long millis). Dopo il risveglio del thread iniziale, stampa il valore pi, che il nuovo thread memorizza nella variabile pi. Il listato 3 presenta CalcPI1il codice sorgente di:

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

Se esegui questo programma, vedrai un output simile (ma probabilmente non identico) al seguente:

pi = -0.2146197014017295 Finished calculating PI