Explicar el paquete concurrente de Java de una manera simple: análisis del principio CountDownLauch

Explicar el paquete concurrente de Java de una manera simple: análisis del principio CountDownLauch

Un rayo de cielo Tianyu Star IT Haha
CountDownLauch es una herramienta de sincronización establecida en el paquete de concurrencia de Java, que a menudo se denomina contador en concurrencia, y otro se llama bloqueo.
CountDownLauch se utiliza principalmente en dos escenarios: uno se llama conmutador, que permite que uno o un grupo de subprocesos esperen continuamente antes de que se complete una tarea. Esta situación a menudo se denomina cerradura. En términos sencillos, es equivalente a una puerta. Todos los hilos se bloquean antes de que se abra la puerta. Una vez que se abre la puerta, todos los hilos pasarán, pero una vez que se abre la puerta, todos los hilos se bloquean. Si pasa, el estado bloqueado se vuelve inválido y el estado de la puerta no se puede cambiar, solo el estado abierto. Otro escenario a menudo se llama contador. Permite dividir una tarea en N tareas pequeñas. El hilo principal espera hasta que se completen todas las tareas. Cuando se completa cada tarea, el contador se reduce en uno hasta que se completan todas las tareas. Bloqueo del hilo principal.
Echemos un vistazo a la API correspondiente a CountDownLauch.
Explicar el paquete concurrente de Java de una manera simple: análisis del principio CountDownLauch
Explicar el paquete concurrente de Java de una manera simple: análisis del principio CountDownLauch

CountDownLatch mantiene un contador positivo, el método countDown disminuye el contador y el método de espera espera a que el contador llegue a 0. Todos los subprocesos en espera se bloquearán hasta que el contador llegue a 0 o el subproceso en espera se interrumpa o se agote el tiempo.
Echemos un vistazo a un ejemplo de aplicación correspondiente:


package com.yhj.lauth;
import java.util.Date;
import java.util.concurrent.CountDownLatch;
//工人
class Worker extends Thread{
    privateintworkNo;//工号
    private CountDownLatch startLauch;//启动器-闭锁
    private CountDownLatch workLauch;//工作进程-计数器
    public Worker(int workNo,CountDownLatch startLauch,CountDownLatch workLauch) {
       this.workNo = workNo;
       this.startLauch = startLauch;
       this.workLauch = workLauch;
    }
    @Override
    publicvoid run() {
       try {
           System.out.println(new Date()+" - YHJ"+workNo+" 准备就绪!准备开工!");
           startLauch.await();//等待老板发指令
           System.out.println(new Date()+" - YHJ"+workNo+" 正在干活...");
           Thread.sleep(100);//每人花100ms干活
       } catch (InterruptedException e) {
           e.printStackTrace();
       }finally{
           System.out.println(new Date()+" - YHJ"+workNo+" 工作完成!");
           workLauch.countDown();
       }
    }
}
//测试用例
publicclass CountDownLauthTestCase {

    publicstaticvoid main(String[] args) throws InterruptedException {
       int workerCount = 10;//工人数目
       CountDownLatch startLauch = new CountDownLatch(1);//闭锁相当于开关
       CountDownLatch workLauch = new CountDownLatch(workerCount);//计数器
       System.out.println(new Date()+" - Boss:集合准备开工了!");
       for(int i=0;i<workerCount;++i){
           new Worker(i, startLauch, workLauch).start();
       }
       System.out.println(new Date()+" - Boss:休息2s后开工!");
       Thread.sleep(2000);
       System.out.println(new Date()+" - Boss:开工!");
       startLauch.countDown();//打开开关
       workLauch.await();//任务完成后通知Boss
       System.out.println(new Date()+" - Boss:不错!任务都完成了!收工回家!");
    }
}
执行结果:
Sat Jun 08 18:59:33 CST 2013 - Boss:集合准备开工了!
Sat Jun 08 18:59:33 CST 2013 - YHJ0 准备就绪!准备开工!
Sat Jun 08 18:59:33 CST 2013 - YHJ2 准备就绪!准备开工!
Sat Jun 08 18:59:33 CST 2013 - YHJ1 准备就绪!准备开工!
Sat Jun 08 18:59:33 CST 2013 - YHJ4 准备就绪!准备开工!
Sat Jun 08 18:59:33 CST 2013 - Boss:休息2s后开工!
Sat Jun 08 18:59:33 CST 2013 - YHJ8 准备就绪!准备开工!
Sat Jun 08 18:59:33 CST 2013 - YHJ6 准备就绪!准备开工!
Sat Jun 08 18:59:33 CST 2013 - YHJ3 准备就绪!准备开工!
Sat Jun 08 18:59:33 CST 2013 - YHJ7 准备就绪!准备开工!
Sat Jun 08 18:59:33 CST 2013 - YHJ5 准备就绪!准备开工!
Sat Jun 08 18:59:33 CST 2013 - YHJ9 准备就绪!准备开工!
Sat Jun 08 18:59:35 CST 2013 - Boss:开工!
Sat Jun 08 18:59:35 CST 2013 - YHJ0 正在干活...
Sat Jun 08 18:59:35 CST 2013 - YHJ2 正在干活...
Sat Jun 08 18:59:35 CST 2013 - YHJ1 正在干活...
Sat Jun 08 18:59:35 CST 2013 - YHJ4 正在干活...
Sat Jun 08 18:59:35 CST 2013 - YHJ8 正在干活...
Sat Jun 08 18:59:35 CST 2013 - YHJ6 正在干活...
Sat Jun 08 18:59:35 CST 2013 - YHJ3 正在干活...
Sat Jun 08 18:59:35 CST 2013 - YHJ7 正在干活...
Sat Jun 08 18:59:35 CST 2013 - YHJ5 正在干活...
Sat Jun 08 18:59:35 CST 2013 - YHJ9 正在干活...
Sat Jun 08 18:59:35 CST 2013 - YHJ5 工作完成!
Sat Jun 08 18:59:35 CST 2013 - YHJ1 工作完成!
Sat Jun 08 18:59:35 CST 2013 - YHJ3 工作完成!
Sat Jun 08 18:59:35 CST 2013 - YHJ6 工作完成!
Sat Jun 08 18:59:35 CST 2013 - YHJ7 工作完成!
Sat Jun 08 18:59:35 CST 2013 - YHJ9 工作完成!
Sat Jun 08 18:59:35 CST 2013 - YHJ4 工作完成!
Sat Jun 08 18:59:35 CST 2013 - YHJ0 工作完成!
Sat Jun 08 18:59:35 CST 2013 - YHJ2 工作完成!
Sat Jun 08 18:59:35 CST 2013 - YHJ8 工作完成!
Sat Jun 08 18:59:35 CST 2013 - Boss:不错!任务都完成了!收工回家!

En este ejemplo, se utilizan dos CountDownLauchs para construir dos escenarios respectivamente. El primer startLauch es equivalente a un interruptor. Antes de que se encienda, no se ejecuta ningún subproceso. Cuando se enciende, todos los subprocesos se pueden ejecutar al mismo tiempo. El segundo workerLauch es en realidad un contador. Cuando el contador no se reduce a cero, el hilo principal espera para siempre. Cuando se ejecutan todos los hilos, el hilo principal se desbloquea y continúa la ejecución.
El segundo escenario se usa a menudo en el grupo de subprocesos que aprenderemos más adelante, ¡lo discutiremos más adelante!
También hay una característica importante aquí, que es el
efecto de la consistencia de la memoria: la operación sucede antes de llamar a countDown () en el hilo inmediatamente sigue la operación que corresponde al retorno exitoso de await () desde otro hilo.
Hemos visto la aplicación de la escena, entonces, ¿en qué principio se basa y cómo se realiza?
Veamos el código fuente correspondiente:


privatestaticfinalclass Sync extends AbstractQueuedSynchronizer

En la segunda línea de la clase, vemos un sincronizador que implementa AQS en su interior. Centrémonos en los métodos que usamos: await y countDown. Primero mira el método de espera


publicvoid await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

Obviamente, se trata de llamar directamente al sincronizador interno reimplementado para adquirir el método de bloqueo compartido (hemos estado hablando de bloqueos exclusivos antes, y hoy aprovechamos esta oportunidad para hablar juntos del mecanismo de bloqueo compartido).


publicfinalvoid acquireSharedInterruptibly(int arg) throws InterruptedException {
        if (Thread.interrupted())
            thrownew InterruptedException();
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }

Aquí, si se interrumpe el hilo, saldrá directamente; de ​​lo contrario, intentará adquirir un bloqueo compartido. Veamos la implementación de tryAcquireShared (arg) (este método es anulado por una clase interna):


publicint tryAcquireShared(int acquires) {
            return getState() == 0? 1 : -1;
        }

El llamado bloqueo compartido significa que todos los subprocesos que comparten el bloqueo comparten el mismo recurso. Una vez que cualquier subproceso obtiene el recurso compartido, todos los subprocesos tienen el mismo recurso. Es decir, en circunstancias normales, el bloqueo compartido es solo un signo, y todos los subprocesos esperan a que se cumpla con este signo. Una vez satisfecho, todos los subprocesos se activan (equivalente a que todos los subprocesos obtengan el bloqueo). El bloqueo CountDownLatch aquí se basa en la realización del bloqueo compartido. Y obviamente, el identificador aquí es que el estado, etc.no es igual a cero, y el estado es en realidad cuántos subprocesos compiten por este recurso. Podemos ver anteriormente que son datos mayores que 0 pasados ​​a través del constructor, por lo que regresa aquí en este momento Siempre es -1.


Sync(int count) {
            setState(count);
        }

Cuando los datos devueltos por tryAcquireShared son menores que cero, significa que el recurso no ha sido adquirido y necesita ser bloqueado. En este momento, se ejecuta el código doAcquireSharedInterruptbly ():


privatevoid doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.SHARED);
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    break;
            }
        } catch (RuntimeException ex) {
            cancelAcquire(node);
            throw ex;
        }
        // Arrive here only if interrupted
        cancelAcquire(node);
        thrownew InterruptedException();
    }

Aquí primero agregue un nodo a la cola CLH en modo compartido, y luego verifique el nodo predecesor del nodo actual (los datos insertados están al final de la cola), si el nodo predecesor es el nodo principal y el contador actual es 0, despierte Nodo sucesor (despertar más tarde), de lo contrario, determine si bloquear, si es necesario, ¡bloquee el hilo actual! ¡Hasta que se despierte o se interrumpa!


privatefinalboolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

Tenga en cuenta aquí que el parámetro obj en LockSupport.park (Obj) es un objeto de supervisión bloqueado, no un objeto bloqueado. El objeto bloqueado es el hilo de la operación actual, por lo que el hilo correspondiente debe resolverse al desempacar. ¡No lo confunda!


publicstaticvoid park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        unsafe.park(false, 0L);
        setBlocker(t, null);
}
publicstaticvoid unpark(Thread thread) {
        if (thread != null)
            unsafe.unpark(thread);
    }

Echemos un vistazo a la implementación del método countDown correspondiente


publicvoid countDown() {
        sync.releaseShared(1);
    }

En primer lugar, cada vez que se ejecuta countDown, se ejecutará la operación de desbloqueo del método interno.


publicfinalboolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            returntrue;
        }
        returnfalse;
    }

Si el intento es exitoso, configure el nodo actual como el nodo principal y active los siguientes nodos del nodo correspondiente.


publicboolean tryReleaseShared(int releases) {
            // Decrement count; signal when transition to zero
            for (;;) {
                int c = getState();
                if (c == 0)
                    returnfalse;
                int nextc = c-1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }

De manera similar, el método de liberación del bloqueo también lo implementa la clase de sincronización dentro de CountDownLauch. Este método gira para detectar el número del contador actual. Si es igual a cero, significa que todos los subprocesos bloqueados previamente se han liberado. Devuelve falso directamente; de ​​lo contrario, CAS establece el contador actual y disminuye Vaya al número de cuenta regresiva. Si los datos son cero después de que la configuración es exitosa, significa que la ejecución se ha completado y el hilo bloqueado debe liberarse. Devuelva verdadero (tenga en cuenta que el sutil devuelve nextc == 0 aquí); de lo contrario, devuelva falso.
Echemos un vistazo al método releaseShared nuevamente. Cuando tryReleaseShared devuelve verdadero, significa que el contador ha llegado a cero y los recursos bloqueados deben liberarse. En este momento, ejecute el método unparkSuccessor (h) para activar el nodo principal de la cola.
Aquí, una cola delicada está diseñada para liberar los subprocesos bloqueados a su vez, en lugar de despertar todos los subprocesos directamente en un método similar a singleAll. ¿Entonces, cómo funciona? En nuestro código, solo se despierta el nodo principal (de hecho, es el nodo sucesor del nodo principal, y el nodo principal es solo un nodo vacío). Veamos primero la implementación de unparkSuccessor


privatevoid unparkSuccessor(Node node) {
        /*
         * Try to clear status in anticipation of signalling.  It is
         * OK if this fails or if status is changed by waiting thread.
         */
        compareAndSetWaitStatus(node, Node.SIGNAL, 0);
        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread);
    }

Obviamente, podemos ver que el parámetro entrante es el nodo principal. Después de configurar los datos a través de CAS, se despiertan los nodos posteriores del nodo principal (tenga en cuenta que el desempaquetado es el hilo en lugar del monitor de bloqueo). ¡Entonces volví!
¿Cómo se despiertan los subprocesos bloqueados restantes? Veamos la implementación de doAcquireSharedInterruptiblemente en el método de espera


privatevoid doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.SHARED);
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg); // tag 2
                    if (r >= 0) {
                        setHeadAndPropagate(node, r); // tag 3
                        p.next = null; // help GC
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())// tag 1
                    break;
            }
        } catch (RuntimeException ex) {
            cancelAcquire(node);
            throw ex;
        }
        // Arrive here only if interrupted
        cancelAcquire(node);
        thrownew InterruptedException();
    }

Anteriormente podemos ver que el bloque se ejecuta cuando se ejecuta parkAndCheckInterrupt (). Cuando despertamos el nodo sucesor del nodo principal (el primer nodo que entra en la cola), esta línea de código de tag1 se despierta, y después de la ruptura, continúa girando, y En este momento, la línea de código tag2 detecta que el contador ya es 0, por lo que el resultado devuelto por tryAcquireShared (arg) es 1 (todo lo devuelto antes es -1), r es mayor que cero, ingrese el código tag3 y tag3 establecerá el hilo actual como el extremo principal Apunte y luego continúe despertando nodos sucesores posteriores.


privatevoid setHeadAndPropagate(Node node, int propagate) {
        setHead(node); // tag 4
        if (propagate > 0 && node.waitStatus != 0) {
            /*
             * Don't bother fully figuring out successor.  If it
             * looks null, call unparkSuccessor anyway to be safe.
             */
            Node s = node.next;
            if (s == null || s.isShared())
                unparkSuccessor(node); // tag 5
        }
    }

Una vez que se despierta el nodo sucesor, continuará despertando al nodo sucesor y, a su vez, activará los datos de la cola.
Todo el CountDownLatch se ve así. De hecho, con los principios y la implementación de operaciones atómicas y AQS, es relativamente fácil analizar CountDownLatch.

Supongo que te gusta

Origin blog.51cto.com/15061944/2593716
Recomendado
Clasificación