Seguridad de hilos en Java

La seguridad de hilos en Java es un tema muy importante. Java proporciona soporte para entornos multihilo mediante hilos de Java. Sabemos que varios hilos creados desde el mismo objeto comparten variables de objeto y esto puede llevar a la inconsistencia de datos cuando los hilos se utilizan para leer y actualizar los datos compartidos.

Seguridad de Hilos

La razón de la inconsistencia de datos es porque la actualización de cualquier valor de campo no es un proceso atómico, requiere tres pasos; primero, leer el valor actual, segundo, realizar las operaciones necesarias para obtener el valor actualizado y tercero, asignar el valor actualizado a la referencia del campo. Vamos a verificar esto con un programa simple donde varios hilos actualizan los datos compartidos.

package com.journaldev.threads;

public class ThreadSafety {

    public static void main(String[] args) throws InterruptedException {
    
        ProcessingThread pt = new ProcessingThread();
        Thread t1 = new Thread(pt, "t1");
        t1.start();
        Thread t2 = new Thread(pt, "t2");
        t2.start();
        // Esperar a que los hilos terminen de procesar
        t1.join();
        t2.join();
        System.out.println("Processing count="+pt.getCount());
    }

}

class ProcessingThread implements Runnable{
    private int count;
    
    @Override
    public void run() {
        for(int i=1; i < 5; i++){
            processSomething(i);
        	count++;
        }
    }

    public int getCount() {
        return this.count;
    }

    private void processSomething(int i) {
        // Procesando algún trabajo
        try {
            Thread.sleep(i*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
}

En el programa anterior, en el bucle for, count se incrementa en 1 cuatro veces y, dado que tenemos dos hilos, su valor debería ser 8 después de que ambos hilos hayan terminado de ejecutarse. Pero cuando ejecutas el programa anterior varias veces, notarás que el valor de count varía entre 6, 7, 8. Esto sucede porque incluso si count++ parece ser una operación atómica, NO lo es y causa corrupción de datos.

Seguridad de hilos en Java

La seguridad de hilos en Java es el proceso para hacer que nuestro programa sea seguro de usar en un entorno multihilo. Hay diferentes formas de lograr que nuestro programa sea seguro de hilos.

  • La sincronización es la herramienta más fácil y ampliamente utilizada para lograr la seguridad de hilos en Java.
  • Uso de clases de envoltura atómica del paquete java.util.concurrent.atomic. Por ejemplo, AtomicInteger
  • Uso de bloqueos del paquete java.util.concurrent.locks.
  • Uso de clases de colección seguras para hilos. Consulte esta publicación para ver el uso de ConcurrentHashMap para lograr la seguridad de hilos.
  • Uso de la palabra clave volatile con variables para hacer que cada hilo lea los datos desde la memoria, no desde la caché del hilo.

Java synchronized

La sincronización es la herramienta mediante la cual podemos lograr la seguridad de hilos. JVM garantiza que el código sincronizado será ejecutado por solo un hilo a la vez. La palabra clave synchronized de Java se utiliza para crear código sincronizado e internamente utiliza bloqueos en Objeto o Clase para asegurar que solo un hilo esté ejecutando el código sincronizado.

  • Java synchronization funciona mediante el bloqueo y desbloqueo del recurso. Antes de que cualquier hilo entre en el código sincronizado, debe adquirir el bloqueo del objeto, y al finalizar la ejecución del código, libera el recurso que puede ser bloqueado por otros hilos. Mientras tanto, otros hilos están en estado de espera para bloquear el recurso sincronizado.
  • Podemos usar la palabra clave `synchronized` de dos maneras: una es hacer un método completo sincronizado y la otra es crear un bloque sincronizado.
  • Cuando un método está sincronizado, bloquea el Objeto; si el método es estático, bloquea la Clase. Por lo tanto, siempre es una buena práctica usar un bloque sincronizado para bloquear solo las secciones del método que necesitan sincronización.
  • Al crear un bloque sincronizado, debemos proporcionar el recurso sobre el cual se adquirirá el bloqueo; puede ser `XYZ.class` o cualquier campo de objeto de la clase.
  • synchronized(this) bloqueará el Objeto antes de entrar en el bloque sincronizado.
  • Deberías usar el nivel más bajo de bloqueo. Por ejemplo, si hay varios bloques sincronizados en una clase y uno de ellos está bloqueando el Objeto, entonces los otros bloques sincronizados tampoco estarán disponibles para la ejecución por otros hilos. Al bloquear un Objeto, se adquiere un bloqueo en todos los campos del Objeto.
  • La sincronización en Java proporciona integridad de datos a costa del rendimiento, por lo que solo debe usarse cuando sea absolutamente necesario.
  • La sincronización en Java solo funciona en la misma JVM, por lo que si necesitas bloquear algún recurso en un entorno de múltiples JVM, no funcionará y es posible que debas buscar algún mecanismo de bloqueo global.
  • Java Synchronization podría resultar en bloqueos, revisa este artículo sobre bloqueo en Java y cómo evitarlo.
  • La palabra clave synchronized de Java no puede ser utilizada para constructores y variables.
  • Es preferible crear un objeto privado ficticio para usar en el bloque synchronized, de modo que su referencia no pueda ser cambiada por otro código. Por ejemplo, si tienes un método setter para un objeto en el que estás sincronizando, su referencia puede ser cambiada por algún otro código, lo que lleva a la ejecución paralela del bloque synchronized.
  • No deberíamos usar ningún objeto que esté mantenido en un pool constante, por ejemplo, no se debería usar String para sincronización porque si algún otro código también está bloqueando en el mismo String, intentará adquirir el bloqueo en el mismo objeto de referencia desde el pool de Strings y aunque ambos códigos no estén relacionados, se bloquearán mutuamente.

Aquí están los cambios de código que necesitamos hacer en el programa anterior para hacerlo a prueba de hilos.

    // Variable de objeto ficticio para sincronización
    private Object mutex=new Object();
    ...
    // Usando un bloque synchronized para leer, incrementar y actualizar el valor del contador de manera sincrónica
    synchronized (mutex) {
            count++;
    }

Vamos a ver algunos ejemplos de sincronización y qué podemos aprender de ellos.

public class MyObject {
 
  // Bloquea el monitor del objeto
  public synchronized void doSomething() { 
    // ...
  }
}
 
// Código de hackers
MyObject myObject = new MyObject();
synchronized (myObject) {
  while (true) {
    // Retraso indefinido en myObject
    Thread.sleep(Integer.MAX_VALUE); 
  }
}

Observa que el código del hacker intenta bloquear la instancia de `myObject` y una vez que obtiene el bloqueo, nunca lo libera, lo que provoca que el método `doSomething()` se bloquee esperando el bloqueo. Esto causará que el sistema entre en un estado de bloqueo y genere una Denegación de Servicio (DoS).

public class MyObject {
  public Object lock = new Object();
 
  public void doSomething() {
    synchronized (lock) {
      // ...
    }
  }
}

//código no confiable

MyObject myObject = new MyObject();
//cambia la referencia del objeto de bloqueo
myObject.lock = new Object();

Observa que el objeto de bloqueo es público y al cambiar su referencia, podemos ejecutar un bloque sincronizado en múltiples hilos. Un caso similar es válido si tienes un objeto privado pero tienes un método setter para cambiar su referencia.

public class MyObject {
  //bloquea el monitor del objeto de la clase
  public static synchronized void doSomething() { 
    // ...
  }
}
 
//código del hacker
synchronized (MyObject.class) {
  while (true) {
    Thread.sleep(Integer.MAX_VALUE); // Indefinitely delay MyObject
  }
}

Observa que el código del hacker obtiene un bloqueo en el monitor de la clase y no lo libera, lo que causará un bloqueo y DoS en el sistema. Aquí hay otro ejemplo donde múltiples hilos están trabajando en el mismo array de Strings y, una vez procesados, están agregando el nombre del hilo al valor del array.

package com.journaldev.threads;

import java.util.Arrays;

public class SyncronizedMethod {

    public static void main(String[] args) throws InterruptedException {
        String[] arr = {"1","2","3","4","5","6"};
        HashMapProcessor hmp = new HashMapProcessor(arr);
        Thread t1=new Thread(hmp, "t1");
        Thread t2=new Thread(hmp, "t2");
        Thread t3=new Thread(hmp, "t3");
        long start = System.currentTimeMillis();
        //inicia todos los hilos
        t1.start();t2.start();t3.start();
        //espera a que los hilos terminen
        t1.join();t2.join();t3.join();
        System.out.println("Time taken= "+(System.currentTimeMillis()-start));
        //verifica el valor de la variable compartida ahora
        System.out.println(Arrays.asList(hmp.getMap()));
    }

}

class HashMapProcessor implements Runnable{
    
    private String[] strArr = null;
    
    public HashMapProcessor(String[] m){
        this.strArr=m;
    }
    
    public String[] getMap() {
        return strArr;
    }

    @Override
    public void run() {
        processArr(Thread.currentThread().getName());
    }

    private void processArr(String name) {
        for(int i=0; i < strArr.length; i++){
            //procesa los datos y agrega el nombre del hilo
            processSomething(i);
            addThreadName(i, name);
        }
    }
    
    private void addThreadName(int i, String name) {
        strArr[i] = strArr[i] +":"+name;
    }

    private void processSomething(int index) {
        //procesando algún trabajo
        try {
            Thread.sleep(index*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
}

Aquí está la salida cuando ejecuto el programa anterior.

Time taken= 15005
[1:t2:t3, 2:t1, 3:t3, 4:t1:t3, 5:t2:t1, 6:t3]

Los valores del array de Strings están corruptos debido a datos compartidos y falta de sincronización. Así es como podemos cambiar el método addThreadName() para hacer que nuestro programa sea seguro para hilos.

    private Object lock = new Object();
    private void addThreadName(int i, String name) {
        synchronized(lock){
        strArr[i] = strArr[i] +":"+name;
        }
    }

Después de este cambio, nuestro programa funciona correctamente y aquí está la salida correcta del programa.

Time taken= 15004
[1:t1:t2:t3, 2:t2:t1:t3, 3:t2:t3:t1, 4:t3:t2:t1, 5:t2:t1:t3, 6:t2:t1:t3]

¡Eso es todo sobre la seguridad de hilos en Java, espero que hayas aprendido sobre la programación segura para hilos y el uso de la palabra clave synchronized!

Source:
https://www.digitalocean.com/community/tutorials/thread-safety-in-java