[Java|Multi-threading and high concurrency] Clases e interfaces comúnmente utilizadas en JUC


inserte la descripción de la imagen aquí

1. ¿Qué es JUC?

JUC es un módulo importante en la programación concurrente de Java. Su nombre completo es Java Util Concurrent(Java Concurrency Toolkit). Proporciona un conjunto de clases de herramientas y marcos para la programación de subprocesos múltiples para ayudar a los desarrolladores a escribir código concurrente seguro para subprocesos de manera más conveniente.

Este artículo presenta principalmente Java Util Concurrentalgunas interfaces y clases de uso común.

2. Interfaz llamable

La interfaz Callable es similar a Runnable. Una diferencia es que la tarea descrita por Runable no tiene valor de retorno, mientras que la interfaz Callable tiene un valor de retorno.

Ejemplo:

Callable<返回值类型> callable = new Callable<Integer>() {
    
    
    @Override
    public 返回值类型 call() throws Exception {
    
    
       // 执行的任务      
    }
};

CallableLa interfaz define un call()método, por lo que este método debe implementarse al crear una instancia. Este método devuelve un resultado después de que se completa la ejecución de la tarea y puede generar una excepción.

A diferencia de Runnable, las tareas descritas por Callable no se pueden pasar directamente a los hilos para su ejecución, por lo que FutureTask<T>esta clase es necesaria.

FutureTask<返回值类型> futureTask = new FutureTask<>(callable);

Para obtener el valor de retorno de la tarea anterior, puede utilizar FuturTaskel método de obtención proporcionado.

Ejemplo:

    public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
        Callable<Integer> callable = new Callable<Integer>() {
    
    
            @Override
            public Integer call() throws Exception {
    
    
                int ret = 0;
                for (int i = 1; i <= 10; i++) {
    
    
                    ret += i;
                }
                return ret;
            }
        };
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        Thread t1 = new Thread(futureTask);
        t1.start();
        System.out.println(futureTask.get());
    }

resultado de la operación:
inserte la descripción de la imagen aquí

3. Bloqueo de reentrada

ReentrantLock: ReentrantLock es una clase de implementación de la interfaz Lock, que implementa todos los métodos de la interfaz Lock. ReentrantLock admite la reentrada, lo que significa que el mismo subproceso puede adquirir el mismo bloqueo varias veces sin punto muerto. Esta característica permite que ReentrantLock se use en escenarios de sincronización de subprocesos más complejos.

En ReentrantLock, hay tres métodos muy importantes:

  1. lock(): cerrar
  2. unlock(): desbloquear.
  3. tryLock(): se usa para intentar adquirir un bloqueo. Si el bloqueo está disponible, se adquirirá inmediatamente y devolverá verdadero. Si el bloqueo no está disponible, devolverá inmediatamente falso sin bloquear el hilo actual. También es posible especificar un tiempo máximo de espera para adquirir un bloqueo.

A diferencia de synchronized, sus operaciones de bloqueo y desbloqueo están separadas y debe agregarlas usted mismo.

Esto también puede provocar que si el código es anormal después del bloqueo, es posible que no se ejecute el método de desbloqueo. Esto también es ReentranLockuna pequeña desventaja de . Pero podemos try finallyevitarlo usándolo.

Hay dos versiones del método tryLock:
inserte la descripción de la imagen aquí

  • El método tryLock() sin parámetros se utiliza para intentar adquirir el bloqueo. Si el bloqueo está disponible, se adquirirá inmediatamente y devolverá verdadero. Si el bloqueo no está disponible, devolverá inmediatamente falso sin bloquear el hilo actual.

  • Y otra versión del método tryLock(), puede especificar un tiempo de espera para intentar adquirir el bloqueo

En el desarrollo real, a menudo es prudente usar esta " estrategia de espera muerta ". tryLock() nos permite tener más opciones en esta situación

ReentrantLockSe pueden implementar bloqueos justos. El valor predeterminado es injusto.

Pero cuando creamos una instancia y le pasamos parámetros true, se 公平锁convierte en

ReentrantLock reentrantLock = new ReentrantLock(true);

synchronizeCon wait/notifyel método para realizar la notificación de espera del hilo, el hilo despertado es aleatorio

ReentrantLockColabore con Conditionlas clases para implementar subprocesos que esperan notificaciones. Puede especificar subprocesos para despertar

Sincronizado es una palabra clave en Java, y la capa inferior está implementada por JVM (C++)

ReentranLock es una clase de la biblioteca estándar, y la capa inferior se implementa en base a Java

4. Clases atómicas

La clase atómica es para resolver el problema de la condición de carrera (Race Condition) y la inconsistencia de datos en un entorno de subprocesos múltiples. En un entorno de subprocesos múltiples, si varios subprocesos leen y escriben una variable compartida al mismo tiempo, pueden producirse incoherencias en los datos, lo que da como resultado resultados incorrectos.

Las clases atómicas están CASbasadas en la implementación.

Java proporciona una variedad de clases atómicas. Las clases atómicas comúnmente utilizadas son las siguientes:

  1. AtomicInteger: se utiliza para realizar operaciones atómicas en variables de tipo int.
  2. AtomicLong: Se utiliza para realizar operaciones atómicas sobre variables de tipo long.
  3. AtomicBoolean: Se utiliza para realizar operaciones atómicas sobre variables de tipo booleano.
  4. AtomicReference: Se utiliza para realizar operaciones atómicas sobre variables de tipo referencia.

Luego, use la clase atómica AtomicIntegerpara implementar la operación de dos subprocesos que incrementan la misma variable 50,000 veces.

Debido a que es un objeto de instancia de la clase, no podemos realizar directamente operaciones ++ en el objeto de instancia de la clase. Solo podemos usar algunos métodos proporcionados por la clase.

AtomicIntegerAlgunos métodos de:

AtomicInteger atomicInteger = new AtomicInteger();
// atomicInteger++
atomicInteger.getAndIncrement();
// ++atomicInteger
atomicInteger.incrementAndGet();
// atomicInteger--
atomicInteger.getAndDecrement();
// --atomicInteger
atomicInteger.decrementAndGet();
public class Demo23 {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        AtomicInteger atomicInteger = new AtomicInteger();
        
        Thread t1 = new Thread(()->{
    
    
            for (int i = 0; i < 50000; i++) {
    
    
                atomicInteger.getAndIncrement();
            }
        });

        Thread t2 = new Thread(() ->{
    
    
            for (int i = 0; i < 50000; i++) {
    
    
                atomicInteger.getAndIncrement();
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(atomicInteger);
    }
}

Resultado en ejecución:
inserte la descripción de la imagen aquí
si no usa la clase atómica, debe usarla synchronizedpara implementarla.

5. Grupo de subprocesos

El grupo de subprocesos se presentó en detalle en mi artículo anterior, por lo que no lo repetiré aquí. Los amigos interesados ​​pueden leer este artículo: [Java|Multi-threading and high concurrency] Explicación detallada del grupo de subprocesos

6. Cantidad de señal

El semáforo (Semaphore) mantiene un contador de licencias que indica el número de licencias disponibles. Cuando un subproceso necesita acceder a un recurso compartido, primero debe obtener un permiso. Si el recuento de permisos es 0, el subproceso se bloqueará hasta que haya un permiso disponible. Cuando un hilo termina usando un recurso compartido, debe liberar la licencia para que otros hilos puedan adquirir la licencia y acceder al recurso.

El número de licencias para un semáforo se puede establecer al crear una instancia de semáforo

// 设置信号量的许可数量为 5
Semaphore semaphore = new Semaphore(5);

Hay dos operaciones principales provistas en semáforos: P (esperar) y V (liberar).

  • Operación P: Intentará obtener una licencia de semáforo, si el número de licencia no es 0, la licencia se puede obtener con éxito y continuar ejecutándose, si el número de licencia es 0, el hilo se bloqueará hasta que otros hilos liberen la licencia.

  • Operación V: Se liberará una licencia de semáforo, para que otros hilos bloqueados puedan obtener la licencia y seguir ejecutándose.

El método correspondiente a la operación P esacquire()

El método correspondiente a la operación V esrelease()

Por ejemplo:

public class Demo24 {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        Semaphore semaphore = new Semaphore(5);
        semaphore.acquire();
        semaphore.acquire();
        semaphore.acquire();
        semaphore.acquire();
        semaphore.acquire();
        System.out.println("此时信号量的许可数量为0");
        semaphore.acquire();
        
        semaphore.release();
    }
}

Ejecución de resultados:
inserte la descripción de la imagen aquí
el semáforo puede limitar la cantidad de subprocesos que acceden a los recursos compartidos al mismo tiempo al controlar la cantidad de permisos, evitando así las condiciones de carrera y la inconsistencia de los datos.

7. CoutDownLatch

CountDownLatch(Countdown latch) es una herramienta de sincronización en la programación concurrente de Java, que se utiliza para esperar a que un grupo de subprocesos complete una determinada tarea.

A través del método de construcción de CountDownLatch, especifique el número de subprocesos en espera (contador).

// 设置等待线程的数量为 5
CountDownLatch countDownLatch = new CountDownLatch(5);

Cuando un subproceso ha completado su tarea, puede llamar al countDown()método CountDownLatch para disminuir el contador en 1. Otros subprocesos pueden esperar a que el contador se convierta en 0 llamando al await()método CountDownLatch.

Ejemplo:

public class Demo25 {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        CountDownLatch countDownLatch = new CountDownLatch(5);
        for (int i = 0;i < 5;i++){
    
    
            Thread t = new Thread(() ->{
    
    
                System.out.println(Thread.currentThread().getName()+" 执行任务");
                countDownLatch.countDown();
            });
            t.start();
        }

        countDownLatch.await();
    }
}

Especifique el número de subprocesos que esperan que CoutDownLatch sea 5 y cree subprocesos 5. Ejecute countDown()el método después de ejecutar el subproceso y llame await()al contador de espera para que se convierta en 0.

resultado de la operación:

inserte la descripción de la imagen aquí

Si el valor inicial del contador es mayor o igual que el número de subprocesos en espera, entrará en el estado de espera bloqueado.

Cambie el valor del contador a 6, el resultado de ejecución:
inserte la descripción de la imagen aquí

Para evitar la situación anterior, puede usar una versión sobrecargada de await para establecer el tiempo máximo de espera

inserte la descripción de la imagen aquí

8. Clases de colección seguras para subprocesos

  1. Hashtable和ConcurrentHashMap: una implementación de tabla hash segura para subprocesos que admite operaciones de lectura y escritura altamente simultáneas.

  2. CopyOnWriteArrayList: implementación de matriz dinámica segura para subprocesos, adecuada para escenarios con más lecturas y menos escrituras

  3. CopyOnWriteArraySet: La implementación de la colección segura para subprocesos, basada en CopyOnWriteArrayList, es adecuada para escenarios con más lecturas y menos escrituras.

  4. ConcurrentLinkedQueue: una implementación de cola ilimitada segura para subprocesos que admite operaciones de encolado y desencolado altamente concurrentes.

  5. BlockingQueueLas clases de implementación de la interfaz son: ArrayBlockingQueue, LinkedBlockingQueue, LinkedTransferQueueetc., que se utilizan para implementar colas de bloqueo seguras para subprocesos.

  6. ConcurrentSkipListMap: una tabla de mapeo ordenada implementada por una tabla de salto segura para subprocesos, que admite operaciones de lectura y escritura altamente simultáneas.

  7. ConcurrentSkipListSet: una colección ordenada implementada por una tabla de salto segura para subprocesos, que admite operaciones de lectura y escritura altamente simultáneas.

Para Hashtable y ConcurrentHashMap:
No se recomienda Hashtable . Es un synchronizedmétodo modificado. Es equivalente a bloquear esto. Una tabla hash tiene solo un bloqueo. Hay
推荐使用ConcurrentHashMapmuchas estrategias de optimización detrás de esta clase.

  • ConcurrentHashMap bloquea cada depósito hash.

    Cuando dos subprocesos acceden al mismo cubo hash, habrá un conflicto. Si no es el mismo cubo hash, no habrá conflicto de bloqueo. Por lo tanto, la probabilidad de conflicto de bloqueo se reduce considerablemente.

  • ConcurrentHashMap solo bloquea las operaciones de escritura, pero no bloquea las operaciones de lectura.

    Cuando varios subprocesos están escribiendo al mismo tiempo, habrá conflictos de bloqueo y no habrá conflictos de bloqueo al leer operaciones al mismo tiempo. Cuando algunos subprocesos están escribiendo, algunos subprocesos están leyendo. No hay problema de seguridad de subprocesos. ConcurrentHashMap garantiza que los datos leídos no se escribirán a medias, ni antes ni después.

  • ConcurrentHashMap aprovecha al máximo las características de CAS. Hay muchos lugares donde CAS se usa internamente en lugar de bloquear directamente

  • ConcurrentHashMap ha optimizado especialmente la operación de expansión.

    Durante el proceso de expansión, la tabla hash antigua y la tabla hash nueva existirán durante un período de tiempo al mismo tiempo. Cada vez que se realice la operación de la tabla hash, se moverá una parte de los elementos de la tabla hash antigua. hasta que se complete la transferencia.Evitar El tiempo de expansión es demasiado largo, lo que resulta en la situación de Caton

La diferencia entre HashMap, Hashtable y ConcurrentHashMap también es una pregunta común en las entrevistas.

Responda esta pregunta. En términos de seguridad de subprocesos, HashMap no es seguro para subprocesos. Hashtable y ConcurrentHashMap son seguros para subprocesos, y luego responda la diferencia entre Hashtable y ConcurrentHashMap. ¿Qué mejoras ha realizado ConcurrentHashMap en comparación con Hashtable, etc.?

CopyOnWriteArrayList es adecuado para escenarios que leen más y escriben menos.
En circunstancias normales, si algunos subprocesos están escribiendo (modificando), el subproceso de ventaja está leyendo y es probable que lea la mitad de los datos modificados. Por lo tanto, para resolver esto problema, CopyOnWriteArrayList hará una copia de los datos originales y la operación de escritura se realizará en los datos copiados

Pero si hay muchos datos/la modificación es muy frecuente, no es adecuado para su uso

¡Gracias por mirar! ¡Espero que este artículo pueda ayudarlo!
Columna: "Java Learning Journey from Scratch" se actualiza constantemente, ¡bienvenido a suscribirse!
"¡Deseo alentarlo y trabajar juntos!"

inserte la descripción de la imagen aquí

Supongo que te gusta

Origin blog.csdn.net/m0_63463510/article/details/131491870
Recomendado
Clasificación