[Súper detallado] Exploración en profundidad de la seguridad de subprocesos en Java para hacer que su programa sea más confiable ~

¡Profundice en la seguridad de subprocesos en Java para que sus programas sean más confiables!

Comenzaremos con las siguientes cuatro preguntas y desentrañaremos los problemas de subprocesos múltiples de Java.

  1. ¿Qué es la seguridad de subprocesos?
  2. ¿Cómo lograr la seguridad del hilo?
  3. ¿Cuál es la diferencia entre los diferentes enfoques para la implementación de seguridad de subprocesos?
  4. ¿Cómo lograr la seguridad de subprocesos de HashMap?

1. ¿Qué es la seguridad de subprocesos?


La seguridad de subprocesos significa que cuando varios subprocesos acceden a recursos compartidos al mismo tiempo, no habrá incoherencias en los datos ni otras situaciones inesperadas. En la programación de subprocesos múltiples, la seguridad de los subprocesos es muy importante, ya que varios subprocesos pueden acceder y modificar los mismos datos al mismo tiempo, si no se sincronizan correctamente, puede generar problemas como inconsistencia de datos, condiciones de carrera y puntos muertos.

Para lograr la seguridad de subprocesos, se deben utilizar algunas tecnologías y métodos para garantizar la coherencia y sincronización de los datos, como el mecanismo de bloqueo, la operación atómica, las variables locales de subprocesos, etc. Las clases seguras para subprocesos de uso común incluyen Vector, CopyOnWriteArrayList, Hashtable, ConcurrentHashMap, clases atómicas, etc.

2. ¿Cómo lograr la seguridad del hilo?


En Java, la seguridad de subprocesos se puede lograr de varias maneras:

  1. palabra clave sincronizada: se lee como "Sen Ke Nai Ri De". El mecanismo de bloqueo más básico en Java. El uso de la palabra clave sincronizada puede garantizar la exclusión mutua cuando varios subprocesos acceden a los recursos compartidos, lo que garantiza que solo un subproceso pueda acceder a los recursos compartidos al mismo tiempo. Se puede utilizar en métodos o bloques de código. Cuando un método o bloque de código es modificado por la palabra clave sincronizada, solo un subproceso puede ingresar al método o bloque de código, y otros subprocesos se bloquearán hasta que el subproceso actual termine de ejecutarse.

El principio de implementación de sincronizado: el bloque de código modificado por sincronizado se llama bloque de sincronización. Cuando un subproceso ingresa a un bloque de sincronización, intentará adquirir el bloqueo del objeto. Si el objeto no está bloqueado o el bloqueo del objeto tiene adquirido, el contador de bloqueo +1; si el objeto ha sido bloqueado por otros subprocesos, el subproceso entrará en un estado bloqueado, esperando que otros subprocesos liberen el bloqueo. Cuando otros subprocesos liberan el bloqueo, el subproceso en espera se activará y volverá a intentar adquirir el bloqueo y ejecutar el código en el bloque sincronizado. Al mismo tiempo, solo un hilo puede adquirir el bloqueo del objeto y ejecutar el código en el bloque sincronizado.

Encabezado de objeto: en Java, cada objeto tiene un encabezado de objeto (encabezado de objeto), que se utiliza para almacenar los metadatos del objeto, incluido el código hash del objeto (hashCode), el estado de bloqueo, el estado de la marca GC y otra información. El tamaño de la cabecera del objeto es fijo, ocupando normalmente 8 bytes (sistema de 64 bits) o 4 bytes (sistema de 32 bits).
La información más importante en el encabezado del objeto es el estado de bloqueo, que se utiliza para implementar el mecanismo de sincronización de la palabra clave sincronizada en Java. El valor del estado de bloqueo puede ser un estado sin bloqueo, un estado de bloqueo sesgado, un estado de bloqueo ligero o un estado de bloqueo pesado. El estado de bloqueo depende de la contención entre subprocesos y de cómo se usa el bloqueo.

Los siguientes métodos se utilizan para la palabra clave sincronizada:

public class Counter {
    
    
    private int count;

    // synchronized 修饰方法
    public synchronized void increment() {
    
    
        count++;
    }

    // synchronized 修饰代码块
    public void add(int n) {
    
    
        synchronized (this) {
    
    
            count += n;
        }
    }
}

Puntos a tener en cuenta al usar la palabra clave sincronizada:

  • El método es un método de instancia (no un método estático)

Pros: fácil de usar, admite candados reutilizables.
Desventajas: problemas de rendimiento, solo puede proteger bloques de código o métodos.

  1. palabra clave volátil: se lee como "soy demasiado europeo". Solo se puede utilizar para modificar variables. El uso de la palabra clave volátil puede garantizar la visibilidad de la variable, incluso cuando varios subprocesos acceden a la misma variable al mismo tiempo, se garantiza que el valor de la variable sea coherente. Además, volátil también tiene el efecto de prohibir el reordenamiento de instrucciones. Cuando una variable es modificada por volátil, todos los subprocesos que acceden a la variable leen el último valor de la memoria principal.

Visibilidad: si dos subprocesos modifican una variable volátil al mismo tiempo, dado que la variable volátil puede garantizar la visibilidad, los resultados de su modificación se actualizarán inmediatamente en la memoria principal, de modo que otro subproceso pueda leer el valor más reciente.

El orden de las operaciones de lectura es el mismo que el orden de las operaciones de escritura: si un subproceso lee una variable volátil y antes de escribirla, otro subproceso también lee la misma variable volátil, luego de que el primer subproceso escribe en la variable, el valor de la variable leída por otro subproceso es el último valor escrito por el primer subproceso, no el valor en el momento de la lectura.

Reorganización de instrucciones: para optimizar la eficiencia de la ejecución del programa, el procesador o el compilador cambia el orden de ejecución de las instrucciones sin cambiar los resultados de ejecución del programa original, a fin de reducir el tiempo de espera de la ejecución de la instrucción y aprovechar la multi- nivel de rendimiento del procesador Canalización, reducción de errores de predicción de saltos, etc. En un entorno de subproceso único, el reordenamiento de instrucciones no causará ningún problema, porque el resultado final de la ejecución no cambiará. Pero en un entorno de subprocesos múltiples, el reordenamiento de instrucciones puede generar algunos resultados inesperados, como inconsistencia de datos, interbloqueo, bucle infinito y otros problemas.

Los siguientes métodos utilizan la palabra clave volatile:

public class VolatileExample {
    
    
    private volatile int count = 0;

    public void increment() {
    
    
        count++;
    }

    public int getCount() {
    
    
        return count;
    }
}

Ventajas: visibilidad de las variables para todos los subprocesos, lectura y escritura secuenciales
Desventajas: no se puede garantizar la atomicidad, la lectura y escritura frecuente de variables volátiles es costosa

  1. Bloqueo de bloqueo: el uso del mecanismo de bloqueo de bloqueo puede lograr operaciones de bloqueo más flexibles, que son más eficientes que la palabra clave sincronizada. El bloqueo de bloqueo más utilizado es el bloqueo reentrante ReentrantLock, y Reentrant se pronuncia como "Ryan usa especial".

Una cerradura reentrante significa que una misma cerradura puede ser bloqueada y desbloqueada repetidamente, cada operación de bloqueo debe corresponder a una operación de desbloqueo, de lo contrario la cerradura estará siempre ocupada. La palabra clave sincronizada también es un bloqueo reentrante.

Los siguientes métodos se utilizan para bloqueos de reentrada ReentrantLock:

import java.util.concurrent.locks.ReentrantLock;

public class Demo {
    
    
    private ReentrantLock lock = new ReentrantLock();

    public void method1() {
    
    
        lock.lock();
        try {
    
    
            System.out.println("method1");
            method2();
        } finally {
    
    
            lock.unlock();
        }
    }

    public void method2() {
    
    
        lock.lock();
        try {
    
    
            System.out.println("method2");
        } finally {
    
    
            lock.unlock();
        }
    }
}


Características de las cerraduras reentrantes:

  • Se admite el bloqueo repetido. En el mismo subproceso, un bloqueo reentrante puede bloquear el mismo bloqueo varias veces sin punto muerto.

Bloqueo repetido: hay una variable (contador de bloqueo) similar a un contador en el interior. Al bloquear, el contador es +1, al desbloquear, el contador es -1 y el bloqueo se libera cuando el contador es 0.

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockDemo {
    
    
    private ReentrantLock lock = new ReentrantLock();

    public void foo() {
    
    
        lock.lock(); // 第一次加锁
        System.out.println(Thread.currentThread().getName() + " get lock.");
        lock.lock(); // 第二次加锁
        System.out.println(Thread.currentThread().getName() + " get lock again.");
        lock.unlock(); // 第一次释放锁
        System.out.println(Thread.currentThread().getName() + " release lock.");
        lock.unlock(); // 第二次释放锁
        System.out.println(Thread.currentThread().getName() + " release lock again.");
    }

    public static void main(String[] args) {
    
    
        ReentrantLockDemo demo = new ReentrantLockDemo();

        Thread t1 = new Thread(() -> {
    
    
            demo.foo();
        }, "Thread-1");

        Thread t2 = new Thread(() -> {
    
    
            demo.foo();
        }, "Thread-2");

        t1.start();
        t2.start();
    }
}

//输出结果
//Thread-1 get lock.
//Thread-1 get lock again.
//Thread-1 release lock.
//Thread-1 release lock again.
//Thread-2 get lock.
//Thread-2 get lock again.
//Thread-2 release lock.
//Thread-2 release lock again.

  • Admite bloqueo justo y bloqueo injusto. Un bloqueo reentrante puede especificar si es un bloqueo justo o un bloqueo injusto, y el valor predeterminado es un bloqueo injusto.

Bloqueo justo y bloqueo injusto: bloqueo justo significa que cuando varios subprocesos están esperando el bloqueo, adquieren el bloqueo en secuencia de acuerdo con el tiempo de espera, es decir, una estrategia de orden de llegada. Los bloqueos injustos aprovechan directamente los bloqueos independientemente del tiempo de espera, lo que puede provocar que algunos subprocesos no puedan obtener los bloqueos.

  • Se admite la respuesta a interrupciones. Los bloqueos reentrantes permiten responder a las interrupciones mientras se espera el bloqueo.

  • Se admiten múltiples variables de condición. Los bloqueos de reentrada pueden crear una cola de espera para cada variable de condición y pueden esperar o activar una cantidad específica de subprocesos en la variable de condición.

Ventajas: los bloqueos se pueden adquirir repetidamente para evitar interbloqueos, buen rendimiento, escalable (bloqueos justos, bloqueos injustos, bloqueos de lectura y escritura reentrantes, etc.)
Desventajas: código complejo

  1. Operación atómica: la operación atómica es un método de operación seguro para subprocesos sin bloqueo, que puede garantizar la coherencia de los datos cuando varios subprocesos acceden a la misma variable al mismo tiempo. En Java, la seguridad de subprocesos se logra utilizando el método de operación atómica en la clase de operación atómica.

Ventajas: garantiza la integridad de la operación, no es necesario bloquear, garantiza la coherencia y la visibilidad de los datos
Desventajas: no se puede garantizar el orden de acceso simultáneo, no se puede garantizar la atomicidad de los datos (si los datos de la operación son relativamente grandes, todavía es necesario bloquearse para garantizar la atomicidad), la implementación es más complicada

  1. Clases de colección seguras para subprocesos: Java proporciona algunas clases de colección seguras para subprocesos, como Vector, CopyOnWriteArrayList, Hashtable, ConcurrentHashMap, etc.

Vector: puede entenderse como un ArrayList seguro para subprocesos. El método proporcionado es similar al de ArrayList, y se implementa utilizando la palabra clave sincronizada. Es relativamente antiguo y se puede comparar con la relación entre HashTable y HashMap.
CopyOnWriteArrayList: ArrayList seguro para subprocesos, copie la matriz original, luego modifique la nueva matriz y finalmente asígnela a la referencia de la matriz original. Es más eficiente que Vector.
Clases de operaciones atómicas: 7 tipos, incluidos AtomicInteger, AtomicLong y AtomicBoolean.

Es necesario seleccionar la tecnología y el método de seguridad de subprocesos apropiados según la situación específica para garantizar que varios subprocesos puedan acceder a los recursos compartidos de manera correcta y eficiente.

3. ¿Cuáles son las diferencias entre las diferentes implementaciones de seguridad de subprocesos?


Los diferentes métodos de implementación de seguridad de subprocesos tienen diferentes escenarios aplicables y rendimiento. Por ejemplo, la palabra clave sincronizada es adecuada para bloquear secciones críticas, lo que puede garantizar la seguridad de subprocesos, pero el rendimiento puede verse afectado; mientras que el uso de clases de colección seguras para subprocesos, como ConcurrentHashMap, puede mejorar el rendimiento y el rendimiento simultáneo en situaciones de alta simultaneidad.

granularidad actuación Facilidad de uso
sincronizado Método modificado o bloque de código, el objeto es la clase completa o el método completo, una variable miembro o bloque de código en la clase Bajo rendimiento, no apto para escenarios de alta concurrencia. Simplemente agregue la palabra clave sincronizada antes del método o bloque de código que debe sincronizarse
volátil Variable modificada, el objeto de la acción es la variable. Altos gastos generales al leer y escribir con frecuencia Simplemente agregue la palabra clave volátil antes de la variable
ReentrantLock bloqueo de reentrada El objeto de la acción es una variable o un bloque de código. Tiene un buen rendimiento y es adecuado para escenarios de alta concurrencia. Debe bloquear y liberar manualmente el bloqueo usted mismo
operación atómica El objeto de la acción es una variable o un bloque de código. relativamente bajo La implementación es relativamente complicada y requiere una comprensión profunda de la plataforma de hardware y el sistema operativo, y tiene requisitos relativamente altos para los desarrolladores.

4. ¿Cómo lograr la seguridad de subprocesos de HashMap?


  1. HashMap和Hashtable
la diferencia mapa hash Tabla de picadillo
seguridad del hilo Inseguro, requiere sincronización adicional Seguridad
valor nulo Tanto la clave como el valor pueden ser nulos No permitido
Capacidad inicial y mecanismo de expansión La capacidad inicial predeterminada es 16 y el mecanismo de expansión consiste en duplicar la longitud del arreglo cuando la cantidad de elementos es mayor que el producto del factor de carga y la longitud del arreglo. La capacidad inicial predeterminada es 11. El mecanismo de expansión consiste en duplicar la longitud de la matriz y agregar 1 cuando el número de elementos es mayor que la longitud de la matriz.
modo transversal Realizado por iterador Realizado por enumeración
  1. Preguntas frecuentes sobre HashTable:
    HashTable es seguro para subprocesos pero HashMap no es seguro para subprocesos: Hashtable utiliza un mecanismo de sincronización para garantizar la seguridad de subprocesos, es decir, la palabra clave sincronizada se usa en cada método público para garantizar que solo un subproceso opere Hashtable en un momento. HashMap no utiliza este mecanismo de sincronización.

Código fuente del método público HashTable:

// 判断Hashtable中是否存在某个value
public synchronized boolean contains(Object value) {
    
    
    if (value == null) {
    
    
        throw new NullPointerException();
    }

    Entry<?,?> tab[] = table;
    for (int i = tab.length ; i-- > 0 ;) {
    
    
        for (Entry<?,?> e = tab[i] ; e != null ; e = e.next) {
    
    
            if (e.value.equals(value)) {
    
    
                return true;
            }
        }
    }
    return false;
}

No se permite que la clave y el valor de HashTable sean nulos: para Hashtable, al insertar un elemento, si ya hay un elemento en el depósito, use el método equals para comparar si la clave recién insertada es igual a la clave existente en el depósito. cubo.Al comparar si la clave es igual El método de igualdad de la clave debe llamarse.Si la clave es nula, no hay método de igualdad. No se permite que el valor de Hashtable sea nulo, porque dentro de Hashtable, el valor se almacena en una matriz de tipo Objeto, y cada elemento de la matriz es un objeto de valor independiente, no una referencia a un valor. Por lo tanto, permitir que el valor sea nulo daría como resultado la incapacidad de distinguir entre las ranuras vacías en la matriz y las ranuras que realmente almacenan valores nulos. Esto puede causar resultados inesperados al operar en Hashtable. Para evitar esto, Hashtable no permite valores nulos.

La clave y el valor de HashMap permiten valores nulos: el objetivo de diseño de HashMap es proporcionar operaciones de búsqueda, inserción y eliminación eficientes tanto como sea posible, por lo que no hay restricciones en el tipo de valor. Esto permite a los usuarios utilizar libremente nulo como valor cuando sea necesario, lo que aumenta la flexibilidad. En HashMap, si la clave es nula, su valor hash es 0, por lo que se colocará en la posición 0 de la tabla hash.

  1. Implementar la seguridad de subprocesos de HashMap
  • Utilice ConcurrentHashMap
    , que es una implementación de HashMap segura para subprocesos proporcionada por Java. Implementa seguridad de subprocesos a través de bloqueos de segmento (Segmento), y múltiples subprocesos pueden acceder a diferentes segmentos al mismo tiempo, mejorando así la concurrencia.

La diferencia entre ConcurrentHashMap y HashTable: Hashtable se basa en la sincronización para lograr la seguridad de subprocesos, mientras que ConcurrentHashMap usa bloqueos segmentados para lograr la seguridad de subprocesos. Si necesita usar una tabla hash en un entorno de subprocesos múltiples, se recomienda usar ConcurrentHashMap.

  • Use Collections.synchronizedMap
    , que es una clase de herramienta proporcionada por Java para envolver un mapa no seguro para subprocesos en un mapa seguro para subprocesos. Logra la seguridad de subprocesos al agregar bloqueos de sincronización (usando la palabra clave sincronizada) a las operaciones de mapa.

Ejemplo de código seguro para subprocesos utilizando Collections.synchronizedMap:

Map<String, String> synchronizedMap = Collections.synchronizedMap(new HashMap<>());

synchronizedMap.put("key1", "value1");
synchronizedMap.put("key2", "value2");

String value = synchronizedMap.get("key1");
System.out.println(value);

  • Uso del mecanismo de bloqueo
    Puede implementar el mecanismo de bloqueo usted mismo para garantizar la seguridad de subprocesos de HashMap. Por ejemplo, puede usar la palabra clave sincronizada o ReentrantLock para implementar el mecanismo de bloqueo para garantizar que solo un subproceso acceda al HashMap al mismo tiempo.

En un entorno de subprocesos múltiples, se recomienda usar ConcurrentHashMap porque tiene una mayor simultaneidad y un mejor rendimiento, pero debe prestar atención a algunos detalles, como la necesidad de usar iteradores al atravesar. Y si se trata de un simple requisito de seguridad de subprocesos, puede considerar el uso de Collections.synchronizedMap.

Supongo que te gusta

Origin blog.csdn.net/m0_56170277/article/details/129786479
Recomendado
Clasificación