Produttore Consumatore in Java

ll problema produttore-consumatore (in Java nei nostri esempi) è un problema classico di sincronizzazione che riguarda la gestione di una risorsa condivisa da più thread. Il problema si verifica quando un thread (produttore) produce una risorsa e un altro thread (consumatore) la consuma, entrambi devono accedere alla stessa area di memoria condivisa.

Synchronized

synchronized è una parola chiave in Java che viene utilizzata per garantire la sincronizzazione tra i thread. Quando un blocco di codice viene contrassegnato come “synchronized”, questo significa che solo un thread alla volta può accedere a quel blocco di codice.

Ciò è utile quando più thread lavorano con la stessa risorsa condivisa, poiché evita che questi thread accedano alla risorsa contemporaneamente causando conflitti o corruzione dei dati.

Il modo più comune per utilizzare la parola chiave synchronized è contrassegnare un metodo come “synchronized”. Questo significa che solo un thread alla volta può eseguire il metodo.

Ecco un esempio di come utilizzare la parola chiave synchronized in un metodo:

public class Counter {
   private int count = 0;

   public synchronized void increment() {
      count++;
   }

   public int getCount() {
      return count;
   }
}

In questo esempio, solo un thread alla volta può eseguire il metodo increment() . Il contatore non si incrementa in modo errato se più thread accedono contemporaneamente a questo metodo.

Strutture dati utilizzate

La classe Queue in Java è un’interfaccia che rappresenta una struttura dati di tipo coda, ovvero un insieme di elementi in cui gli elementi vengono inseriti alla fine (in fondo alla coda) e rimossi dall’inizio (testa della coda). La classe Queuedefinisce i metodi per l’inserimento, la rimozione e l’accesso agli elementi in una coda.

Ad esempio, i metodi più comuni della classe Queue sono:

  • offer(E e): inserisce un elemento alla fine della coda.
  • poll(): rimuove e restituisce l’elemento in testa alla coda, o restituisce null se la coda è vuota.
  • peek(): restituisce l’elemento in testa alla coda, o restituisce null se la coda è vuota.

La classe Queue è un’interfaccia generica, il che significa che è possibile specificare il tipo di elementi che la coda deve contenere, ad esempio Queue<Integer> o Queue<String>. La classe Queue è implementata da alcune classi concrete come LinkedList e ArrayDeque, che possono essere utilizzate per creare un’istanza di una coda.

La classe Queue è spesso utilizzata per la risoluzione del problema produttore-consumatore, poiché fornisce una semplice struttura dati per la gestione della coda di messaggi tra i due thread.

Un esempio pratico di problema produttore-consumatore potrebbe essere la gestione di una coda buffer condivisa da due thread. Il primo thread (produttore) genera elementi e li inserisce in coda, mentre il secondo thread (consumatore) li rimuove dalla coda per elaborarli.

La classe LinkedList in Java è una classe concreta che implementa l’interfaccia **List**e la classe Deque(che a sua volta estende l’interfaccia Queue). Ciò significa che LinkedList fornisce le funzionalità di entrambe le strutture dati: una lista che mantiene l’ordine degli elementi e una coda che mantiene la proprietà di “prima entrata, prima uscita”.

Esempio 1: Coda buffer (produttore consumatore in Java)

import java.util.LinkedList;
import java.util.Queue;

public class ProducerConsumerExample {
   public static void main(String[] args) {
      Queue<Integer> buffer = new LinkedList<>();
      int maxSize = 5;

      Thread producer = new Thread(new Producer(buffer, maxSize), "Producer");
      Thread consumer = new Thread(new Consumer(buffer, maxSize), "Consumer");

      producer.start();
      consumer.start();
   }
}

class Producer implements Runnable {
   private final Queue<Integer> buffer;
   private final int maxSize;

   public Producer(Queue<Integer> buffer, int maxSize) {
      this.buffer = buffer;
      this.maxSize = maxSize;
   }

   @Override
   public void run() {
      int counter = 0;
      while (true) {
         try {
            produce(counter++);
         } catch (InterruptedException e) {
            e.printStackTrace();
         }
      }
   }

   private void produce(int counter) throws InterruptedException {
      synchronized (buffer) {
         while (buffer.size() == maxSize) {
            System.out.println("Buffer is full, Producer is waiting...");
            buffer.wait();
         }

         buffer.offer(counter);
         System.out.println("Produced: " + counter);
         buffer.notifyAll();
      }
   }
}

class Consumer implements Runnable {
   private final Queue<Integer> buffer;
   private final int maxSize;

   public Consumer(Queue<Integer> buffer, int maxSize) {
      this.buffer = buffer;
      this.maxSize = maxSize;
   }

   @Override
   public void run() {
      while (true) {
         try {
            consume();
         } catch (InterruptedException e) {
            e.printStackTrace();
         }
      }
   }

   private void consume() throws InterruptedException {
      synchronized (buffer) {
         while (buffer.isEmpty()) {
            System.out.println("Buffer is empty, Consumer is waiting...");
            buffer.wait();
         }

         int value = buffer.poll();
         System.out.println("Consumed: " + value);
         buffer.notifyAll();
      }
   }
}

Esempio 2: Coda di messaggi (produttore consumatore in Java)

Un altro esempio di problema produttore-consumatore potrebbe essere la gestione di una coda di messaggi condivisa da due thread. Il primo thread (produttore) genera messaggi e li inserisce in coda, mentre il secondo thread (consumatore) li rimuove dalla coda per elaborarli.

import java.util.LinkedList;
import java.util.Queue;

public class ProducerConsumerExample {
   public static void main(String[] args) {
      Queue<String> messageQueue = new LinkedList<>();
      int maxSize = 5;

      Thread producer = new Thread(new MessageProducer(messageQueue, maxSize), "Producer");
      Thread consumer = new Thread(new MessageConsumer(messageQueue, maxSize), "Consumer");

      producer.start();
      consumer.start();
   }
}

class MessageProducer implements Runnable {
   private final Queue<String> messageQueue;
   private final int maxSize;

   public MessageProducer(Queue<String> messageQueue, int maxSize) {
      this.messageQueue = messageQueue;
      this.maxSize = maxSize;
   }

   @Override
   public void run() {
      int counter = 0;
      while (true) {
         try {
            produce("Message " + counter++);
         } catch (InterruptedException e) {
            e.printStackTrace();
         }
      }
   }

   private void produce(String message) throws InterruptedException {
      synchronized (messageQueue) {
         while (messageQueue.size() == maxSize) {
            System.out.println("Message queue is full, Producer is waiting...");
            messageQueue.wait();
         }

         messageQueue.offer(message);
         System.out.println("Produced: " + message);
         messageQueue.notifyAll();
      }
   }
}

class MessageConsumer implements Runnable {
   private final Queue<String> messageQueue;
   private final int maxSize;

   public MessageConsumer(Queue<String> messageQueue, int maxSize) {
      this.messageQueue = messageQueue;
      this.maxSize = maxSize;
   }

   @Override
   public void run() {
      while (true) {
         try {
            consume();
         } catch (InterruptedException e) {
            e.printStackTrace();
         }
      }
   }

   private void consume() throws InterruptedException {
      synchronized (messageQueue) {
         while (messageQueue.isEmpty()) {
            System.out.println("Message queue is empty, Consumer is waiting...");
            messageQueue.wait();
         }

         String message = messageQueue.poll();
         System.out.println("Consumed: " + message);
         messageQueue.notifyAll();
      }
   }
}

In questo esempio, il produttore e il consumatore accedono alla coda di messaggi condivisa utilizzando un blocco di codice sincronizzato. Ciò garantisce che solo un thread alla volta può accedere alla coda e modificare i suoi dati. Così evitiamo conflitti tra i due thread e garantiamo che i messaggi sono prodotti e consumati in modo sincronizzato e coerente.

Inoltre, utilizzando la chiamata a wait all’interno del blocco sincronizzato, il produttore e il consumatore possono attendere che la coda sia piena o vuota, a seconda delle esigenze, prima di continuare con la loro attività. La chiamata a notifyAll viene utilizzata per notificare a tutti i thread in attesa che la coda è cambiata e che possono riprendere l’esecuzione.

Pubblicato da Carlo Contardi

Carlo Contardi, docente di informatica e sviluppatore Full Stack, condivide la sua passione per la programmazione e l’informatica attraverso il suo blog Space Coding. Offre preziosi consigli e soluzioni pratiche a chi vuole imparare a programmare o migliorare le proprie abilità. 🚀👨‍💻

Translate »