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.
- Metodi sincronizzati
- Strutture dati utilizzate
- Esempio1: Coda buffer
- Esempio 2: Coda messaggi
- Link utili
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 Queue
definisce 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 restituiscenull
se la coda è vuota.peek()
: restituisce l’elemento in testa alla coda, o restituiscenull
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.