Skip to the content.

Concurrencia en Java


Repositorio GitHub con ejemplos de código

–> matiaspakua/java_concurerncy: code samples of concurrency in java (github.com)

Introducción: sincronización

Que es la concurrencia?

Los usuarios de ordenadores dan por sentado que sus sistemas pueden hacer más de una cosa a la vez. Suponen que pueden seguir trabajando en un procesador de textos mientras otras aplicaciones descargan archivos, gestionan la cola de impresión y transmiten audio. Incluso se espera que una sola aplicación haga más de una cosa a la vez. Por ejemplo, esa aplicación de transmisión de audio debe leer simultáneamente el audio digital de la red, descomprimirlo, gestionar la reproducción y actualizar su pantalla. Incluso el procesador de textos debería estar siempre preparado para responder a los eventos del teclado y del ratón, sin importar lo ocupado que esté reformateando el texto o actualizando la pantalla. El software que puede hacer estas cosas se conoce como software concurrente.

Procesos e hilos

En la programación concurrente, hay dos unidades básicas de ejecución: procesos e hilos. En el lenguaje de programación Java, la programación concurrente se ocupa principalmente de los hilos. Sin embargo, los procesos también son importantes.

Un sistema informático normalmente tiene muchos procesos e hilos activos. Esto es así incluso en sistemas que solo tienen un único núcleo de ejecución y, por lo tanto, solo tienen un hilo ejecutándose en un momento dado. El tiempo de procesamiento de un solo núcleo se comparte entre los procesos e hilos a través de una característica del sistema operativo llamada división de tiempo (time slicing).

Cada vez es más común que los sistemas informáticos tengan varios procesadores o procesadores con varios núcleos de ejecución. Esto mejora enormemente la capacidad de un sistema para la ejecución concurrente de procesos e hilos, pero la concurrencia es posible incluso en sistemas simples, sin varios procesadores o núcleos de ejecución.

Modelo de memoria: RAM

El modelo de memoria y los subprocesos en Java tienen su propia memoria del stack, pero comparten la memoria del heap. El stack contiene variables locales y de referencia, mientras que el heap contiene objetos.

Procesos

Un proceso tiene un entorno de ejecución autónomo. Un proceso generalmente tiene un conjunto completo y privado de recursos básicos de tiempo de ejecución; en particular, cada proceso tiene su propio espacio de memoria.

Los procesos suelen considerarse sinónimos de programas o aplicaciones. Sin embargo, lo que el usuario ve como una única aplicación puede ser en realidad un conjunto de procesos cooperativos. Para facilitar la comunicación entre procesos, la mayoría de los sistemas operativos admiten recursos de comunicación entre procesos (IPC), como tuberías y conectores. Los IPC se utiliza no solo para la comunicación entre procesos del mismo sistema, sino también para procesos de diferentes sistemas.

La mayoría de las implementaciones de la máquina virtual Java se ejecutan como un único proceso. Una aplicación Java puede crear procesos adicionales utilizando un objeto ProcessBuilder.

Hilos

A veces, los hilos se denominan procesos ligeros. Tanto los procesos como los hilos proporcionan un entorno de ejecución, pero la creación de un nuevo hilo requiere menos recursos que la creación de un nuevo proceso.

Los hilos existen dentro de un proceso: cada proceso tiene al menos uno. Los hilos comparten los recursos del proceso, incluida la memoria y los archivos abiertos. Esto permite una comunicación eficiente, pero potencialmente problemática.

La ejecución multiproceso es una característica esencial de la plataforma Java. Cada aplicación tiene al menos un hilo, o varios, si se cuentan los hilos del “sistema” que hacen cosas como la gestión de la memoria y el manejo de señales. Pero desde el punto de vista del programador de aplicaciones, se comienza con un solo hilo, llamado hilo principal. Este hilo tiene la capacidad de crear hilos adicionales, como demostraremos en la siguiente sección.

Java, keywords

/**
A thread is a thread of execution in a program. The Java virtual machine 
allows an application to have multiple threads of execution running 
concurrently.
*/
public class Thread implements Runnable

/**
Represents an operation that does not return a result.
This is a functional interface whose functional method is run().
*/
public interface Runnable

Algo particular de Runnable es que se trata de una interfaz funcional, ahora: ¿Qué es una interfaz funcional?:

–> Interfaz Funcional en Java

Acceso a Memoria en java y sus problemas


public class RunnableCounter implements Runnable {

    int localThreadVariable;
    String nameOfThread;

    @Override
    public void run() {
        for(localThreadVariable = 0; localThreadVariable < 100; localThreadVariable++) {
            System.out.println(this.nameOfThread + " " + localThreadVariable);
        }
    }

    public void setNameOfThread(String name){
        this.nameOfThread = name;
    }
}
public class RunnableCounterDataRace implements Runnable{  
  
    // Recurso compartido por los thread.  
    private int counter;  
  
    @Override  
    public void run() {  
        for (int i = 0; i <1_000_000; i++) {  
            this.counter++;  
        }  
    }  
  
    public int getCounter() {  
        return counter;  
    }  
}

El resultado en la ejecución del test: “RunnableCounterDataRace” da un valor superior a 1.000.000 pero inferior a 2 millones, eso se debe a que ambos thread están incrementando el contado, en una situación de “carrera”.

Este comportamiento puede o no ocurrir dependiendo de como el SO gestione los thread.

Race Condition: Operaciones “atómicas”

keyword: “volatile”

Volatile: el uso de la palabra clave volatile puede ayudar a evitar los data race al garantizar la visibilidad de los valores de las variables en todos los subprocesos, pero no bloquea los datos.

keyword: “synchronized”

Mecanismo de sincronización: la sincronización puede evitar tanto las carreras de datos como las condiciones de carrera al usar la palabra clave synchronized para marcar secciones críticas, lo que garantiza que solo un subproceso pueda ejecutar el código crítico a la vez.

Por ejemplo, si tenemos un edificio de oficinas con salas de reuniones para reservar:

Mundo Real Java
Edificio Objetos Java
Sala de reuniones Sección Critica
Guardia de Seguridad Objeto Monitor
La gente Los threads

En código, la sincronización se puede hacer a nivel método u objeto:

@Override  
public void run() {  
    long startTime = System.nanoTime();  
    for (int i = 0; i < 1_000_000; i++) {  
  
        /*  
        En este caso el monitor es el objecto actual (THIS).         */        synchronized (this) {  
            // código critico, sincronizado, solo un thread puede acceder a la vez.  
            counter++;  
        }  
    }  
    long elapsedTime = System.nanoTime() - startTime;  
  
    System.out.println(Thread.currentThread().getName() + " increased the counter up to: " +  
            counter + " in " + elapsedTime / 1000000 + " milliseconds");  
}

Synchronized: se utiliza para proteger secciones críticas del código y garantizar que solo un subproceso pueda acceder al recurso compartido a la vez.

Objeto de monitor: el objeto cuyo monitor se utiliza para sincronizar el bloque de código. Puede ser el objeto actual (this) o cualquier otro objeto.

Bloqueo de instancia frente a bloqueo de nivel de clase: los métodos de instancia utilizan el monitor del objeto actual, mientras que los métodos estáticos utilizan el monitor de la clase.

Sección crítica: la parte del código en la que se accede a los recursos compartidos y se modifican. Debe estar sincronizada para evitar carreras de datos y condiciones de carrera. Consideraciones de rendimiento: la sincronización puede provocar sobrecargas de rendimiento, bloqueos y falta de rendimiento, por lo que debe utilizarse con prudencia.

Programación Asíncrona

Non-blocking operations

Keywork: Future (promesas)


/*
A task that returns a result and may throw an exception. Implementors define a single method with no arguments called call.
*/
public interface Callable<V>


/*
An Executor that provides methods to manage termination and methods that can produce a Future for tracking progress of one or more asynchronous tasks.
An ExecutorService can be shut down, which will cause it to reject new tasks. Two different methods are provided for shutting down an ExecutorService.
*/
public interface ExecutorService extends Executor, AutoCloseable

ExecutorService

Un buen ejemplo del uso y funcionamiento de un ExecutorService está en la misma documentación de java:


class NetworkService implements Runnable 
{   
	private final ServerSocket serverSocket;   
	private final ExecutorService pool;    
	
	public NetworkService(int port, int poolSize) throws IOException {
	    serverSocket = new ServerSocket(port);
	    pool = Executors.newFixedThreadPool(poolSize);
    }    
    
    public void run() { // run the service
         try {       
	         for (;;) {
                  pool.execute(new Handler(serverSocket.accept()));
			 }
		  }
          catch (IOException ex) {
			 pool.shutdown();
		  }   
	  } 
  }

// otra clase

class Handler implements Runnable {
	private final Socket socket;
    
    Handler(Socket socket) {
		this.socket = socket;
	}   
	public void run() 
	{     
		// read and service request on socket   
	} 
}

CompletableFuture


/*
A Future that may be explicitly completed (setting its value and status), and may be used as a CompletionStage, supporting dependent functions and actions that trigger upon its completion.
When two or more threads attempt to complete, completeExceptionally, or cancel a CompletableFuture, only one of them succeeds.
*/
public class CompletableFuture<T> implements Future<T>, CompletionStage<T>

Referencias