En este volumen os vamos a recomendar ciertos trucos para programar de forma concurrente con un mejor rendimiento dependiendo de en qué casos nos encontremos. Imaginemos que tenemos un servidor web, y que para cada cliente tenemos que crear un thread. Si tenemos pocas visitas, el ordenador será capaz de gestionar 500 hilos sin problemas, pero imaginemos que tenemos 10.000 visitas simultáneamente, el ordenador estaría echando humo (literalmente). Para programas cliente/servidor, es recomendable usar ciertas estructuras más eficientes, con las que lograremos un mayor rendimiento (tanto en velocidad como en tiempo de respuesta) y que además, serán más fácil de gestionar. Podéis leer sobre qué es un socket TCP y para qué sirve.
Aquí podéis ver un ejemplo de lo que no debemos hacer:
[java]
package ejecutadores;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
*
* @author Bron
*/
public class ServidorReventado {
public static void main(String[] args) throws IOException {
ServerSocket socket = new ServerSocket(80);
while (true) {
final Socket connection = socket.accept();
Runnable r = new Runnable() {
@Override
public void run() {
handleRequest(connection);
}
};
new Thread(r).start();
}
}
}
[/java]
La solución a muchos de estos problemas es la creación de un «pool de threads» para ejecutar las diferentes tareas, estas tareas estarán organizadas en una cola y cada thread se encargará de realizar una tarea, y cuando termine, empezará con otra. De esta forma tendremos un mismo número de Threads haciendo múltiples tareas. Anteriormente con cada tarea ejecutábamos un thread.
Para hacer esto, podemos usar el framework Executor, cuya función es aplicar una determinada política de ejecución a una cola de tareas (u otra estructura de datos como una pila por ejemplo). De esta forma, podremos definir el Executor para ejecutar un determinado número de tareas, la creación de un número determinados de threads o qué hacer cuando ha terminado todas las peticiones.
Executor es una interfaz, y describe un objeto para ejecutar Runnables de tal forma que potencia la mantenibilidad (cambiar la política sin cambiar el código de la ejecución). Una clase muy útil en este aspecto es la ThreadPoolExecutor, que podéis encontrar en el propio Javadoc con todos sus métodos y que sirve para crear y gestionar un pool de Threads, en los métodos podéis ver todos los que hay como por ejemplo limitar el número máximo de Threads en el pool para no colapsar el servidor.
Hay varios Executors que conviene saber su existencia, podéis verlo aquí: Executors javadoc.
Aquí podéis ver un ejemplo de lo que sí debemos hacer, utilizando Executors y un pool de Threads (Executors.newFixedThreadPool).
[java]
package ejecutadores;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
/**
*
* @author Bron
*/
public class ServidorRelajado {
public static void main(String[] args) throws IOException {
Executor pool = Executors.newFixedThreadPool(50);
ServerSocket socket = new ServerSocket(80);
while (true) {
final Socket connection = socket.accept();
Runnable r = new Runnable() {
@Override
public void run() {
handleRequest(connection);
}
};
pool.execute(r);
}
}
}
[/java]
En los anteriores ejemplos los objetos están en una interfaz Runnable y por tanto no podemos retornar valores ni tampoco lanzar excepciones. Para evitar esto podemos utilizar las interfaces Callable y Future, tenéis más información sobre estas interfaces en el enlace anterior.