La relación productor/consumidor es interesante pues ambos comparten un buffer. Sin embargo, ambos funcionan de manera distinta teniendo una relación entre ellos. Esta estrategia tiene muchas aplicaciones en diversos ámbitos de la vida real.

Por ejemplo: las impresoras, estas funcionan con esta estrategia, pues debe esperar a que el productor almacene los documentos a imprimir y el consumidor debe comenzar a vaciar dichos archivos e imprimirlos.

El ejemplo que plantearemos en código es sobre una pastelería implementando la estrategia productor/consumidor.

Digamos que existen 10 rebanas de pastel sobre la mesa, entonces los clientes (consumidor) toman una rebana hasta acabarse. Cuando esto sucede, el cliente debe notificarle al productor (cocinero) que produzca mas rebanas y las coloque sobre la mesa. Entonces, al terminar de producir, ahora el productor debe notificarle al consumidor que ya existen mas rebanadas.

Como primer paso, vamos a crear y lanzar hilos. Ademas se declaran las variables necesarias.

import static java.lang.Thread.sleep;

public class ProductorConsumidorMain implements Runnable {

    public static void main(String[] args) {
        Thread[] hilos = new Thread[8];
        Runnable runnable = null;
        for (int i = 0; i < hilos.length; i++) {
            if (i == 0) {
                runnable = new ProductorConsumidorMain(true);//productor
            } else {
                runnable = new ProductorConsumidorMain(false);//consumidor
            }
            hilos[i] = new Thread(runnable);
            hilos[i].start();
        }

        /*Aqui le indicamos, que el hilo principal del main debe esperar a que finalicen los hilos creados*/
        try {
            for (int i = 0; i < hilos.length; i++) {
                hilos[i].join();
            }
        } catch (Exception e) {
        }

    }

    /*como esta variable se esta compartiendo, todos los hilos trabajan con ella. 
    Y esto es debido a que ninguno tiene una copia propia de la variable*/
    private static int rebanadas;
    private boolean productor;
    //Tambien es importante que el cerrojo se comparta con todos los hilos
    private static Object lock = new Object();

    public ProductorConsumidorMain(boolean productor) {
        this.productor = productor;
    }

    @Override
    public void run() {
        while (true) {
            if (productor) {//es productor
                productor();
            } else {//es consumidor
                consumidor();
            }
        }
    }

    public void esperar() {
        try {
            lock.wait();
        } catch (InterruptedException ex) {
            System.out.println("Error de interrupcion");
        }
    }

La variable de tipo boolean llamada productor, indica que si el valor es true es productor y si es false entonces es consumidor.

¿Como detectar la sección critica?

No profundizare sobre el tema. Sin embargo, para detectar fácilmente nuestra sección critica debemos identificar el problema. Por ejemplo: en esta ocasión deseamos controlar la producción y consumo de rebanadas, esta variable debe ser manipulada constantemente mediante hilos incrementando y decrementando su valor. Con esto, sabemos que al manipular este dato debemos asegurarnos de que solo un hilo tome su valor y no dos al mismo tiempo (ya que esto podría llevar a incongruencia de datos). Teniendo esto en mente debemos sincronizar hilos protegiendo nuestra sección critica.

¿Como proteger la sección critica?

Podemos hacerlo implementando distintas soluciones que ofrece java. Sin embargo, hablaremos sobre la palabra reservada synchronized, recibe por parámetro cualquier objeto y este actúa como nuestro cerrojo (es importante declarar como static nuestro objeto que funcionara como cerrojo).

Funciona de la siguiente forma: supongamos que lanzamos 5 hilos, entonces estos en una especie de concurso luchan para ver quien llega primero, tenemos los hilos: h1, h2, … , h5.

h2 llega primero y le pregunta al cerrojo: ¿esta ocupado el cerrojo? este le dice, no, adelante. Ahora el h2 se encuentra activo, después llega h1 y hace la misma pregunta ¿esta ocupado el cerrojo? este le dice, si, así que te mandare a la espera.

Activo: h2  – Espera: h1, h3, … , h5

Cuando h2 termina de realizar su tarea, despierta a todos los hilos produciendo una especie de concurso para que solamente un hilo entre nuevamente a la sección critica.

El código anterior no incluye los métodos productor y consumidor, ahora toca hablar de ellos. Primero comenzare:

Creando el productor

    public void productor() {
        synchronized (lock) {
            if (rebanadas == 0) {
                rebanadas = 10;
                lock.notifyAll();
                System.out.println("Productor ha terminado de cocinar " + rebanadas + " pasteles");
            }
            esperar();
        }
    }

Simplemente preguntamos, si productor es igual a cero entonces produces diez rebanadas. Ahora con el objeto utilizado como cerrojo debemos notificar a todos los hilos que despierten (con esto indicamos que aquellos hilos que sean consumidores, pueden comenzar a tomar rebanadas). Después de notificarle a todos los hilos, nuestro hilo actual (hilo productor) debe dormir y esperar a que el consumidor lo despierte (le notifique que no hay rebanadas).

Creando el consumidor

    public void consumidor() {
        synchronized (lock) {
            if (rebanadas > 0) {
                System.out.println("Consumidor tiene la rebanada numero: " + rebanadas);
                rebanadas--;
                try {
                    sleep(1000);
                } catch (InterruptedException ex) {
                    System.out.println("Error de interrupcion");
                }
            } else {
                lock.notifyAll();
                esperar();
            }
        }
    }

Con el código, primero nos aseguramos de tener rebanadas. Mientras rebanadas sea mayor a cero podrán tomar una rebanada.

Si no existen rebanadas, debemos notificar al productor (realmente se notifican todos los hilos que están a la espera) que ya no existen rebanadas. El hilo actual (hilo consumidor) debe esperar hasta que el productor le notifique que ha terminado de cocinar.

Si analizas detenidamente lo explicado y el código, haces pruebas comentando partes del código. Podras entender facilmente la relacion productor consumidor.