Preparare la certificazione Java Programmer OCPJP7: Threads e Concorrenza

Threads e Concorrenza

  • La classe Thread ha un metodo run() di default che però non fa nulla. Quando si crea un thread estendendo la classe Thread occorre ridefinire tale metodo. Se non lo si fa il compilatore non segnala nessun errore, però il thread utilizzerà il metodo della classe base e quindi non farà nulla.
  • L’interfaccia Runnable definisce il metodo run()
  • Non bisogna MAI invocare esplicitamente il metodo run(), altrimenti il metodo non viene eseguito come thread separato, ma come parte del main. Occorre SEMPRE invocare il metodo start() di un thread, in modo che venga schedulato per l’esecuzione. La JVM si occupa poi di invocare il metodo run() quando è il suo turno.
  • Tre aspetti importanti legati ad un Thread sono:
    • Nome: assegnato di default e non attribuito esplicitamente, ma che può essere modificato tramite il metodo setName(String name)
    • Priorità: può variare da un valore minimo di 1 ad un valore di priorià massima di 10. Il valore di default quando un thread viene creato è 5.
      • Può essere modificata tramite il metodo setPriority(int priority);
      • I livelli di priorità possono anche essere settati in modo rapido tramite i membri statici della classe Thread per i valori minimo, medio e massimo:
        Thread.MIN_PRIORITY = 1
        Thread.NORM_PRIORITY = 5
        Thread.MAX_PRIORITY = 10
      • Gruppo: ogni Thread appartiene ad un gruppo
  • Il metodo join() indica al main di aspettare la fine dell’esecuzione del thread su cui join è invocato prima di procedere con gli statements successivi.
  • Gli stati attraverso cui passa un thread sono:
    • NEW: dopo la creazione e prima di invocare start()
    • RUNNABLE: dopo aver invocato start()
    • TERMINATED: dopo aver invocato join() oppure quanto il thread ha terminato la sua esecuzione
  • Lo stato RUNNABLE, a livello di sistema operativo, indica 2 stati differenti:
    • READY: il thread è pronto per eseguire ed aspetta di essere schedulato, cioè che gli venga assegnata la CPU
    • RUNNING: il thread sta effettivamente eseguendo
  • Per generare numeri casuali in ambiente multithread occorre utilizzare la classe ThreadLocalRandom.
  • Nell’estensione della classe Thread se si fa l’override del metodo start(), il metodo run() non viene invocato.
  • Un thread può tranquillamente acquisire un lock su un oggetto sul quale ha già un lock. Non viene generata nessuna eccezione in questo caso.
  • Se si invoca il metodo start() su un thread più volte si ottiene una IllegalThreadStateException
  • Un thread può stare in uno dei seguenti stati, che sono definiti nell’enumeratore java.lang.Thread.State:
    • NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED
    • Stati come ad esempio EXECUTING non esistono
  • I costruttori validi della classe Thread sono:
    • Thread()
    • Thread(String name)
    • Thread(Runnable target, String name)
    • Thread(ThreadGroup group, String name)
  • synchronized può essere utilizzato solo con i metodi e i blocchi
  • Se all’interno del blocco synchronized si verifica un’eccezione il lock viene immediatamente rilasciato
  • Con i blocchi synchronized si può acquisire un lock solo su oggetti e non su tipi primitivi. Se si prova ad utilizzarlo con un tipo primitivo si ottiene un errore di compilazione.
  • Un metodo synchronized acquisisce il lock sull’oggetto sul quale viene invocato, quindi implicitamente sulla referenza this.
  • Anche un metodo statico può essere dichiarato synchronized ed in questo caso il lock è acquisito sull’oggetto class che è un oggetto di tipo Class associato ad ogni classe.
  • NON si possono dichiarare synchronized i costruttori. Si ottiene un errore di compilazione
    • E’ la JVM che assicura che il costruttore venga invocato da un solo thread per volta
    • All’interno dei costruttori è però possibile utilizzare dei blocchi synchronized
  • Un deadlock si può verificare quando thread diversi cercano di accedere in modo esclusivo alle stesse risorse effettuando però i lock in ordine diverso.
    • Se i lock vengono acquisiti nello stesso ordine non c’è rischio di deadlock.
  • Il deadlock ed, in generale, tutti i problemi legati alla concorrenza dei thread sono non deterministici e non riproducibili volontariamente, perchÈ dipendono dall’ordine di esecuzione in cui sono schedulati i thread, che può variare da esecuzione ad esecuzione.
  • La differenza tra deadlock e livelock è che in caso di deadlock i thread sono in uno stato di waiting in attesa che un segnale li avvisi della disponibilità delle risorse di cui necessitano.
    • o In caso di Livelock invece i thread stanno facendo qualcosa e sono running, però ripetono continuamente le stesse operazioni e non stanno eseguendo i task che fanno andare avanti l’applicazione globale.
  • I metodi abstract NON possono essere dichiarati synchronized.
  • Il metodo sleep() di un thread non rilascia le risorse su cui ha acquisito il lock
  • Le utilities per la gestione della concorrenza ad alto livello sono contenute nel package java.util.concurrent
  • I meccanismi di alto livello per la gestione della concorrenza sono principalmente:
    • Semaphore
    • Phaser
    • CountDownLatch
    • Exchanger
    • CyclicBarrier
  • Un thread può riacquisire un lock che già possiede
  • Le classi presenti nel package java.util.concurrent.atomic che estendono la classe Number sono:
    • AtomicInteger
    • AtomicLong
  • Un ReentrantLock può essere acquisito più volte, tramite il metodo lock(). Deve poi però essere rilasciato uno stesso numero di volte tramite il metodo unlock().
  • Se su un oggetto di tipo Lock viene invocato il metodo unlock() prima del metodo lock(), viene scatenata una IllegalMonitorStateException.
  • L’interfaccia Executor dichiara un solo metodo execute(Runnable command) che esegue il comando specificato in qualche momento nel futuro.
  • L’interfaccia Callable dichiara un singolo metodo call() che calcola un risultato.
  • L’overload del metodo submit() nell’interfaccia ExecutorService restituisce un oggetto di tipo Future.
  • Chiamando il metodo wait() senza prima acquisire un lock si ottiene una IllegalMonitorStateException
  • Executor è un’interfaccia che definisce un solo metodo: execute(Runnable)
  • Gli oggetti che implementano l’interfaccia Callable o l’interfaccia Runnable possono essere eseguiti da un ExecutorService
  • Un oggetto che implementa Runnable non può ritornare un risultato, in quanto il metodo run() definito dall’interfaccia ha valore di ritorno void.
  • Un oggetto che implementa Callable può ritornare un risultato, in quanto il metodo call() definito dall’interfaccia restituisce un oggetto del tipo generics definito.
  • Callable è disponibile a partire da Java5
  • Un oggetto che implementa l’interfaccia Runnable non può scatenare delle checked exception.
  • L’interfaccia ThreadFactory definisce un solo metodo: Thread newThread(Runnable r)
  • Se utilizzando il framework fork-join con i RecursiveTask si inverte l’ordine delle chiamate compute() e join() si ottiene lo stesso risultato ma con performance peggiori, perché l’esecuzione diventa sequenziale e NON parallela.
  • Il metodo join() della classe Thread può scatenare delle InterruptedException che sono checked exception e quindi vanno gestite (try-catch o throws).
  • Quando viene invocato il metodo sleep() il Thread NON perde gli eventuali lock che ha acquisito.
  • RecursiveTask eredita da ForkJoinTask e definisce un metodo compute() che restituisce un tipo generics V.
    • RecursiveAction è simile a RecursiveTask ma NON restituisce nessun valore.
    • Sul primo task si invoca il metodo fork() e sul secondo si invoca compute();
This entry was posted in $1$s. Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *