Java 101: comprensione dei thread Java, parte 3: pianificazione dei thread e attesa / notifica

Questo mese, continuo la mia introduzione in quattro parti ai thread Java concentrandomi sulla pianificazione dei thread, sul meccanismo di attesa / notifica e sull'interruzione del thread. Indagherai su come una JVM o uno scheduler di thread del sistema operativo sceglie il thread successivo per l'esecuzione. Come scoprirai, la priorità è importante per la scelta di uno scheduler di thread. Esaminerai come un thread attende fino a quando non riceve una notifica da un altro thread prima di continuare l'esecuzione e imparerai come utilizzare il meccanismo di attesa / notifica per coordinare l'esecuzione di due thread in una relazione produttore-consumatore. Infine, imparerai come risvegliare prematuramente un thread addormentato o in attesa per la terminazione del thread o altre attività. Ti insegnerò anche come un thread che non è né dormiente né in attesa rileva una richiesta di interruzione da un altro 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, attesa / notifica e interruzione del thread
  • Parte 4: gruppi di thread, volatilità, variabili locali del thread, timer e morte del thread

Pianificazione dei thread

In un mondo idealizzato, tutti i thread del programma avrebbero i propri processori su cui eseguire. Fino al momento in cui i computer dispongono di migliaia o milioni di processori, i thread spesso devono condividere uno o più processori. O la JVM o il sistema operativo della piattaforma sottostante decifra come condividere la risorsa del processore tra i thread, un'attività nota come pianificazione dei thread . Quella parte della JVM o del sistema operativo che esegue la pianificazione dei thread è uno scheduler dei thread .

Nota: per semplificare la discussione sulla pianificazione dei thread, mi concentro sulla pianificazione dei thread nel contesto di un singolo processore. È possibile estrapolare questa discussione a più processori; Lascio a te questo compito.

Ricorda due punti importanti sulla pianificazione dei thread:

  1. Java non forza una VM a pianificare i thread in un modo specifico o contiene uno scheduler di thread. Ciò implica la pianificazione dei thread dipendente dalla piattaforma. Pertanto, è necessario prestare attenzione durante la scrittura di un programma Java il cui comportamento dipende dalla modalità di pianificazione dei thread e deve operare in modo coerente su piattaforme diverse.
  2. Fortunatamente, quando si scrivono programmi Java, è necessario pensare a come Java pianifica i thread solo quando almeno uno dei thread del programma utilizza pesantemente il processore per lunghi periodi di tempo ei risultati intermedi dell'esecuzione di quel thread si dimostrano importanti. Ad esempio, un'applet contiene un thread che crea dinamicamente un'immagine. Periodicamente, si desidera che il thread di pittura disegni il contenuto corrente di quell'immagine in modo che l'utente possa vedere come procede l'immagine. Per garantire che il thread di calcolo non monopolizzi il processore, considerare la pianificazione del thread.

Esaminare un programma che crea due thread ad alta intensità di processore:

Listato 1. SchedDemo.java

// SchedDemo.java class SchedDemo { public static void main (String [] args) { new CalcThread ("CalcThread A").start (); new CalcThread ("CalcThread B").start (); } } class CalcThread extends Thread { CalcThread (String name) { // Pass name to Thread layer. super (name); } double calcPI () { boolean negative = true; double pi = 0.0; 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; return pi; } public void run () { for (int i = 0; i < 5; i++) System.out.println (getName () + ": " + calcPI ()); } }

SchedDemocrea due thread che calcolano ciascuno il valore di pi greco (cinque volte) e stampa ogni risultato. A seconda di come la tua implementazione JVM pianifica i thread, potresti vedere un output simile al seguente:

CalcThread A: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894

Secondo l'output precedente, lo scheduler dei thread condivide il processore tra entrambi i thread. Tuttavia, potresti vedere un output simile a questo:

CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894

L'output sopra mostra lo scheduler dei thread che preferisce un thread rispetto a un altro. I due output sopra illustrano due categorie generali di scheduler di thread: verde e nativo. Esplorerò le loro differenze comportamentali nelle prossime sezioni. Mentre discuto di ciascuna categoria, mi riferisco agli stati dei thread, di cui ce ne sono quattro:

  1. Stato iniziale: un programma ha creato l'oggetto thread di un thread, ma il thread non esiste ancora perché il start()metodo dell'oggetto thread non è stato ancora chiamato.
  2. Stato eseguibile: questo è lo stato predefinito di un thread. Dopo il start()completamento della chiamata a , un thread diventa eseguibile indipendentemente dal fatto che il thread sia in esecuzione o meno, ovvero utilizzando il processore. Sebbene molti thread possano essere eseguibili, solo uno viene attualmente eseguito. Gli scheduler dei thread determinano quale thread eseguibile assegnare al processore.
  3. Stato di blocco: Quando un thread esegue i sleep(), wait()o join()metodi, quando un filo tentativi di leggere dati non ancora disponibili da una rete, e quando un thread attende di acquisire un blocco, quel filo è nello stato bloccato: non è né esecuzione né in grado di correre. (Probabilmente puoi pensare ad altre volte in cui un thread attende che accada qualcosa.) Quando un thread bloccato si sblocca, quel thread si sposta nello stato eseguibile.
  4. Stato di terminazione: una volta che l'esecuzione lascia il run()metodo di un thread , quel thread è nello stato di terminazione. In altre parole, il filo cessa di esistere.

In che modo lo scheduler dei thread sceglie quale thread eseguibile eseguire? Comincio a rispondere a questa domanda mentre discuto della pianificazione del thread verde. Termino la risposta mentre discuto della pianificazione dei thread nativi.

Pianificazione del filo verde

Non tutti i sistemi operativi, ad esempio l'antico sistema operativo Microsoft Windows 3.1, supportano i thread. Per tali sistemi, Sun Microsystems può progettare una JVM che divide il suo unico thread di esecuzione in più thread. La JVM (non il sistema operativo della piattaforma sottostante) fornisce la logica di threading e contiene lo scheduler del thread. I thread JVM sono thread verdi o thread utente .

Lo scheduler dei thread di una JVM pianifica i thread verdi in base alla priorità , ovvero l'importanza relativa di un thread, che si esprime come numero intero da un intervallo di valori ben definito. In genere, lo scheduler di thread di una JVM sceglie il thread con la priorità più alta e consente a quel thread di essere eseguito finché non termina o si blocca. A quel punto, lo scheduler dei thread sceglie un thread con la priorità successiva più alta. Quel thread (di solito) viene eseguito fino a quando non termina o si blocca. Se, mentre viene eseguito un filo, un filo di sblocca priorità superiore (forse tempo di sonno del thread con priorità maggiore scaduto), i fili scheduler ha precedenza, o interrupt, il filo di priorità inferiore e assegna il sbloccato filo con priorità più alta al processore.

Nota: un thread eseguibile con la priorità più alta non verrà sempre eseguito. Ecco la priorità delle specifiche del linguaggio Java :

Ogni thread ha una priorità. Quando c'è competizione per le risorse di elaborazione, i thread con priorità più alta vengono generalmente eseguiti preferibilmente rispetto ai thread con priorità più bassa. Tale preferenza non è, tuttavia, una garanzia che il thread con la priorità più alta sarà sempre in esecuzione e le priorità dei thread non possono essere utilizzate per implementare in modo affidabile l'esclusione reciproca.

Questa ammissione la dice lunga sull'implementazione delle JVM green thread. Quelle JVM non possono permettersi di lasciare che i thread si blocchino perché ciò legherebbe l'unico thread di esecuzione della JVM. Pertanto, quando un thread deve bloccarsi, ad esempio quando il thread legge i dati che arrivano lentamente da un file, la JVM potrebbe interrompere l'esecuzione del thread e utilizzare un meccanismo di polling per determinare quando arrivano i dati. Mentre il thread rimane interrotto, lo scheduler thread della JVM potrebbe pianificare l'esecuzione di un thread con priorità inferiore. Supponiamo che i dati arrivino mentre il thread con priorità inferiore è in esecuzione. Sebbene il thread con priorità più alta dovrebbe essere eseguito non appena arrivano i dati, ciò non accade fino a quando la JVM non esegue il polling successivo del sistema operativo e scopre l'arrivo. Pertanto, il thread con priorità più bassa viene eseguito anche se deve essere eseguito il thread con priorità più alta.È necessario preoccuparsi di questa situazione solo quando è necessario un comportamento in tempo reale da Java. Ma allora Java non è un sistema operativo in tempo reale, quindi perché preoccuparsi?

Per capire quale thread verde eseguibile diventa il thread verde attualmente in esecuzione, considerare quanto segue. Supponiamo che la tua applicazione sia composta da tre thread: il thread principale che esegue il main()metodo, un thread di calcolo e un thread che legge l'input da tastiera. Quando non è presente alcun input da tastiera, il thread di lettura si blocca. Si supponga che il thread di lettura abbia la priorità più alta e il thread di calcolo abbia la priorità più bassa. (Per semplicità, si supponga anche che non siano disponibili altri thread JVM interni.) La Figura 1 illustra l'esecuzione di questi tre thread.

Al momento T0, il thread principale inizia a funzionare. All'istante T1, il thread principale avvia il thread di calcolo. Poiché il thread di calcolo ha una priorità inferiore rispetto al thread principale, il thread di calcolo attende il processore. Al momento T2, il thread principale avvia il thread di lettura. Poiché il thread di lettura ha una priorità maggiore rispetto al thread principale, il thread principale attende il processore durante l'esecuzione del thread di lettura. Al momento T3, il thread di lettura si blocca e il thread principale viene eseguito. Al momento T4, il thread di lettura si sblocca e viene eseguito; il thread principale attende. Infine, al tempo T5, il thread di lettura si blocca e viene eseguito il thread principale. Questa alternanza nell'esecuzione tra la lettura e il thread principale continua finché il programma viene eseguito. Il thread di calcolo non viene mai eseguito perché ha la priorità più bassa e quindi affama l'attenzione del processore,una situazione nota comefame del processore .

Possiamo modificare questo scenario dando al thread di calcolo la stessa priorità del thread principale. La figura 2 mostra il risultato, a partire dal tempo T2. (Prima di T2, la Figura 2 è identica alla Figura 1.)

All'istante T2, il thread di lettura viene eseguito mentre i thread principale e di calcolo attendono il processore. Al tempo T3, il thread di lettura si blocca e il thread di calcolo viene eseguito, perché il thread principale è stato eseguito appena prima del thread di lettura. Al momento T4, il thread di lettura si sblocca e viene eseguito; i thread principale e di calcolo aspettano. Al tempo T5, il thread di lettura si blocca e il thread principale viene eseguito, perché il thread di calcolo è stato eseguito appena prima del thread di lettura. Questa alternanza nell'esecuzione tra i thread principale e di calcolo continua finché il programma viene eseguito e dipende dal thread con priorità più alta in esecuzione e dal blocco.

We must consider one last item in green thread scheduling. What happens when a lower-priority thread holds a lock that a higher-priority thread requires? The higher-priority thread blocks because it cannot get the lock, which implies that the higher-priority thread effectively has the same priority as the lower-priority thread. For example, a priority 6 thread attempts to acquire a lock that a priority 3 thread holds. Because the priority 6 thread must wait until it can acquire the lock, the priority 6 thread ends up with a 3 priority—a phenomenon known as priority inversion.

L'inversione della priorità può ritardare notevolmente l'esecuzione di un thread con priorità più alta. Ad esempio, si supponga di avere tre thread con priorità 3, 4 e 9. Il thread con priorità 3 è in esecuzione e gli altri thread sono bloccati. Supponiamo che il thread con priorità 3 acquisisca un blocco e che il thread con priorità 4 si sblocchi. Il thread con priorità 4 diventa il thread attualmente in esecuzione. Poiché il thread con priorità 9 richiede il blocco, continua ad attendere finché il thread con priorità 3 non rilascia il blocco. Tuttavia, il thread con priorità 3 non può rilasciare il blocco fino a quando il thread con priorità 4 non si blocca o termina. Di conseguenza, il thread con priorità 9 ritarda la sua esecuzione.