Alta concurrencia en Java

Cuando nos enfrentamos a aplicaciones con una alta concurrencia, solemos encontrarnos con una serie de problemas tipo. En este artículo me centraré en los problemas de recursos (cpu y memoria), dejando los problemas de sincronización para otro futuro artículo. De momento me centraré en los problemas más típicos y sus soluciones más directas.

Cuando descubrimos los hilos y las ventajas del procesamiento en paralelo puede ocurrir que acabemos abusando de su uso. Si el problema es que tenemos una gran cantidad de hilos (¿100? ¿1000?) simultáneos, el procesador estará saltando de uno a otro sin parar, sin dejar que realmente terminen, por muy corta que sea su ejecución real. Y con el tiempo se irán encolando más y más hilos que sólo ralentizarán el proceso. A eso hay que sumarle que el coste de crear y destruir hilos puede llegar a ser significativo cuando hablamos de tantos hilos a la vez.
When facing high concurrency applications, we often find a number of generic problems. In this article I will focus on the problems of resources (CPU and memory), allowing synchronization problems for another future article. For now on, I will focus on the most typical and most direct solutions.

When we discover threads and the advantages of parallel processing it can happen that we end up abusing their use. We have a lot of threads (100 ¿? 1000?) simultaneously, and the processor will be jumping from one to another without stopping, not letting them finish, no matter how fast is their real excution. And over time there will be more and more threads only slowing down the process. To the cost of execution of each thread, we must consider also the added cost of creating and destroying threads, which can become significant when we talk about so many threads at once.

En este caso, al primer método al que debemos recurrir es al Patrón Thread Pool. Este patrón consiste en limitar el número de hilos que hay ejecutándose en un momento dado.

En vez de crear hilos nuevos, creamos tareas, que esperan apiladas. Así mismo, tendremos un pool de hilos que irán cogiendo esas tareas y ejecutándolas lo más pronto posible. Un ejemplo clásico de este patrón se encuentra en la clase SwingWorker. Si queremos implementar “a mano” este patrón, conviene que le echemos un vistazo a la interfaz ExecutorService

Si tenemos un hilo en background que está haciendo un uso muy intensivo del procesador, pero no nos importa ralentizar su ejecución, podemos hacer uso del comando sleep (Thread.sleep(...)) para que el hilo libere periódicamente el procesador, permitiendo que el resto de hilos puedan ejecutarse más rápidamente. Esto es útil para hilos que se ejecutan en modo mantenimiento, que deben estar siempre ejecutándose pero que no tienen por qué responder en tiempo real. Otra forma de parar temporalmente un hilo mientras otro se ejecuta, es mediante el método join (thread.join()), que hace que un hilo espere hasta que el otro hilo termine. Aunque más útil en caso de que tengamos un hilo claramente más prioritario que otro, no es viable si no podemos tener una referencia al hilo más prioritario desde el menos prioritario para indicarle a qué hilo tiene que esperar.

Pero la alta concurrencia no viene dada sólo por el uso del procesador. Puede ocurrir que varios hilos necesiten acceder a grandes cantidades de información de forma casi simultánea. Estos hilos no sólo estarán repitiendo la información en memoria sino que muchas veces estarán repitiendo todo el proceso de extraer dicha información.

Este problema suele estar resuelto en la mayoría de librerías de acceso a datos (base de datos mayormente). Por ejemplo, podemos encontrarnos con el caso de ehcache, que utiliza hilos para almacenar información (Thread-Specific Storage Pattern). De esta forma, el acceso y almacenamiento de esta información es compartido, por lo que se disminuye tanto el uso de la memoria necesaria como el tiempo de procesador requerido para extraer y darle forma a la información. Los diferentes hilos que procesarán esta información, irán pidiéndole los datos a ehcache, que se encargará de optimizar estos accesos.

Para mejorar esta solución tenemos las colecciones concurrentes, que permiten a diferentes hilos utilizar los mismos objetos sin presentar problemas de concurrencia.

Existen más soluciones para mejorar la alta concurrencia (sin entrar en optimizaciones del propio código), pero las aquí descritas suelen ser buenas ideas con las que comenzar.

Referencias útiles:


In this case, the first method that we think of is the Thread Pool Pattern . This pattern will limit the number of threads running at the same time.

Instead of creating new threads, we create tasks, which are piled. Also, we have a pool of threads that will work picking these up and running as soon as possible. A classic example of this thread can be found on SwingWorker. If we want to implement bare hands our own pattern, we should take a look at the interface ExecutorService.

If you have a background thread that is making heavy use of processor, but we do not mind slowing it down for performance, we can use the command sleep ( Thread.sleep (...)) to periodically release the thread processor, allowing other threads to run faster . This is useful for threads running in maintenance mode, which must be kept running but do not have to respond in real time. Another way to temporarily stop a running thread while another is using the method join ( Thread.Join () ), which makes a thread wait until another thread ends. Although more useful if we have a clearly higher priority thread than another, it is not viable if we can not have a reference to a higher priority thread from the lowest priority to tell which thread has to wait.

But the high turnout is not given only by the use of the processor. It may be that multiple threads need access to large amounts of information almost simultaneously. These threads will not only be repeating the information in memory but often will be repeating the entire process of extracting that information.

This problem is usually solved in the majority of data access libraries (mostly database). For example, we have ehcache , which uses threads to store information ( Thread-Specific Storage Pattern ). This way, access and storage of this information is shared, thus decreasing both the memory usage required and the processor time required to extract and shape information. As the threads wants to process this information, they will be asking ehcache for the data, which will optimize these hits.

To improve this solution have the concurrent collections, which allow different threads to use the same objects without any problems of concurrency.

There are more solutions to improve the high turnout (without going into optimizations to the code itself), but those described here are usually good ideas to start.

Useful References:

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.