Java 101: concorrenza Java senza problemi, parte 1

Con la crescente complessità delle applicazioni simultanee, molti sviluppatori scoprono che le capacità di threading di basso livello di Java non sono sufficienti per le loro esigenze di programmazione. In tal caso, potrebbe essere il momento di scoprire le Java Concurrency Utilities. Inizia con java.util.concurrentl'introduzione dettagliata di Jeff Friesen al framework Executor, ai tipi di sincronizzazione e al pacchetto Java Concurrent Collections.

Java 101: la prossima generazione

Il primo articolo di questa nuova serie JavaWorld introduce l' API Java Date and Time .

La piattaforma Java fornisce funzionalità di threading di basso livello che consentono agli sviluppatori di scrivere applicazioni simultanee in cui diversi thread vengono eseguiti contemporaneamente. Tuttavia, il threading Java standard ha alcuni svantaggi:

  • Primitive di basso livello di concorrenza di Java ( synchronized, volatile, wait(), notify(), e notifyAll()) non sono facili da usare in modo corretto. Anche i rischi di threading come deadlock, fame di thread e condizioni di competizione, che derivano da un uso scorretto delle primitive, sono difficili da rilevare ed eseguire il debug.
  • Affidarsi synchronizedal coordinamento dell'accesso tra i thread porta a problemi di prestazioni che influiscono sulla scalabilità delle applicazioni, un requisito per molte applicazioni moderne.
  • Le funzionalità di threading di base di Java sono di livello troppo basso. Gli sviluppatori spesso necessitano di costrutti di livello superiore come semafori e pool di thread, che le capacità di threading di basso livello di Java non offrono. Di conseguenza, gli sviluppatori costruiranno i propri costrutti, il che richiede tempo ed è soggetto a errori.

Il framework JSR 166: Concurrency Utilities è stato progettato per soddisfare la necessità di una struttura di threading di alto livello. Iniziato all'inizio del 2002, il framework è stato formalizzato e implementato due anni dopo in Java 5. Sono seguiti miglioramenti in Java 6, Java 7 e l'imminente Java 8.

Questa serie in due parti Java 101: The next generation introduce gli sviluppatori di software che hanno familiarità con il threading Java di base nei pacchetti e nel framework di Java Concurrency Utilities. Nella Parte 1, presento una panoramica del framework Java Concurrency Utilities e presento il suo framework Executor, le utilità di sincronizzazione e il pacchetto Java Concurrent Collections.

Capire i thread Java

Prima di immergerti in questa serie, assicurati di conoscere le basi del threading. Inizia con l' introduzione di Java 101 alle funzionalità di threading di basso livello di Java:

  • Parte 1: presentazione di thread ed eseguibili
  • Parte 2: sincronizzazione dei thread
  • 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

All'interno di Java Concurrency Utilities

Il framework Java Concurrency Utilities è una libreria di tipi progettati per essere utilizzati come elementi costitutivi per la creazione di classi o applicazioni simultanee. Questi tipi sono thread-safe, sono stati accuratamente testati e offrono prestazioni elevate.

I tipi nelle Java Concurrency Utilities sono organizzati in piccoli framework; vale a dire, framework Executor, sincronizzatore, raccolte simultanee, blocchi, variabili atomiche e Fork / Join. Sono ulteriormente organizzati in un pacchetto principale e un paio di sottopacchetti:

  • java.util.concurrent contiene tipi di utilità di alto livello comunemente utilizzati nella programmazione simultanea. Gli esempi includono semafori, barriere, pool di thread e hashmap concorrenti.
    • Il sottopacchetto java.util.concurrent.atomic contiene classi di utilità di basso livello che supportano la programmazione thread-safe senza blocchi su singole variabili.
    • Il sottopacchetto java.util.concurrent.locks contiene tipi di utilità di basso livello per il blocco e l'attesa delle condizioni, che sono diversi dall'utilizzo della sincronizzazione e dei monitor di basso livello di Java.

Il framework Java Concurrency Utilities espone anche le istruzioni hardware CAS (compare-and-swap) di basso livello , varianti delle quali sono comunemente supportate dai processori moderni. CAS è molto più leggero del meccanismo di sincronizzazione basato su monitor di Java e viene utilizzato per implementare alcune classi simultanee altamente scalabili. La java.util.concurrent.locks.ReentrantLockclasse basata su CAS , ad esempio, è più performante della synchronizedprimitiva basata su monitor equivalente . ReentrantLockoffre un maggiore controllo sul blocco. (Nella parte 2 spiegherò di più su come funziona CAS java.util.concurrent.)

System.nanoTime ()

Il framework Java Concurrency Utilities include long nanoTime(), che è un membro della java.lang.Systemclasse. Questo metodo consente l'accesso a una sorgente temporale a nanosecondi per effettuare misurazioni del tempo relativo.

Nelle prossime sezioni introdurrò tre utili funzionalità delle Java Concurrency Utilities, spiegando prima perché sono così importanti per la concorrenza moderna e poi dimostrando come funzionano per aumentare la velocità, l'affidabilità, l'efficienza e la scalabilità delle applicazioni Java simultanee.

Il framework Executor

Nel threading, un'attività è un'unità di lavoro. Un problema con il threading di basso livello in Java è che l'invio di attività è strettamente associato a una politica di esecuzione delle attività, come dimostrato dal Listato 1.

Listato 1. Server.java (Versione 1)

import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; class Server { public static void main(String[] args) throws IOException { ServerSocket socket = new ServerSocket(9000); while (true) { final Socket s = socket.accept(); Runnable r = new Runnable() { @Override public void run() { doWork(s); } }; new Thread(r).start(); } } static void doWork(Socket s) { } }

Il codice sopra descrive una semplice applicazione server (con doWork(Socket)lasciato vuoto per brevità). Il thread del server chiama ripetutamente socket.accept()per attendere una richiesta in arrivo e quindi avvia un thread per soddisfare questa richiesta quando arriva.

Poiché questa applicazione crea un nuovo thread per ogni richiesta, non si ridimensiona bene di fronte a un numero enorme di richieste. Ad esempio, ogni thread creato richiede memoria e troppi thread potrebbero esaurire la memoria disponibile, forzando la chiusura dell'applicazione.

È possibile risolvere questo problema modificando la politica di esecuzione delle attività. Invece di creare sempre un nuovo thread, è possibile utilizzare un pool di thread, in cui un numero fisso di thread servirebbe le attività in arrivo. Tuttavia, dovresti riscrivere l'applicazione per apportare questa modifica.

java.util.concurrentinclude il framework Executor, un piccolo framework di tipi che separano l'invio di attività dalle politiche di esecuzione delle attività. Utilizzando il framework Executor, è possibile ottimizzare facilmente la politica di esecuzione delle attività di un programma senza dover riscrivere in modo significativo il codice.

All'interno del framework Executor

Il framework Executor si basa Executorsull'interfaccia, che descrive un esecutore come qualsiasi oggetto in grado di eseguire java.lang.Runnableattività. Questa interfaccia dichiara il seguente metodo solitario per eseguire Runnableun'attività:

void execute(Runnable command)

Si invia Runnableun'attività passandola a execute(Runnable). Se l'esecutore non può eseguire l'attività per qualsiasi motivo (ad esempio, se l'esecutore è stato chiuso), questo metodo genererà un file RejectedExecutionException.

Il concetto chiave è che l' invio di attività è disaccoppiato dalla politica di esecuzioneExecutor dell'attività , che è descritta da un'implementazione. L' attività eseguibile è quindi in grado di essere eseguita tramite un nuovo thread, un thread in pool, il thread chiamante e così via.

Nota che Executorè molto limitato. Ad esempio, non è possibile arrestare un esecutore o determinare se un'attività asincrona è terminata. Inoltre, non è possibile annullare un'attività in esecuzione. Per questi e altri motivi, il framework Executor fornisce un'interfaccia ExecutorService, che si estende Executor.

Cinque dei ExecutorServicemetodi di sono particolarmente degni di nota:

  • booleano awaitTermination (timeout lungo, unità TimeUnit) blocca il thread chiamante fino a quando tutte le attività non hanno completato l'esecuzione dopo una richiesta di arresto, si verifica il timeout o il thread corrente viene interrotto, a seconda di cosa si verifica per primo. Il tempo massimo di attesa è specificato da timeout, e questo valore è espresso nelle unitunità specificate da TimeUnitenum; per esempio TimeUnit.SECONDS,. Questo metodo genera java.lang.InterruptedExceptionquando il thread corrente viene interrotto. Restituisce true quando l'esecutore viene terminato e false quando il timeout scade prima della terminazione.
  • boolean isShutdown () restituisce true quando l'esecutore è stato chiuso.
  • void shutdown () avvia un arresto ordinato in cui vengono eseguite le attività inviate in precedenza ma non vengono accettate nuove attività.
  • Invio futuro (attività richiamabile) invia un'attività di restituzione del valore per l'esecuzione e restituisce un che Futurerappresenta i risultati in sospeso dell'attività.
  • Invio futuro (attività eseguibile) invia Runnableun'attività per l'esecuzione e restituisce un Futuresimbolo che rappresenta tale attività.

L' Futureinterfaccia rappresenta il risultato di un calcolo asincrono. Il risultato è noto come futuro perché in genere non sarà disponibile fino a un certo momento nel futuro. È possibile richiamare metodi per annullare un'attività, restituire il risultato di un'attività (in attesa indefinitamente o che scada un timeout quando l'attività non è terminata) e determinare se un'attività è stata annullata o è terminata.

L' Callableinterfaccia è simile Runnableall'interfaccia in quanto fornisce un unico metodo che descrive un'attività da eseguire. A differenza Runnabledel void run()metodo di, Callableil V call() throws Exceptionmetodo di può restituire un valore e generare un'eccezione.

Metodi di fabbrica esecutore

At some point, you'll want to obtain an executor. The Executor framework supplies the Executors utility class for this purpose. Executors offers several factory methods for obtaining different kinds of executors that offer specific thread-execution policies. Here are three examples:

  • ExecutorService newCachedThreadPool() creates a thread pool that creates new threads as needed, but which reuses previously constructed threads when they're available. Threads that haven't been used for 60 seconds are terminated and removed from the cache. This thread pool typically improves the performance of programs that execute many short-lived asynchronous tasks.
  • ExecutorService newSingleThreadExecutor() creates an executor that uses a single worker thread operating off an unbounded queue -- tasks are added to the queue and execute sequentially (no more than one task is active at any one time). If this thread terminates through failure during execution before shutdown of the executor, a new thread will be created to take its place when subsequent tasks need to be executed.
  • ExecutorService newFixedThreadPool(int nThreads) creates a thread pool that re-uses a fixed number of threads operating off a shared unbounded queue. At most nThreads threads are actively processing tasks. If additional tasks are submitted when all threads are active, they wait in the queue until a thread is available. If any thread terminates through failure during execution before shutdown, a new thread will be created to take its place when subsequent tasks need to be executed. The pool's threads exist until the executor is shut down.

The Executor framework offers additional types (such as the ScheduledExecutorService interface), but the types you are likely to work with most often are ExecutorService, Future, Callable, and Executors.

See the java.util.concurrent Javadoc to explore additional types.

Lavorare con il framework Executor

Scoprirai che il framework Executor è abbastanza facile da lavorare. Nel listato 2, ho usato Executore Executorsper sostituire l'esempio del server dal listato 1 con un'alternativa basata sul pool di thread più scalabile.

Listato 2. Server.java (versione 2)

import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import java.util.concurrent.Executor; import java.util.concurrent.Executors; class Server { static Executor pool = Executors.newFixedThreadPool(5); public static void main(String[] args) throws IOException { ServerSocket socket = new ServerSocket(9000); while (true) { final Socket s = socket.accept(); Runnable r = new Runnable() { @Override public void run() { doWork(s); } }; pool.execute(r); } } static void doWork(Socket s) { } }

Il Listato 2 utilizza newFixedThreadPool(int)per ottenere un esecutore basato su pool di thread che riutilizza cinque thread. Sostituisce anche new Thread(r).start();con pool.execute(r);per eseguire attività eseguibili tramite uno qualsiasi di questi thread.

Il listato 3 presenta un altro esempio in cui un'applicazione legge il contenuto di una pagina web arbitraria. Emette le righe risultanti o un messaggio di errore se i contenuti non sono disponibili entro un massimo di cinque secondi.