Threading moderno: un primer sulla concorrenza Java

Molto di ciò che c'è da imparare sulla programmazione con thread Java non è cambiato radicalmente durante l'evoluzione della piattaforma Java, ma è cambiato in modo incrementale. In questo manuale sui thread Java, Cameron Laird colpisce alcuni dei punti alti (e bassi) dei thread come tecnica di programmazione concorrente. Ottieni una panoramica di ciò che è perennemente impegnativo nella programmazione multithread e scopri come si è evoluta la piattaforma Java per affrontare alcune delle sfide.

La concorrenza è tra le maggiori preoccupazioni per i nuovi arrivati ​​alla programmazione Java, ma non c'è motivo di lasciarti scoraggiare. Non solo è disponibile un'eccellente documentazione (esploreremo diverse fonti in questo articolo) ma è diventato più facile lavorare con i thread Java man mano che la piattaforma Java si è evoluta. Per imparare a fare la programmazione multithread in Java 6 e 7, hai davvero bisogno solo di alcuni elementi costitutivi. Inizieremo con questi:

  • Un semplice programma a thread
  • Il threading è tutto basato sulla velocità, giusto?
  • Sfide della concorrenza Java
  • Quando usare Runnable
  • Quando i buoni thread vanno male
  • Novità di Java 6 e 7
  • Quali sono le prospettive per i thread Java

Questo articolo è un'indagine per principianti sulle tecniche di threading Java, inclusi collegamenti ad alcuni degli articoli introduttivi letti più di frequente di JavaWorld sulla programmazione multithread. Avvia i motori e segui i collegamenti sopra se sei pronto per iniziare a conoscere il threading Java oggi.

Un semplice programma a thread

Considera la seguente fonte Java.

Listato 1. FirstThreadingExample

class FirstThreadingExample { public static void main (String [] args) { // The second argument is a delay between // successive outputs. The delay is // measured in milliseconds. "10", for // instance, means, "print a line every // hundredth of a second". ExampleThread mt = new ExampleThread("A", 31); ExampleThread mt2 = new ExampleThread("B", 25); ExampleThread mt3 = new ExampleThread("C", 10); mt.start(); mt2.start(); mt3.start(); } } class ExampleThread extends Thread { private int delay; public ExampleThread(String label, int d) { // Give this particular thread a // name: "thread 'LABEL'". super("thread '" + label + "'"); delay = d; } public void run () { for (int count = 1, row = 1; row < 20; row++, count++) { try { System.out.format("Line #%d from %s\n", count, getName()); Thread.currentThread().sleep(delay); } catch (InterruptedException ie) { // This would be a surprise. } } } }

Ora compila ed esegui questo sorgente come faresti con qualsiasi altra applicazione della riga di comando Java. Vedrai un output simile a questo:

Listato 2. Output di un programma a thread

Line #1 from thread 'A' Line #1 from thread 'C' Line #1 from thread 'B' Line #2 from thread 'C' Line #3 from thread 'C' Line #2 from thread 'B' Line #4 from thread 'C' ... Line #17 from thread 'B' Line #14 from thread 'A' Line #18 from thread 'B' Line #15 from thread 'A' Line #19 from thread 'B' Line #16 from thread 'A' Line #17 from thread 'A' Line #18 from thread 'A' Line #19 from thread 'A'

Questo è tutto: sei un Threadprogrammatore Java !

Va bene, forse non così in fretta. Per quanto piccolo sia il programma nel Listato 1, contiene alcune sottigliezze che meritano la nostra attenzione.

Discussioni e indeterminatezza

Un tipico ciclo di apprendimento con programmazione consiste di quattro fasi: (1) studio di un nuovo concetto; (2) eseguire il programma di esempio; (3) confrontare l'output con le aspettative; e (4) iterare finché i due non corrispondono. Nota, tuttavia, che in precedenza ho detto che l'output per FirstThreadingExamplesarebbe stato "qualcosa come" Listato 2. Quindi, questo significa che il tuo output potrebbe essere diverso dal mio, riga per riga. Che cosa è che circa?

Nei programmi Java più semplici, c'è una garanzia di ordine di esecuzione: main()verrà eseguita per prima la prima riga in entrata, poi la successiva e così via, con la traccia appropriata in entrata e in uscita da altri metodi. Threadindebolisce quella garanzia.

Il threading porta nuovo potere alla programmazione Java; puoi ottenere risultati con thread di cui non potresti fare a meno. Ma quel potere arriva a costo della determinazione . Nei programmi Java più semplici, c'è una garanzia di ordine di esecuzione: main()verrà eseguita per prima la prima riga in entrata, poi la successiva e così via, con la traccia appropriata dentro e fuori da altri metodi. Threadindebolisce quella garanzia. In un programma multithread, " Line #17 from thread B" potrebbe apparire sullo schermo prima o dopo " Line #14 from thread A," e l'ordine potrebbe differire nelle successive esecuzioni dello stesso programma, anche sullo stesso computer.

L'indeterminatezza può non essere familiare, ma non deve essere disturbante. L'ordine di esecuzione all'interno di un thread rimane prevedibile e ci sono anche vantaggi associati all'indeterminatezza. Potresti aver sperimentato qualcosa di simile lavorando con le interfacce utente grafiche (GUI). I listener di eventi in Swing oi gestori di eventi in HTML sono esempi.

Sebbene una discussione completa sulla sincronizzazione dei thread esuli dall'ambito di questa introduzione, è facile spiegarne le basi.

Ad esempio, considera i meccanismi di come HTML specifica ... onclick = "myFunction();" ...per determinare l'azione che avverrà dopo che l'utente fa clic. Questo caso familiare di indeterminatezza illustra alcuni dei suoi vantaggi. In questo caso, myFunction()non viene eseguito in un momento definito rispetto ad altri elementi del codice sorgente, ma in relazione all'azione dell'utente finale . Quindi l'indeterminatezza non è solo una debolezza del sistema; è anche un arricchimento del modello di esecuzione, che offre al programmatore nuove opportunità per determinare sequenza e dipendenza.

Ritardi di esecuzione e sottoclassi di thread

Puoi imparare FirstThreadingExamplesperimentando da solo. Prova ad aggiungere o rimuovere ExampleThreads, ovvero invocazioni di costruttori come ... new ExampleThread(label, delay);, e ad armeggiare con delays. L'idea di base è che il programma inizi tre distinti Thread, che poi vengono eseguiti indipendentemente fino al completamento. Per rendere la loro esecuzione più istruttiva, ognuna ritarda leggermente tra le righe successive che scrive in output; questo dà agli altri thread la possibilità di scrivere il loro output.

Si noti che la Threadprogrammazione basata su -based non richiede, in generale, la gestione di un file InterruptedException. Quello mostrato in FirstThreadingExampleha a che fare con sleep(), piuttosto che essere direttamente correlato Thread. La Threadfonte più basata non include un sleep(); lo scopo di sleep()qui è quello di modellare, in modo semplice, il comportamento di metodi di lunga durata trovati "in natura".

Un'altra cosa da notare nel Listato 1 è che Threadè una classe astratta , progettata per essere sottoclasse. Il suo run()metodo predefinito non fa nulla, quindi deve essere sovrascritto nella definizione della sottoclasse per ottenere qualcosa di utile.

Si tratta di velocità, giusto?

Quindi ormai puoi vedere un po 'di ciò che rende complessa la programmazione con i thread. Ma il punto principale per sopportare tutte queste difficoltà non è guadagnare velocità.

Programmi multithreaded non , in generale, completano più velocemente di quelli a thread singolo - in realtà possono essere significativamente più lento nei casi patologici. Il valore aggiunto fondamentale dei programmi multithread è la reattività . Quando più core di elaborazione sono disponibili per la JVM, o quando il programma impiega molto tempo ad aspettare su più risorse esterne come le risposte di rete, il multithreading può aiutare il programma a completare più velocemente.

Pensa a un'applicazione GUI: se risponde ancora ai punti e ai clic dell'utente finale durante la ricerca "in background" di un'impronta digitale corrispondente o il ricalcolo del calendario per il torneo di tennis del prossimo anno, allora è stata costruita pensando alla concorrenza. Una tipica architettura di applicazioni simultanee mette il riconoscimento e la risposta alle azioni dell'utente in un thread separato dal thread di calcolo assegnato per gestire il grande carico di back-end. (Vedere "Threading swing e thread di invio di eventi" per un'ulteriore illustrazione di questi principi.)

Nella tua programmazione, quindi, è molto probabile che tu prenda in considerazione l'utilizzo di Threads in una di queste circostanze:

  1. Un'applicazione esistente ha la funzionalità corretta ma a volte non risponde. Questi "blocchi" hanno spesso a che fare con risorse esterne al di fuori del tuo controllo: query di database che richiedono molto tempo, calcoli complicati, riproduzione multimediale o risposte in rete con latenza incontrollabile.
  2. Un'applicazione ad alta intensità di calcolo potrebbe fare un uso migliore degli host multicore. Questo potrebbe essere il caso di qualcuno che riproduce grafici complessi o simula un modello scientifico coinvolto.
  3. Threadesprime naturalmente il modello di programmazione richiesto dall'applicazione. Supponiamo, ad esempio, di modellare il comportamento degli automobilisti delle ore di punta o delle api in un alveare. Implementare ogni driver o ape come un Threadoggetto correlato potrebbe essere conveniente dal punto di vista della programmazione, a parte qualsiasi considerazione di velocità o reattività.

Sfide della concorrenza Java

Il programmatore esperto Ned Batchelder ha recentemente scherzato

Alcune persone, quando si trovano di fronte a un problema, pensano: "Lo so, userò i thread", e poi due hanno erpoblesmi.

È divertente perché modella così bene il problema con la concorrenza. Come ho già detto, è probabile che i programmi multithread forniscano risultati diversi in termini di sequenza o tempistica esatta dell'esecuzione del thread. Questo è preoccupante per i programmatori, che sono addestrati a pensare in termini di risultati riproducibili, rigida determinazione e sequenza invariante.

La situazione peggiora. Diversi thread potrebbero non solo produrre risultati in diversi ordini, ma possono contendersi a livelli più essenziali per i risultati. È facile per un nuovo arrivato eseguire il multithreading su close()un handle di file in uno Threadprima che un altro Threadabbia finito tutto ciò di cui ha bisogno per scrivere.

Test di programmi concorrenti

Dieci anni fa su JavaWorld, Dave Dyer notò che il linguaggio Java aveva una caratteristica così "usata in modo così pervasivo in modo errato" da classificarlo come un grave difetto di progettazione. Quella caratteristica era il multithreading.

Il commento di Dyer evidenzia la sfida di testare i programmi multithread. Quando non è più possibile specificare facilmente l'output di un programma in termini di una sequenza definita di caratteri, ci sarà un impatto sull'efficacia con cui testare il codice threaded.

The correct starting point to resolving the intrinsic difficulties of concurrent programming was well stated by Heinz Kabutz in his Java Specialist newsletter: recognize that concurrency is a topic that you should understand and study it systematically. There are of course tools such as diagramming techniques and formal languages that will help. But the first step is to sharpen your intuition by practicing with simple programs like FirstThreadingExample in Listing 1. Next, learn as much as you can about threading fundamentals like these:

  • Synchronization and immutable objects
  • Thread scheduling and wait/notify
  • Race conditions and deadlock
  • Thread monitors for exclusive access, conditions, and assertions
  • JUnit best practices -- testing multithreaded code

When to use Runnable

Object orientation in Java defines singly inherited classes, which has consequences for multithreading coding. To this point, I have only described a use for Thread that was based on subclasses with an overridden run(). In an object design that already involved inheritance, this simply wouldn't work. You cannot simultaneously inherit from RenderedObject or ProductionLine or MessageQueue alongside Thread!

This constraint affects many areas of Java, not just multithreading. Fortunately, there's a classical solution for the problem, in the form of the Runnable interface. As explained by Jeff Friesen in his 2002 introduction to threading, the Runnable interface is made for situations where subclassing Thread isn't possible:

L' Runnableinterfaccia di dichiara una sola firma del metodo: void run();. Che la firma è identica a Thread's run()firma del metodo e serve come entrata di un thread di esecuzione. Poiché Runnableè un'interfaccia, qualsiasi classe può implementare quell'interfaccia allegando una implementsclausola all'intestazione della classe e fornendo un run()metodo appropriato . Al momento dell'esecuzione, il codice del programma può creare un oggetto, o eseguibile , da quella classe e passare il riferimento dell'eseguibile a un Threadcostruttore appropriato .

Quindi, per quelle classi che non possono estendersi Thread, è necessario creare un eseguibile per sfruttare il multithreading. Semanticamente, se stai facendo programmazione a livello di sistema e la tua classe è in una relazione è-un Thread, allora dovresti sottoclassare direttamente da Thread. Ma la maggior parte dell'uso a livello di applicazione del multithreading si basa sulla composizione e quindi definisce un Runnablediagramma di classe compatibile con l'applicazione. Fortunatamente, sono necessarie solo una o due righe in più per codificare utilizzando l' Runnableinterfaccia, come mostrato nel Listato 3 di seguito.