I THREAD IN JAVA

I THREAD POOL

java-logoUn thread pool è un componente software che si occupa di gestire i thread, con l’obiettivo di ottimizzare e semplificarne l’utilizzo. Quando scriviamo un programma multithreading ogni thread ha il compito di eseguire un determinato task. Il thread pool, consente di gestire l’esecuzione di una lista di thread. Un thread pool è dotato di una coda interna, che consente di aggiungere più thread, accodandoli tra loro. La gestione dell’esecuzione dei thread è a carico del thread pool. Per decidere quale thread eseguire per primo e quanti eseguirne, esistono diversi algoritmi. In Java esistono diverse classi che implementano questi algoritmi.

Perché utilizzare i thread pool?

  • aumentano le prestazioni delle applicazioni che li utilizzano, poiché questi ottimizzano l’utilizzo della memoria e della CPU
  • le risorse vengono ottimizzate e si ha un aumento della velocità di esecuzione dei task, dato che le operazioni vengono parallelizzate
  • il codice sorgente è più pulito, poiché non è necessario occuparsi della creazione e gestione dei thread, poiché questi compiti sono in carico al thread pool.

I THREAD POOL IN JAVA

Interfaccia java.util.concurrent.Executor

Questa è l’interfaccia base che definisce i meccanismi principali per la gestione di un thread pool. Il metodo principale è execute(thread da eseguire) che consente di aggiungere nuovi thread al pool.

Interfaccia java.util.concurrent.ExecutorService

Questa interfaccia estende l’interfaccia Executor ed aggiunge dei metodi che consentono una migliore gestione del pool, tra cui shutdown() che indica al pool di avviare la chiusura di tutti i thread. Quando viene invocato questo metodo, non è possibile aggiungere nuovi thread al pool.

Classe java.util.concurrent.Executors

Classe factory che consente di creare istanze di pool (Executor, ExecutorService, etc.)

THREAD POOL
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class EsempioThreadPool {
    public static void main(String[ ] args) {
        /* creo il thread pool */
        ExecutorService pool = Executors.newCachedThreadPool();
        /* aggiunge i thread al pool */
        pool.execute(new GetSitePage("https://www.marcoalbasini.com"));
        pool.execute(new GetSitePage("https://www.google.it"));
        pool.execute(new GetSitePage("https://www.amazon.com"));
        pool.shutdown();
    }
}
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;

public class GetSitePage extends Thread {
    private String url;
    public GetSitePage(String url) {
        super();
        this.url = url;
    }
    @Override
    public void run() {
        try {
            URL site = new URL(url);
            URLConnection con = site.openConnection();
            InputStream in = con.getInputStream();
            String encoding = con.getContentEncoding();
            encoding = encoding == null ? "UTF-8" : encoding;
            System.out.println("************************************************");
            System.out.println("CONTENUTO DELLA PAGINA WEB: " + url);
            System.out.println(getString(in));
            System.out.println("************************************************");
        } catch (IOException e) {
            String a = e.getMessage();
            System.out.println(a);
        }
    }
    private String getString(InputStream is) {
        BufferedReader br = null;
        StringBuilder sb = new StringBuilder();
        String line;
        try {
            br = new BufferedReader(new InputStreamReader(is));
            while ((line = br.readLine()) != null) {
                sb.append(line);
            }

        } catch (IOException e) {
            String a = e.getMessage();
            System.out.println(a);
        } finally {
            if (br != null) {
                try {
                    br.close();
                } catch (IOException e) {
                    String a = e.getMessage();
                    System.out.println(a);
                }
            }
        }
        return sb.toString();
    }
}

LE CLASSI ARRAYBLOCKINGQUEUE E LINKEDBLOCKINGQUEUE

In Java, una generica coda è rappresentata dall’interfaccia java.util.Queue. Questa interfaccia estende l’interfaccia java.util.Collection e pertanto ne eredita tutte le caratteristiche (ad esempio i metodi add(e), remove(e), …).

In aggiunta, l’interfaccia Queue definisce altri metodi, tra cui:

  • peek(), recupera il primo elemento della coda, senza eliminarlo. Il metodo ritorna l’elemento recuperato o null se la coda è vuota.
  • element(), analogamente a peek() recupera il primo elemento della coda, senza eliminarlo, solo che se la coda è vuota non ritorna null ma genera l’eccezione: NoSuchElementException.
  • poll(), recupera e rimuove il primo elemento della coda. Il metodo ritorna l’elemento rimosso o null se la coda è vuota.

L’interfaccia Queue viene estesa dall’interfaccia java.util.BlockingQueue, che rappresenta una generica coda bloccante, garantendo l’esecuzione sicura delle operazioni. BlockingQueue eredita i metodi di Queue, oltre a nuovi metodi, tra cui:

  • put(), inserisce un oggetto alla fine della coda; se la coda è piena il metodo si blocca mettendo il thread corrente in attesa. Il thread verrà riattivato quando viene rimosso un elemento dalla coda.
  • take(), restituisce il primo elemento della coda; se la coda è vuota il metodo si blocca mettendo il thread corrente in attesa. Il thread verrà riattivato quando viene inserito un nuovo elemento.

Mettendo in attesa i thread, i software che utilizzano questa interfaccia sono sincronizzati. L’interfaccia BlockingQueue è implementata da diverse classi. Due classi che la implementano e che sono Thread Safe sono:

  • util.concurrent.ArrayBlockingQueue
  • util.concurrent.LinkedBlockingQueue

Queste classi sono state progettate per lavorare con i thread e pertanto sono in grado di garantire l’accesso concorrente a più thread. La classe ArrayBlockingQueue è realizzata come un array circolare di tipo bloccante. Un oggetto di tipo ArrayBlockingQueue è, quindi, una coda bloccante. Un oggetto di tipo ArrayBlockingQueue ha una capacità fissa e non è ridimensionabile. La dimensione si definisce nel costruttore, in fase di creazione dell’oggetto.Gli elementi sono ordinati all’interno della coda secondo le specifiche FIFO (First-In First-Out). La classe LinkedBlockingQueue, a differenza della classe ArrayBlockingQueue, consente di creare istanze senza specificare la capacità. In questo caso, la capacità massima sarà Integer.MAX_VALUE, cioè 231-1 elementi (2.147.483.647). Se la coda non è mai piena, il metodo put() non si potrà mai bloccare! Nell’esempio Produttore/Consumatore, se non impostiamo una capacità alla coda, il produttore aggiungerà elementi fino a quando avrà risorse disponibili!

import java.util.concurrent.BlockingQueue;

public class Consumatore implements Runnable {
    private int i = 0;
    private BlockingQueue<String> queue;
    public Consumatore(BlockingQueue<String> queue) {
        this.queue = queue;
    }
    @Override
    public void run() {
        while(i < 30) {
            if (queue.remainingCapacity() > 0) {
                System.out.println("E' possibile aggiungere ancora " + queue.remainingCapacity() + " su " + queue.size());
            } else if (queue.remainingCapacity() == 0) {
                String elementoRimosso = queue.remove();
                System.out.println("E' stato rimosso l'elemento " + elementoRimosso);
            }
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            i++;
        }
    }
}
import java.util.concurrent.BlockingQueue;

public class Produttore implements Runnable {
    private BlockingQueue<String> queue;
    public Produttore(BlockingQueue<String> queue) {
        this.queue = queue;
    }
    @Override
    public void run() {
        int i = 0;
        while(i < 30) {
            String elem = "Elemento numero " + i;
            /* provo ad aggiungere un elemento alla coda */
            boolean aggiunto = queue.offer(elem);
            System.out.println("L'elemento " + i + " è stato aggiunto? " + aggiunto);
            i++;
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class EsempioArrayBlockingQueue {
    public static void main(String[] args) {
        // Creo una coda che può contenere al massimo 10 elementi.
        BlockingQueue<String> queue = new ArrayBlockingQueue<String>(10);
        // Producer e Consumer accedono alla stessa coda...
        Thread prod = new Thread(new Produttore(queue));
        Thread cons = new Thread(new Consumatore(queue));
        prod.start();
        cons.start();
    }
}
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class EsempioLinkedBlockingQueue {
    public static void main(String[] args) {
        // Creo una coda senza specificare la capacit�.
        BlockingQueue<String> queue = new LinkedBlockingQueue<String>();

        // Producer e Consumer accedono alla stessa coda...
        Thread prod = new Thread(new Produttore(queue));
        Thread cons = new Thread(new Consumatore(queue));

        prod.start();
        cons.start();
    }
}

DEADLOCK, STARVATION E LIVELOCK

Il deadlock si ha quando il thread A si blocca in attesa che il thread B liberi la risorsa condivisa tra i due ed a sua volta, B resta bloccato in attesa che A liberi un’altra risorsa. I due thread A e B sono bloccati a vicenda, l’uno in attesa dell’altro. In questo caso non c’è via d’uscita!

Deadlock

La starvation si ha quando un thread non riesce mai ad acquisire (o ci riesce dopo troppo tempo) le risorse di cui necessita, perché bloccate da altri thread. Supponiamo di avere 3 thread A, B, C che accedono alla stessa risorsa R1 e che A e B hanno una priorità più alta di C. Finché A e B tentano di acquisire la risorsa R1, C non sarà in grado di effettuare il lock e di utilizzarla, perché A e B hanno priorità più alta di C. In questo caso C è soggetto a starvation. Il livelock si ha quando 2 thread sono impegnati a rispondersi reciprocamente e quindi non sono in grado di proseguire nell’esecuzione del lavoro. I thread non sono bloccati come accade nel deadlock, sono occupati.

LINK AI POST PRECEDENTI

IL LINGUAGGIO JAVA

LINK AL CODICE SU GITHUB

GITHUB

ESECUZIONE DEL CODICE DI ESEMPIO

  • Scaricare il codice da GITHUB, lanciare il file JAR con il seguente comando in Visual Studio Code, posizionandosi nella directory contenente il JAR.

        java -jar –enable-preview CorsoJava.jar

  • Oppure mettere in esecuzione il main che si trova nel file CorsoJava.java.