introduzione
Il multithreading è stato integrato in Java sin dall'inizio. Quindi, diamo un'occhiata brevemente a questa cosa chiamata multithreading.
![Meglio insieme: Java e la classe Thread. Parte I - Fili di esecuzione - 1]()
Prendiamo come punto di riferimento la lezione ufficiale di Oracle: "
Lesson: The "Hello World!" Application ". Modificheremo leggermente il codice del nostro programma Hello World come segue:
class HelloWorldApp {
public static void main(String[] args) {
System.out.println("Hello, " + args[0]);
}
}
args
è un array di parametri di input passati all'avvio del programma. Salva questo codice in un file con un nome che corrisponda al nome della classe e con estensione
.java
. Compilalo utilizzando l' utility
javacjavac HelloWorldApp.java
: . Quindi, eseguiamo il nostro codice con alcuni parametri, ad esempio "Roger":
java HelloWorldApp Roger
![Meglio insieme: Java e la classe Thread. Parte I - Fili di esecuzione - 2]()
il nostro codice attualmente presenta un grave difetto. Se non passi alcun argomento (cioè esegui solo "java HelloWorldApp"), otteniamo un errore:
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 0
at HelloWorldApp.main(HelloWorldApp.java:3)
Si è verificata un'eccezione (ovvero un errore) nel thread denominato "main". Quindi, Java ha dei thread? È qui che inizia il nostro viaggio.
Java e thread
Per capire cos'è un thread, devi capire come si avvia un programma Java. Modifichiamo il nostro codice come segue:
class HelloWorldApp {
public static void main(String[] args) {
while (true) {
}
}
}
Ora compiliamolo di nuovo con
javac
. Per comodità, eseguiremo il nostro codice Java in una finestra separata. Su Windows, questo può essere fatto in questo modo:
start java HelloWorldApp
. Ora useremo l' utility
jps per vedere quali informazioni può fornirci Java:
![Meglio insieme: Java e la classe Thread. Parte I - Fili di esecuzione - 3]()
Il primo numero è il PID o Process ID. Cos'è un processo?
A process is a combination of code and data sharing a common virtual address space.
Con i processi, diversi programmi sono isolati l'uno dall'altro durante l'esecuzione: ogni applicazione utilizza la propria area di memoria senza interferire con altri programmi. Per saperne di più, consiglio di leggere questo tutorial:
Processi e Thread . Un processo non può esistere senza un thread, quindi se esiste un processo, allora ha almeno un thread. Ma come avviene questo in Java? Quando avviamo un programma Java, l'esecuzione inizia con il
main
metodo. È come se stessimo entrando nel programma, quindi questo
main
metodo speciale è chiamato punto di ingresso. Il
main
metodo deve essere sempre "public static void", in modo che la Java virtual machine (JVM) possa avviare l'esecuzione del nostro programma. Per ulteriori informazioni,
Perché il metodo principale Java è statico?. Si scopre che il launcher Java (java.exe o javaw.exe) è una semplice applicazione C: carica le varie DLL che compongono effettivamente la JVM. Il programma di avvio Java effettua un set specifico di chiamate JNI (Java Native Interface). JNI è un meccanismo per connettere il mondo della macchina virtuale Java con il mondo del C++. Quindi, il programma di avvio non è la JVM stessa, ma piuttosto un meccanismo per caricarla. Conosce i comandi corretti da eseguire per avviare la JVM. Sa come utilizzare le chiamate JNI per configurare l'ambiente necessario. L'impostazione di questo ambiente include la creazione del thread principale, chiamato "principale", ovviamente. Per illustrare meglio quali thread esistono in un processo Java, utilizziamo il
metodo jvisualvmstrumento, che è incluso con JDK. Conoscendo il pid di un processo, possiamo vedere immediatamente le informazioni su quel processo:
jvisualvm --openpid <process id>
![Meglio insieme: Java e la classe Thread. Parte I - Fili di esecuzione - 4]()
è interessante notare che ogni thread ha la propria area separata nella memoria allocata al processo. Questa struttura di memoria è chiamata stack. Una pila è composta da frame. Un frame rappresenta l'attivazione di un metodo (una chiamata di metodo non terminata). Un frame può anche essere rappresentato come StackTraceElement (vedere l'API Java per
StackTraceElement ). Puoi trovare maggiori informazioni sulla memoria assegnata a ciascun thread nella discussione qui: "
In che modo Java (JVM) alloca lo stack per ciascun thread ". Se guardi l'
API Java e cerchi la parola "Thread", troverai
java.lang.Threadclasse. Questa è la classe che rappresenta un thread in Java e dovremo lavorare con essa.
java.lang.Thread
In Java, un thread è rappresentato da un'istanza della
java.lang.Thread
classe. Dovresti capire immediatamente che le istanze della classe Thread non sono esse stesse thread di esecuzione. Questa è solo una sorta di API per i thread di basso livello gestiti dalla JVM e dal sistema operativo. Quando avviamo la JVM utilizzando il launcher Java, crea un
main
thread chiamato "main" e alcuni altri thread di pulizia. Come indicato nel JavaDoc per la classe Thread:
When a Java Virtual Machine starts up, there is usually a single non-daemon thread
. Esistono 2 tipi di thread: demoni e non demoni. I thread daemon sono thread in background (pulizie) che svolgono un lavoro in background. La parola "daemon" si riferisce al demone di Maxwell. Puoi saperne di più in questo
articolo di Wikipedia . Come indicato nella documentazione, la JVM continua l'esecuzione del programma (processo) fino a quando:
- Viene chiamato il metodo Runtime.exit()
- Tutti i thread NON-daemon terminano il proprio lavoro (senza errori o con eccezioni generate)
Ne consegue un dettaglio importante: i thread daemon possono essere terminati in qualsiasi momento. Di conseguenza, non ci sono garanzie circa l'integrità dei loro dati. Di conseguenza, i thread daemon sono adatti per determinate attività di pulizia. Ad esempio, Java ha un thread responsabile dell'elaborazione
finalize()
delle chiamate ai metodi, ovvero i thread coinvolti nel Garbage Collector (gc). Ogni thread fa parte di un gruppo (
ThreadGroup ). E i gruppi possono far parte di altri gruppi, formando una certa gerarchia o struttura.
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());
}
I gruppi portano ordine nella gestione dei thread. Oltre ai gruppi, i thread hanno il proprio gestore di eccezioni. Dai un'occhiata a un esempio:
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);
}
La divisione per zero causerà un errore che verrà rilevato dal gestore. Se non specifichi il tuo gestore, la JVM richiamerà il gestore predefinito, che restituirà l'analisi dello stack dell'eccezione a StdError. Ogni thread ha anche una priorità. Puoi leggere ulteriori informazioni sulle priorità in questo articolo:
Java Thread Priority in Multithreading .
Creazione di un filo
Come affermato nella documentazione, abbiamo 2 modi per creare un thread. Il primo modo è creare la tua sottoclasse. Per esempio:
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();
}
}
Come puoi vedere, il lavoro dell'attività avviene nel
run()
metodo, ma il thread stesso viene avviato nel
start()
metodo. Non confondere questi metodi: se chiamiamo
un()
direttamente il metodo r, non verrà avviato alcun nuovo thread. È il
start()
metodo che chiede alla JVM di creare un nuovo thread. Questa opzione in cui ereditiamo Thread è già negativa in quanto includiamo Thread nella nostra gerarchia di classi. Il secondo inconveniente è che stiamo iniziando a violare il principio della "responsabilità unica". Cioè, la nostra classe è contemporaneamente responsabile del controllo del thread e di alcune attività da eseguire in questo thread. Qual è il modo giusto? La risposta si trova nello stesso
run()
metodo, che sovrascriviamo:
public void run() {
if (target != null) {
target.run();
}
}
Ecco
target
alcuni
java.lang.Runnable
, che possiamo passare durante la creazione di un'istanza della classe Thread. Ciò significa che possiamo fare questo:
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
è stata anche un'interfaccia funzionale da Java 1.8. Ciò rende possibile scrivere codice ancora più bello per l'attività di un thread:
public static void main(String[] args) {
Runnable task = () -> {
System.out.println("Hello, World!");
};
Thread thread = new Thread(task);
thread.start();
}
Conclusione
Spero che questa discussione chiarisca cos'è un thread, come nascono i thread e quali operazioni di base possono essere eseguite con i thread. Nella
parte successiva cercheremo di capire come i thread interagiscono tra loro ed esploreremo il ciclo di vita dei thread.
Meglio insieme: Java e la classe Thread. Parte II — Sincronizzazione Meglio insieme: Java e la classe Thread. Parte III — Interazione Meglio insieme: Java e la classe Thread. Parte IV — Callable, Future e friends Better together: Java e la classe Thread. Parte V — Executor, ThreadPool, Fork/Join Better insieme: Java e la classe Thread. Parte VI - Spara via!
GO TO FULL VERSION