[Conceptos básicos de Java] Clase de sincronización de subprocesos CountDownLatch

Sobre el autor: socio de contenido de CSDN, experto técnico, comenzando desde cero para hacer decenas de millones de aplicaciones de actividades diarias.
Concéntrese en compartir series originales de artículos en varios campos, buenos en backend de Java, desarrollo móvil, inteligencia artificial, etc. Espero que me apoyen mucho.

Ocurrió que CountDownLatch se usó en el proyecto de hoy, así que solo resumiremos. A través de este artículo, puede aprender qué es CountDownLatch y su principio, los escenarios de uso de CountDownLatch, etc.

Antes de leer este artículo, espero que tenga las siguientes reservas de conocimiento:

  1. AQS - AbstractQueuedSynchronizer (por actualizar)
  2. CAS (Comparar e intercambiar) (por actualizar)
  3. [Conceptos básicos de Java] palabra clave volátil

1. Introducción

Seguimos resumiendo y aprendiendo los conceptos básicos de Java , repasando el pasado y aprendiendo lo nuevo.

2. Resumen

CountDownLatch es una herramienta de sincronización proporcionada por JDK, que permite que uno o más subprocesos esperen hasta que se complete un conjunto de operaciones en otros subprocesos.

CountDownLatch construye un sincronizador basado en AQS:
AQS - AbstractQueuedSynchronizer, un sincronizador de cola abstracto, es un marco para construir bloqueos y sincronizadores .

Use LockSupport.park(this); para suspender el hilo y, finalmente, llame al parque de UnSafe y luego llame a la capa nativa.

Nota : Cuando el valor del contador se reduce a 0, no se puede restaurar.

2.1 Función

Cree bloqueos y sincronizadores para esperar subprocesos.

2.2 Escenarios de uso

CountDownLatch es adecuado para escenarios de subprocesos múltiples donde es necesario esperar a que todos los subprocesos terminen de ejecutarse antes de realizar operaciones.

CountDownLatch puede entenderse como un contador concurrente. El escenario de uso principal es que cuando una tarea se divide en varias subtareas , es necesario esperar a que todas las subtareas se completen antes de operar, de lo contrario, el hilo (hilo actual) se bloqueará . Cada vez que se completa una tarea, el contador será - 1 hasta no.

Generalmente se usa como un contador de cuenta regresiva de múltiples subprocesos, lo que los obliga a esperar un grupo de otras tareas.La resta del contador es un proceso irreversible.

ej.: Come cuando todos estén llenos. Si no hay suficientes personas, no puedes mover los palillos. Todos se sientan y esperan.
Esperar a la gente en una reunión, y cuando no todos están presentes, todos se sientan y esperan.
La secuencia de inicio, la secuencia y las dependencias de la secuencia de inicio.

Tengamos un trozo de código:

public class Waitress implements Runnable {
    
    
    private CountDownLatch latch;
    private String name;

    public Waitress(CountDownLatch latch, String name) {
    
    
        this.latch = latch;
        this.name = name;
    }

    @Override
    public void run() {
    
    
        try {
    
    
            SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSS");
            System.out.println(sdf.format(new Date()) + " " + name  + "等待顾客");
            latch.await();
            System.out.println(sdf.format(new Date()) + " " + name  + "开始上菜");
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
    }
}

Cliente

public class Customer implements Runnable {
    
    
    // 主线程 countdown
    private CountDownLatch mainThreadLatch;

    // 当前线程 countdown
    private CountDownLatch latchThread;
    private String name;

    public Customer(CountDownLatch mainThreadLatch, String name) {
    
    
        this.mainThreadLatch = mainThreadLatch;
        this.name = name;
        latchThread = new CountDownLatch(1);
        try {
    
    
            // 阻塞当前线程 1s
            latchThread.await(1000, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
    
    
        try {
    
    
            SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSS");
            Random random = new Random();

            System.out.println(sdf.format(new Date()) + " " + name + "出发去饭店");
            Thread.sleep((long) (random.nextDouble() * 3000) + 1000);
            System.out.println(sdf.format(new Date()) + " " + name + "到了饭店");
            mainThreadLatch.countDown();
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
    }
}

código de prueba

public static void main(String[] args) throws InterruptedException {
    
    
    CountDownLatch latch = new CountDownLatch(3);

    List<Thread> threads = new ArrayList<>(3);
    threads.add(new Thread(new Customer(latch, "张三")));
    threads.add(new Thread(new Customer(latch, "李四")));
    threads.add(new Thread(new Customer(latch, "王五")));
    for (Thread thread : threads) {
    
    
        thread.start();
    }

    Thread.sleep(100);
    new Thread(new Waitress(latch, "♥小芳♥")).start();
}

2.3 Ventajas y desventajas

  • Ventajas: código elegante, no es necesario operar en el grupo de subprocesos y hay buenos escenarios de uso cuando el grupo de subprocesos se usa como un Bean.
  • Desventajas: Necesita saber el número de subprocesos por adelantado ; el rendimiento no es muy bueno. También es necesario agregar un juicio de excepción en el bloque de código del subproceso , de lo contrario, se produce una excepción antes de la cuenta regresiva y no se maneja, lo que hará que el subproceso principal se bloquee en espera para siempre.

En comparación con el método join(), CountDownLatch proporciona más API y es más flexible.

3. Principio

3.1 Principio

CountDownLatch se implementa mediante el bloqueo compartido de AQS - AbstractQueuedSynchronizer.
Al mismo tiempo, use CAS (Compare And Swap) para operar el contador.
Luego use el estado variable modificado por volátil para mantener el estado de cuenta regresiva, de modo que se pueda ver la variable compartida de subprocesos múltiples.

El núcleo de la estructura de datos de AQS son dos colas virtuales: la cola de sincronización y la cola de condición. Diferentes condiciones tendrán diferentes colas de condición.

Veamos el código fuente:

// 1、 创建CountDownLatch并设置计数器值,count代表计数器个数
//(内部是共享锁,本质就是上了几次锁)
public CountDownLatch(int count)

// 2、启动多线程并且调用CountDownLatch实例的countDown()方法
// 每countDown一次,计数器就减一,就是释放了一次共享锁,直到为0全部结束
//调用countDown的线程可以继续执行,不需要等待计数器被减到0,
//只是调用await方法的线程需要等待。
public void countDown()

// 3、 主线程调用 await() 方法,这样主线程的操作就会在这个方法上阻塞,
//直到count值为0,停止阻塞,主线程继续执行
// 在AQS队列里一直等待,直到countDown减到0,才会继续往下执行,
// 使用 LockSupport.park(this);  挂起线程
//方法在倒计数为0之前会阻塞 = 当前线程 =
public void await()

// 等待一定时间
public void await(long timeout, TimeUnit unit)

Las tareas de la subclase son:

  1. El estado de la variable compartida se mantiene mediante operaciones CAS.
  2. Anula cómo se obtienen los recursos.
  3. Anule la forma en que se liberan los recursos.

CountDownLatch tiene una clase interna llamada Sync (sɪŋk), que hereda la clase AbstractQueuedSynchronizer, que mantiene un estado entero y garantiza la visibilidad y la atomicidad de la modificación del estado.
Al crear una instancia de CountDownLatch, también se crea una instancia de Sync y el valor del contador se pasa a la instancia de Sync, de la siguiente manera:

public CountDownLatch(int count) {
    
    
    if (count < 0) throw new IllegalArgumentException("count < 0");
    this.sync = new Sync(count);
}

En el método countDown, solo se llama al método releaseShared de la instancia Sync, específicamente de la siguiente manera:

public void countDown() {
    
    
    sync.releaseShared(1);
}

El método releaseShared primero decrementa el contador en 1. Si el contador es 0 después de decrementar 1, active todos los subprocesos bloqueados por el método await, específicamente de la siguiente manera:

public final boolean releaseShared(int arg) {
    
    
    if (tryReleaseShared(arg)) {
    
     //对计数器进行减一操作
        doReleaseShared();//如果计数器为0,唤醒被await方法阻塞的所有线程
        return true;
    }
    return false;
}

El método tryReleaseShared primero obtiene el valor del contador actual, y devuelve directamente si el contador es 0; si no es 0, utiliza el método CAS (Compare And Swap) para decrementar el contador en 1, concretamente de la siguiente manera:

protected boolean tryReleaseShared(int releases) {
    
    
    for (;;) {
    
    //死循环,如果CAS操作失败就会不断继续尝试。  自旋不断判断是否释放了state同步锁
        int c = getState();//获取当前计数器的值。
        if (c == 0)// 计数器为0时,就直接返回。说明之前已经释放了同步锁,这时候就不需要做任何操作了,因为之前已经做完了
            return false;
            
        // state - 1释放一次同步锁
        int nextc = c-1;
        
        // 通过CAS设置state同步状态,如果成功判断state是否为0,为0代表锁全部释放
        // 被await的程序可以继续往下执行了
        if (compareAndSetState(c, nextc))// 使用CAS方法对计数器进行减1操作
            return nextc == 0;//如果操作成功,返回计数器是否为0
    }
}

3.2 Cómo CountDownLatch bloquea hilos

El principio del bloqueo de subprocesos, en el método await, se llama al método adquireSharedInterruptably de la instancia Sync:

// 创建一个节点,加入到AQS阻塞队列,并同时把当前线程挂起
public void await() throws InterruptedException {
    
    
    sync.acquireSharedInterruptibly(1);
}

Entre ellos, el método adquirirSharedInterruptably determina si el contador es 0, y si no es 0, bloquea el hilo actual, específicamente de la siguiente manera:

public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    
    
    if (Thread.interrupted())
        throw new InterruptedException();
    if (tryAcquireShared(arg) < 0)//判断计数器是否为0
        doAcquireSharedInterruptibly(arg);//如果不为0 则阻塞当前线程
}


private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException {
    
    
    // 以共享模式添加到等待队列 ,新建节点加入阻塞队列   
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
    
    
        for (;;) {
    
    
            // 返回前一个节点
            final Node p = node.predecessor();
            if (p == head) {
    
    
                int r = tryAcquireShared(arg); //返回锁的state

                if (r >= 0) {
    
    
                    setHeadAndPropagate(node, r);
                    p.next = null;
                    failed = false;
                    return;
                }
            }
            // 检查并更新未能获取的节点的状态。如果线程应该阻塞,则返回 true
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
    
    
        // 失败就取消
        if (failed)
            cancelAcquire(node);
    }
}

// 挂起线程
private final boolean parkAndCheckInterrupt() {
    
    
    LockSupport.park(this);
    return Thread.interrupted();
}

Entre ellos, el método tryAcquireShared es un método de plantilla en AbstractQueuedSynchronizer. Su implementación específica está en la clase Sync. Juzga principalmente si el contador es cero. Si es cero, devuelve 1. Si no es cero, devuelve -1. En concreto, es el siguiente:

 // await判断共享锁是否全部释放,是则从队列中移除,继续往下执行,实现AQS的模板方法
protected int tryAcquireShared(int acquires) {
    
    
    return (getState() == 0) ? 1 : -1;
}

4. Lectura recomendada

[Conceptos básicos de Java] Atomicidad, visibilidad, orden

[Conceptos básicos de Java] Ocurre antes de la visibilidad de Java

[Conceptos básicos de Java] entrevista java-android sincronizada

[Conceptos básicos de Java] entrevista java-android - estado del hilo

[Conceptos básicos de Java] Tema relacionado

[Conceptos básicos de Java] excepción de Java

[Conceptos básicos de Java] reflejo de Java

[Conceptos básicos de Java] genéricos de Java

[Conceptos básicos de Java] anotaciones de Java

[Conceptos básicos de Java] proxy dinámico de Java

[Conceptos básicos de Java] Java SPI

[Fundamentos de Java] Java SPI II Java APT

[Conceptos básicos de Java] jvm montón, pila, área de método y modelo de memoria java

[Conceptos básicos de Java] palabra clave volátil

Supongo que te gusta

Origin blog.csdn.net/fumeidonga/article/details/131541258
Recomendado
Clasificación