En el anterior volumen vimos un ejemplo muy completo de uso de semáforos en Java. ¿Te lo has perdido? Podéis leerlo aquí: Todo sobre semáforos en Java.
En este volumen vamos a hablar sobre locks en Java. En el primer volumen sobre concurrencia hablamos sobre qué eran los locks en Java y sus principales características, también hablamos sobre los monitores y semáforos que ya hemos visto así como las RC y RCC.
A continuación os vamos a poner un ejemplo muy completo sobre locks.
Los Locks se encuentran en el paquete java.util.concurrent.locks y proporciona una implementación de alto rendimiento, aunque es más «manual» que el uso de monitores por lo que hay que controlar adecuadamente lo que hacemos. Los locks se definen a través de la clase ReentrantLock y sus funcionalidades a través de una interfaz Lock.
Los Locks se crearon porque los monitores tenían algunas limitaciones como por ejemplo que no se podía interrumpir un Thread que está en wait(), los locks implícitos de los monitores deben ser liberados en el mismo bloque de código que cuando se suspendió, no se puede hacer desde otro «sitio».
Podemos decir, que los locks son más difíciles de manejar, pero nos proporcionarán un mayor rendimiento y flexibilidad a la hora de programar.
El problema a resolver es el mismo que el de Todo sobre semáforos en Java y Todo sobre monitores en Java.
La parte más importante del código es la siguiente:
[java]
private ReadWriteLock lock = new ReentrantReadWriteLock();
private Lock r = lock.readLock();
private Lock w = lock.writeLock();
[/java]
La clase ReentrantReadWriteLock implementa la interfaz ReadWriteLock que nos proporciona dos tipos de locks, uno para lectura y otro para escritura. Gracias a esta interfaz podremos programar de una forma muy fácil el problema de los lectores y escritores ya que diferencia entre lector/lector, lector/escritor, y escritor/escritor y permite mantener la lógica de estos lectores y escritores:
- Varios lectores sí es compatible.
- Un lector y un escritor no es compatible.
- Más de un escritor no es compatible.
Si no tuvieramos esta interfaz, habría que controlarlo con condicionales como hicimos cuando utilizamos monitores.
La solución al problema constaría de las siguientes clases:
Lector.java
[java] package locks;
/**
*
* @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);
}
}
}
[/java]
Escritor.java
[java] package locks;
/**
*
* @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);
}
}
}
[/java]
Libro.java
[java] package locks;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
*
* @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,
* incluyendo los locks para gestionar la entrada y salida de los escritores
*/
private String libro;
private Interfaz interfaz;
private ReadWriteLock lock = new ReentrantReadWriteLock();
private Lock r = lock.readLock();
private Lock w = lock.writeLock();
/**
*
* @param interfaz
*/
public Libro(Interfaz interfaz) {
/**
* Creamos el constructor y inicializamos a lo que queremos.
*/
this.interfaz = interfaz;
libro = "";
}
public void leerLibro(int identificador) {
/**
* Este método se encarga de leer el libro, antes de leer, bloqueamos el
* lock de lectura.
*/
r.lock();
interfaz.meterDatos(1, interfaz.leerDatos(1) + "" + identificador + " ");
}
public 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 void terminarLeer(int identificador) {
/**
* Este método se encarga de terminar de leer los datos, desbloquea el
* LOCK de lectura cuando hemos terminado.
*/
interfaz.meterDatos(1, interfaz.leerDatos(1).replaceAll("" + identificador + " ", ""));
if (libroTerminado()) {
interfaz.meterDatos(4, interfaz.leerDatos(4) + "Leido por " + identificador + ": " + libro + "n");
}
r.unlock();
}
public void escribirLibro(int identificador) {
/**
* Este método se encarga de escribir en el libro, la estructura es como
* la de lectura. Bloqueamos el LOCK de escritura.
*
*/
w.lock();
libro = libro + identificador;
interfaz.meterDatos(2, interfaz.leerDatos(2) + identificador);
interfaz.meterDatos(3, libro);
}
public void terminarEscribir(int identificador) {
/**
* Método que termina de escribir un libro. Desbloqueamos el LOCK de
* escritura.
*/
interfaz.meterDatos(2, interfaz.leerDatos(2).replaceAll("" + identificador, ""));
w.unlock();
}
}
[/java]
Gestion.java
[java] package locks;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
*
* @author Bron
*/
public class Gestion {
/**
* Esta clase es la que gestiona los botones de reanudar y parar.
*/
private ReadWriteLock lock = new ReentrantReadWriteLock();
private Lock w = lock.writeLock();
public boolean pausar;
public Gestion() {
}
public void reanudar() {
/*
* Ponemos pausar a false y desbloqueamos el escritor
*/
pausar = false;
w.unlock();
}
public void detener() {
/*
* Ponemos el pausar a true y bloqueamos el escritor
*/
w.lock();
pausar = true;
}
public void parar() {
/*
* Si está pausado, al mirar sigue estando bloqueado, y cuando termine lo desbloqueamos
*/
if (pausar) {
w.lock();
try {
} finally {
w.unlock();
}
}
}
}
[/java]
Interfaz.java
[java]
package locks;
/**
*
* @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 "";
}
}
}
[/java]
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):
Esperamos que os haya gustado y hayáis aprendido algo. Si tenéis cualquier duda, podéis poner un comentario.
Próximamente empezaremos con la programación concurrente distribuída (RMI y Sockets) ¡diversión a tope!