Curso Java. Volumen V. Todo sobre monitores en java

Escrito por Sergio De Luz
Java
1

En el anterior volumen vimos cómo se gestionan los Threads o hilos en Java, os pusimos un código de ejemplo con los principales métodos que se pueden usar.

En este volumen vamos a hablar sobre monitores en Java. En el primer volumen sobre concurrencia hablamos sobre qué eran los monitores en Java, qué era una RC (Región Crítica) y también una RCC (Región Crítica Condicional).

A continuación os vamos a poner un ejemplo muy completo para usar monitores.

El problema a resolver consiste en el típico problema de leer y escribir en un libro. En la vida real, muchas personas pueden leer un mismo libro (recurso compartido) simultáneamente, pero sin embargo no puede escribir más de uno (simultáneamente) porque entonces habría incoherencias, ¿verdad?.

De esta forma, pretendemos hacer un programa de tal forma que cuando esté el escritor escribiendo, ningún lector ni escritor pueda acceder al recurso compartido.

Si hay un lector en el libro, esperaremos hasta que termine el último para comenzar a escribir.

Habrá 15 lectores (15 hilos lectores) y 10 hilos escritores. El libro será un simple String vacío (inicializado a “”). Tanto los escritores como los lectores estarán perfectamente identificados con un número. En el caso de los escritores, dicho caracter se escribirá en el libro cuando le toque su turno.

Un libro se terminará cuando contenga 50 caracteres, a continuación mostraremos un mensaje por pantalla diciendo todos los escritores que han accedido (y escrito) el libro.

En la clase de los escritores, el ciclo que deberán seguir es: esperar tiempo aleatorio entre 1 y 2 segundos, y luego escribir en el libro. Este ciclo lo repetiremos 5 veces (un simple bucle for).

En la clase de los lectores, el ciclo que deberán seguir es: esperar entre 1 y 2 segundos, leer el libro, esperar un tiempo “leyendo” y vuelven a empezar a no ser que el libro ya esté terminado (50 caracteres).

Parece muy difícil, pero en cuanto veáis el código entenderéis todo perfectamente. Ya de paso, vamos a realizarlo con una interfaz gráfica en la que se muestren todos los datos.

Antes de mirar la solución, os recomendamos intentar programarlo, y si os atascáis, podéis ayudaros de la solución.

Solución

Lector.java

package monitores;

/**
*
* @author Bron
*/
public class Lector extends Thread {

/*
* La clase lector se encargará de leer el libro, varios lectores pueden
* leer el mismo libro simultáneamente.
*/
private int identLector;
private Libro libro;
private Gestion gestion;

public Lector(int identLector, Libro libro, Gestion gestion) {

/**
* Constructor de la clase lectores con atributos identificador, y las
* clases libro y gestion.
*/
this.identLector = identLector;
this.libro = libro;
this.gestion = gestion;
}

private void Esperar(int min, int max) {

/*
* Nos encargamos de proporcionar un tiempo aleatorio, creamos esta
* función por comodidad y porque ya estaba en ejercicios anteriores.
*/

try {
sleep(min + (int) (max * Math.random()));
} catch (Exception e) {
}
}

@Override
public void run() {

/*
* El método run se ejecutará mientras que el libro no esté terminado.
* La funcionalidad es la que se pide en el enunciado, con cada acción
* llamamos a gestion.parar por si hemos pulsado el botón de pausa.
*/

while (libro.libroTerminado() == false) {
gestion.parar();
Esperar(1000, 1000);
gestion.parar();
libro.leerLibro(identLector);
gestion.parar();
Esperar(500, 1000);
gestion.parar();
libro.terminarLeer(identLector);
}
}
}

Escritor.java

package monitores;

/**
*
* @author Bron
*/
public class Escritor extends Thread {

/**
* La clase escritor sólo podrá escribir el libro de 1 en 1 ya que estamos
* en una región crítica con variables compartidas tal y como pone el
* enunciado.
*/
private int identEscritor;
private Libro libro;
private Gestion gestion;

public Escritor(int identEscritor, Libro libro, Gestion gestion) {

/**
* Constructor de la clase Escritor, tenemos los atributos identificador
* del escritor y luego le pasamos el libro y la gestión del libro por
* si tenemos que parar
*/
this.identEscritor = identEscritor;
this.libro = libro;
this.gestion = gestion;
}

private void Esperar(int min, int max) {

/*
* Nos encargamos de proporcionar un tiempo aleatorio, creamos esta
* función por comodidad y porque ya estaba en ejercicios anteriores.
*/

try {
sleep(min + (int) (max * Math.random()));
} catch (Exception e) {
}
}

@Override
public void run() {

/**
* Aquí tenemos todas las acciones que se piden en el enunciado con
* respecto a los escritores.
*/

for (int i = 0; i < 5; i++) {
gestion.parar();
Esperar(1000, 1000);
gestion.parar();
libro.escribirLibro(identEscritor);
gestion.parar();
Esperar(100, 0);
libro.terminarEscribir(identEscritor);
}
}
}

Libro.java

package monitores;

/**
*
* @author Bron
*/
public class Libro {

/**
* Esta clase libro es la variable compartida y por tanto, la que tenemos
* que proteger de lectores/escritores para que el programa funcione
* satisfactoriamente.
*
* Creamos todos los atributos necesarios para que el programa funcione.
*/
private String libro;
private Interfaz interfaz;
private int lecturas;
private boolean escritores;
private int numPeticionesE;

/**
*
* @param interfaz
*/
public Libro(Interfaz interfaz) {

/**
* Creamos el constructor y inicializamos a lo que queremos.
*/
this.interfaz = interfaz;
libro = "";
lecturas = 0;
escritores = false;
numPeticionesE = 0;
}

public synchronized void leerLibro(int identificador) {

/**
* Este método se encarga de leer el libro, esperaremos si los
* escritores están funcionando o hay peticiones de esctitura. Si no se
* da ninguna de las 2 condiciones, procedemos a introducir los datos en
* el jTextField e incrementamos las lecturas. No devolvemos nada.
*/
while (escritores || numPeticionesE > 0) {
try {
wait();
} catch (InterruptedException ex) {
}
}
interfaz.meterDatos(1, interfaz.leerDatos(1) + "" + identificador + " ");
lecturas++;
}

public synchronized boolean libroTerminado() {

/**
* Si el libro ha llegado a 50 habremos terminado de leer y devolvemos
* un valor booleano.
*/
if (libro.length() == 50) {
return true;
} else {
return false;
}
}

public synchronized void terminarLeer(int identificador) {

/**
* Este método se encarga de terminar de leer los datos, decrementamos
* las lecturas conforme vayamos leyendo. Introducimos los datos en el
* jTextField de los libros leídos y si llegamos a 0 despertamos a los
* hilos que estaban esperando. Si el libro está terminado (ha llegado a
* 50) entonces procedemos a meter los datos en el jTextArea
*/
lecturas--;
interfaz.meterDatos(1, interfaz.leerDatos(1).replaceAll("" + identificador + " ", ""));
if (lecturas == 0) {
notifyAll();
}
if (libroTerminado()) {
interfaz.meterDatos(4, interfaz.leerDatos(4) + "Leido por " + identificador + ": " + libro + "n");

}
}

public synchronized void escribirLibro(int identificador) {

/**
* Este método se encarga de escribir en el libro, la estructura es como
* la de lectura, si hay escritores (otro hilo escritor) o si hay
* lecturas por hacer, entonces esperamos.
*
*/

numPeticionesE++;
while (escritores || (lecturas > 0)) {
try {
wait();
} catch (InterruptedException ex) {
}
}
escritores = true;
libro = libro + identificador;
interfaz.meterDatos(2, interfaz.leerDatos(2) + identificador);
interfaz.meterDatos(3, libro);
numPeticionesE--;
}

public synchronized void terminarEscribir(int identificador) {

/**
* Método que termina de escribir un libro. Cuando termina, despertamos a todos los hilos que estaban esperando.
*/

escritores = false;
interfaz.meterDatos(2, interfaz.leerDatos(2).replaceAll("" + identificador, ""));
notifyAll();
}
}

Gestion.java

package monitores;

/**
*
* @author Bron
*/
public class Gestion {

/**
* Esta clase es la que gestiona los botones de reanudar y parar.
*/
public Gestion() {
}
public boolean pausar;

public synchronized void reanudar() {
/*
* Si pulsamos el botón reanudar pondremos pausar a falso y el programa
* continuará, notificamos a todos los hilos esperando que ya pueden
* seguir trabajando.
*/

pausar = false;
notifyAll();
}

public synchronized void detener() {

/**
* Si pulsamos el botón detener, pondremos pausar a true y los hilos
* harán wait.
*/

pausar = true;
}

public synchronized void parar() {

/*
* Analizamos la condicion de pausar. Si es true hacemos un wait y
* esperamos, si es false no hacemos nada ni ponemos ningún mensaje.
*/

if (pausar) {
try {
wait();
} catch (InterruptedException e) {
System.out.println("" + e);
}
}
}
}

Interfaz.java

package monitores;

/**
*
* @author Bron
*/
public class Interfaz extends javax.swing.JFrame {

/**
* Creates new form Interfaz
*/
public Interfaz() {
initComponents();
}
/**
* Creamos un nuevo objeto Gestion para reanudar y parar el sistema.
*/
public static Gestion gestion = new Gestion();

/**
* This method is called from within the constructor to initialize the form.
* WARNING: Do NOT modify this code. The content of this method is always
* regenerated by the Form Editor.
*/
@SuppressWarnings("unchecked")
// <editor-fold defaultstate="collapsed" desc="Generated Code">
private void initComponents() {

jLabel1 = new javax.swing.JLabel();
jLabel2 = new javax.swing.JLabel();
jLabel3 = new javax.swing.JLabel();
jLabel4 = new javax.swing.JLabel();
jTextField1 = new javax.swing.JTextField();
jTextField2 = new javax.swing.JTextField();
jTextField3 = new javax.swing.JTextField();
jLabel5 = new javax.swing.JLabel();
jScrollPane1 = new javax.swing.JScrollPane();
jTextArea1 = new javax.swing.JTextArea();
detener = new javax.swing.JButton();
reanudar = new javax.swing.JButton();

setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
setTitle("PEC2L : Lectores y Escritores de Libros");
setResizable(false);

jLabel1.setFont(new java.awt.Font("Verdana", 1, 12)); // NOI18N
jLabel1.setText("PEC2L : Lectores y Escritores de Libros");

jLabel2.setFont(new java.awt.Font("Verdana", 1, 12)); // NOI18N
jLabel2.setText("Lectores que actualmente están leyendo el Libro:");

jLabel3.setFont(new java.awt.Font("Verdana", 1, 12)); // NOI18N
jLabel3.setText("Escritores que actualmente están escribiendo el Libro:");

jLabel4.setFont(new java.awt.Font("Verdana", 1, 12)); // NOI18N
jLabel4.setText("Contenido del Libro:");

jTextField1.setFont(new java.awt.Font("Verdana", 1, 12)); // NOI18N

jTextField2.setFont(new java.awt.Font("Verdana", 1, 12)); // NOI18N
jTextField2.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
jTextField2ActionPerformed(evt);
}
});

jTextField3.setFont(new java.awt.Font("Verdana", 1, 12)); // NOI18N

jLabel5.setFont(new java.awt.Font("Verdana", 1, 12)); // NOI18N
jLabel5.setText("Libro terminado:");

jTextArea1.setColumns(20);
jTextArea1.setRows(5);
jScrollPane1.setViewportView(jTextArea1);

detener.setFont(new java.awt.Font("Verdana", 1, 12)); // NOI18N
detener.setText("DETENER");
detener.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
detenerActionPerformed(evt);
}
});

reanudar.setFont(new java.awt.Font("Verdana", 1, 12)); // NOI18N
reanudar.setText("REANUDAR");
reanudar.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
reanudarActionPerformed(evt);
}
});

javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
getContentPane().setLayout(layout);
layout.setHorizontalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addGap(25, 25, 25)
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false)
.addComponent(jLabel5, javax.swing.GroupLayout.PREFERRED_SIZE, 144, javax.swing.GroupLayout.PREFERRED_SIZE)
.addComponent(jTextField3, javax.swing.GroupLayout.DEFAULT_SIZE, 662, Short.MAX_VALUE)
.addComponent(jTextField2, javax.swing.GroupLayout.DEFAULT_SIZE, 662, Short.MAX_VALUE)
.addComponent(jLabel2, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
.addComponent(jLabel3, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
.addComponent(jLabel4, javax.swing.GroupLayout.PREFERRED_SIZE, 144, javax.swing.GroupLayout.PREFERRED_SIZE)
.addComponent(jTextField1)
.addComponent(jScrollPane1)))
.addGroup(layout.createSequentialGroup()
.addGap(221, 221, 221)
.addComponent(jLabel1)))
.addContainerGap())
.addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
.addGap(0, 169, Short.MAX_VALUE)
.addComponent(reanudar, javax.swing.GroupLayout.PREFERRED_SIZE, 123, javax.swing.GroupLayout.PREFERRED_SIZE)
.addGap(76, 76, 76)
.addComponent(detener, javax.swing.GroupLayout.PREFERRED_SIZE, 123, javax.swing.GroupLayout.PREFERRED_SIZE)
.addGap(223, 223, 223))
);
layout.setVerticalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addGap(4, 4, 4)
.addComponent(jLabel1, javax.swing.GroupLayout.PREFERRED_SIZE, 29, javax.swing.GroupLayout.PREFERRED_SIZE)
.addGap(18, 18, 18)
.addComponent(jLabel2)
.addGap(18, 18, 18)
.addComponent(jTextField1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
.addGap(28, 28, 28)
.addComponent(jLabel3)
.addGap(18, 18, 18)
.addComponent(jTextField2, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
.addGap(27, 27, 27)
.addComponent(jLabel4)
.addGap(27, 27, 27)
.addComponent(jTextField3, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
.addGap(32, 32, 32)
.addComponent(jLabel5)
.addGap(18, 18, 18)
.addComponent(jScrollPane1, javax.swing.GroupLayout.PREFERRED_SIZE, 131, javax.swing.GroupLayout.PREFERRED_SIZE)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 47, Short.MAX_VALUE)
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
.addComponent(detener, javax.swing.GroupLayout.PREFERRED_SIZE, 31, javax.swing.GroupLayout.PREFERRED_SIZE)
.addComponent(reanudar, javax.swing.GroupLayout.PREFERRED_SIZE, 31, javax.swing.GroupLayout.PREFERRED_SIZE))
.addGap(20, 20, 20))
);

java.awt.Dimension screenSize = java.awt.Toolkit.getDefaultToolkit().getScreenSize();
setBounds((screenSize.width-730)/2, (screenSize.height-616)/2, 730, 616);
}// </editor-fold>

private void reanudarActionPerformed(java.awt.event.ActionEvent evt) {

/*
* En este botón de reanudar, procedemos a habilitar el botón detener y
* a deshabilitar el botón reanudar (que ya ha sido pulsado). A
* continuación, reanudamos el sistema llamando a "gestión"
*/

detener.setEnabled(true);
reanudar.setEnabled(false);
gestion.reanudar();
}

private void detenerActionPerformed(java.awt.event.ActionEvent evt) {

/*
* En este botón de reanudar, procedemos a habilitar el botón detener y
* a deshabilitar el botón reanudar (que ya ha sido pulsado). A
* continuación, reanudamos el sistema llamando a "gestión"
*/

reanudar.setEnabled(true);
detener.setEnabled(false);
gestion.detener();
}

private void jTextField2ActionPerformed(java.awt.event.ActionEvent evt) {
// TODO add your handling code here:
}
// Variables declaration - do not modify
public javax.swing.JButton detener;
private javax.swing.JLabel jLabel1;
private javax.swing.JLabel jLabel2;
private javax.swing.JLabel jLabel3;
private javax.swing.JLabel jLabel4;
private javax.swing.JLabel jLabel5;
private javax.swing.JScrollPane jScrollPane1;
private javax.swing.JTextArea jTextArea1;
private javax.swing.JTextField jTextField1;
private javax.swing.JTextField jTextField2;
private javax.swing.JTextField jTextField3;
public javax.swing.JButton reanudar;
// End of variables declaration

public static void main(String args[]) {

/*
* Programa principal y el que ejecuta todos los hilos concurrentemente.
*/

java.awt.EventQueue.invokeLater(new Runnable() {

@Override
public void run() {

/*
* Creamos nuevos objetos de la interfaz y del libro y ajustamos la interfaz
*/

Interfaz interfaz = new Interfaz();
interfaz.setVisible(true);
interfaz.reanudar.setEnabled(false);

Libro libro = new Libro(interfaz);

/*
* Creamos los hilos con los datos pedidos
*/

for (int i = 20; i < 35; i++) {
Lector l1 = new Lector(i, libro, gestion);
l1.start();
}
for (int i = 0; i < 10; i++) {
Escritor e1 = new Escritor(i, libro, gestion);
e1.start();
}
}
});
}

public void meterDatos(int i, String datos) {

/*
* Esta clase introduce los datos en el jTextField, recibimos un número
* de otra clase para saber dónde debemos introducir la información.
* Gracias al switch podremos introducir fácilmente los datos en el
* jTextField correcto. Al ser void, no devuelve nada.
*/

switch (i) {
case 1: {
jTextField1.setText(datos);
break;
}
case 2: {
jTextField2.setText(datos);
break;
}
case 3: {
jTextField3.setText(datos);
break;
}
case 4: {
jTextArea1.setText(datos);
break;
}
}
}

public String leerDatos(int i) {

/*
* Esta clase devuelve un String con el contenido del jTextField.
* Dependiendo del número pasado por parámetro leeremos un jTextField u
* otro. Si no se corresponde con ninguno, devolvemos cadena vacía.
*/

switch (i) {
case 1:
return jTextField1.getText();
case 2:
return jTextField2.getText();
case 3:
return jTextField3.getText();
case 4:
return jTextArea1.getText();
default:
return "";
}
}
}

La interfaz gráfica la podéis hacer a vuestro gusto, hay otra alternativa para leer/escribir los datos y es que en lugar de hacerlo directamente, podemos utilizar la estructura de datos ArrayList. Tal vez os sea más sencillo de esta forma, ambas alternativas son correctas.

Por supuesto, el código puede tener muchas modificaciones y mejoras, esto es un simple ejemplo de por dónde van los tiros con el uso de monitores.

Para facilitar la ejecución del código con la misma interfaz gráfica que he usado yo (por si queréis copiar la estructura) os subo el paquete (no el proyecto entero):

Descargar paquete monitores.ZIP

Esperamos que os haya gustado y hayáis aprendido algo. Si tenéis cualquier duda, podéis poner un comentario.

Próximamente trataremos este mismo problema con semáforos.


Continúa leyendo